NextJS
是一款基于 React 进行全栈开发的框架,是当下非常火的React全栈框架之一,在去年NextJS
发布了V13版本,而本文将基于V13版本的app
路由,来梳理它的几种不同的渲染方式的实现,并且与pages
路由做对比。
官方文档传送门:nextjs.org/docs
SSR也就是服务端渲染,页面在后端先获取到数据,然后发回前端注水渲染,如果你不是很熟悉,可以先看一下SSR相关的文章介绍。
在app路由下,只要我们的组件是使用 async
进行了修饰的,都会默认开启SSR.
export default async function PokemonName({ params }: { params: { name: string } }) {
const { name } = params;
const res = (await fetch('http://localhost:3000/api/pokemon?name=' + name)) as any;
const resdata = await res.json();
const { data } = resdata;
return (
//...
);
}
在pages
路由下,如果我们要开启SSR,需要实现getServerSideProps
这个API,在请求页面的时候,提前获取到数据,然后传入组件中。
export async function getServerSideProps(context: any) {
const data = await getPokemon(null, context.params.name);
return {
props: {
data: data,
},
};
}
const PokemonName = ({ data }: any) => {
return (
//...
);
};
SSG 也就是静态站点生成,在构建时生成静态页面,不同用户访问到的都是同一个页面。
在pages路由中,我们要实现SSG,需要先创建一个通用的模版文件,来表示所有的静态页面路由
[]
中的变量,就代表访问页面时传入的变量名称,然后我们需要实现generateStaticParams
这个方法
generateStaticParams
方法返回静态页面所有路由变量值的数组,假如使用的是[name]
这个变量做文件名,该方法就需要返回name
的所有情况
和pages不同的是,app路由不需要用特定的静态方法获取数据,只需要直接在组件中获取数据即可。
export async function generateStaticParams() {
return pokemon.map(({ name: { english } }) => ({
name: english,
}));
}
export default async function PokemonName({ params }: { params: { name: string } }) {
const { name } = params;
const res = (await fetch('http://localhost:3000/api/pokemon?name=' + name)) as any;
return (
//...
);
}
在pages路由中,我们需要实现getStaticPaths
和getStaticProps
这两个方法
[name]
这个变量,就需要返回name
的所有情况。// 获取所有二级路由
export async function getStaticPaths() {
return {
paths: pokemon.map(({ name: { english } }) => ({
params: {
name: english,
},
})),
};
}
// 返回匹配到后的数据
export async function getStaticProps(context: any) {
const time = new Date().toLocaleTimeString();
return {
props: {
data: pokemon.filter(({ name: { english } }) => english === context.params.name)[0],
time,
},
};
}
const PokemonName = ({ data, time }: any) => {
//...
};
SSG 的优点就是快,部署不需要服务器,任何静态服务空间都可以部署,而缺点也是因为静态,不能动态渲染,每添加一篇博客,就需要重新构建。所以有了ISR,增量静态生成,可以在一定时间后重新生成静态页面,不需要手动处理。
app路由实现ISR,需要利用到fetch
的缓存策略,在请求接口的时候,添加参数revalidate
,来指定接口的缓存时间,让它在一定时间过后重新发起请求。
export default async function PokemonName({ params }: { params: { name: string } }) {
const { name } = params;
// revalidate表示在指定的秒数内缓存请求,和pages目录中revalidate配置相同
const res = await fetch('http://localhost:3000/api/pokemon?name=' + name, {
next: { revalidate: 60 ,tags: ['collection']},
headers: { 'Content-Type': 'application/json' },
});
return (
//...
)
}
但是在通常情况下,我们的静态页面更新实际上没有那么频繁,但是有些情况有需要连续更新(发布博客有错别字),这个时候其实需要一种能手动更新的策略,来发布指定的静态页面。
NextJS提供了更新静态页面的方法,我们可以在 app
目录下新建一个 app/api/revalidate/route.ts
接口,用于实现触发增量更新的接口。
为了区分需要更新的页面,这里可以在调接口的时候传入更新的页面路径,也可以传入在fetch请求中指定的collection
变量。
import { NextRequest, NextResponse } from 'next/server';
import { revalidatePath, revalidateTag } from 'next/cache';
// 手动更新页面
export async function GET(request: NextRequest) {
// 保险起见,这里可以设置一个安全校验,防止接口被非法调用
//这里的process.env.NEXT_PUBLIC_UPDATE_SSG名字要与你设置在项目中的环境变量名字相同
if (request.query.secret !== process.env.NEXT_PUBLIC_UPDATE_SSG) {
return NextResponse.json(
{ data: error, message: 'Invalid token' },
{
status: 401,
},
);
}
const path = request.nextUrl.searchParams.get('path') || '/pokemon/[name]';
// 这里可以匹配fetch请求中指定的collection变量
const collection = request.nextUrl.searchParams.get('collection') || 'collection';
// 触发更新
revalidatePath(path);
revalidateTag(collection);
return NextResponse.json({
revalidated: true,
now: Date.now(),
cache: 'no-store',
});
}
假如我们数据库中的内容有修改,访问http://localhost:3000/api/revalidate?path=/pokemon/Charmander
就可以实现/pokemon/Charmander
这个路由的手动更新。
我们的静态页面在生成期间,如果用户访问对应路由会报错,这时需要有一个兜底策略来防止这种情况发生。
Nextjs在组件中指定了dynamicParams
的值(true默认),当dynamicParams设置为true时,当请求尚未生成的路由段时,我们的页面将通过SSR这种方式来进行渲染。
export const dynamicParams = true;
pages路由实现ISR需要在getStaticProps
方法中添加参数revalidate
,来指定周期时间重新生成静态页面。
export async function getStaticProps(context: any) {
const time = new Date().toLocaleTimeString();
return {
props: {
data: pokemon.filter(({ name: { english } }) => english === context.params.name)[0],
time,
},
// 当访问页面时,发现 20s 没有更新页面就会重新生成新的页面
revalidate: 20,
};
}
和app路由一样,pages路由也有手动更新策略。
pages路由实现增量生成和app路由类似,我们可以在 pages
目录下新建一个 pages/api/revalidate.ts
接口,用于触发增量生成。
export default async function handler(req: any, res: any) {
// 防止非法调用
if (req.query.secret !== process.env.NEXT_PUBLIC_UPDATE_SSG) {
return res.status(401).json({ message: 'Invalid token' });
}
try {
// 更新传入path路径对应的页面
await res.revalidate(req.query.path);
return res.json({ revalidated: true });
} catch (err) {
return res.status(500).send('Error revalidating');
}
}
比如我们修改了数据库中的内容,此时访问 https://localhost:3000/api/revalidate?secret=<token>&path=/pokemon/isauga
,便可让/pokemon/isauga
这个路由生成新的静态页面。
getStaticPaths
方法中还有一个参数 fallback
用于控制未生成静态页面的渲染方式。设置此变量后,我们可以指定路由未生成时的页面渲染内容,避免出现报错。
export async function getStaticPaths() {
return {
paths: pokemon.map(({ name: { english } }) => ({
params: {
name: english,
},
})),
//路由存在但是页面没有生成之前,给一个标志位
fallback: true,
};
}
const PokemonName = ({ data, time }: any) => {
const router = useRouter();
if (router.isFallback) {
return <div>Loading...</div>;
}
return (
//...
)
}
Server component 是 React18 提供的能力, 与上面的 SSR 不同,相当于是流式 SSR。
而以上每个步骤必须完成,才可以开始下一个步骤。
比如一个传统的博客页面采用 SSR 的方式使用 getServerSideProps
的方式渲染,那么就需要等 3 个接口全部返回才可以看到页面。
export async function getServerSideProps() {
const list = await getBlogList()
const detail = await getBlogDetail()
const comments = await getComments()
return { props: { list,detail,comments } }
}
如果评论接口返回较慢,那么整个程序就是待响应状态。
在 app 目录下的组件默认都是 React Server Components,如果你不想使用这个特性,可以在组件页面最上面添加use client
的修饰表示只使用客户端渲染或者SSR。
在pages目录下,可以使用 Suspense
开启流渲染的能力,将组件使用 Suspense
包裹。
import { SkeletonCard } from '@/ui/SkeletonCard';
import { Suspense } from 'react';
import Comments from './Comments';
export default function Posts() {
return (
<BlogList />
<section>
<BlogDetail />
<Suspense
fallback={
<div className="w-full h-40 ">
<SkeletonCard isLoading={true} />
</div>
}
>
<Comments />
</Suspense>
</section>
);
}
组件数据请求使用 use
API,就可以实现流渲染了。
import { use } from 'react';
async function fetchComment(): Promise<string> {
return fetch('http://www.example.com/api/comments').then((res)=>res.json())
}
export default function Comments() {
let data = use(fetchComment());
return (
<section>
{data.map((item)=><Item key={item.id}/>)}
</section>
);
}
当开启RSC后,整个渲染流程如下图。
如图所示,如果评论部分接口还在请求中,那么页面左侧注水完成,也是可以交互可以点击的。
感谢你能看到这里,本文梳理了NextJS两种路由下的不同渲染方式,希望对你有用,如果可以的话,不妨留个赞再走呢,这对我很重要。