1. 前言
此为《Next.js 14 App Router 海底捞针我帮你做了!》的下篇,主要讲解在 Next.js 中的网络数据请求,对路由有疑惑的可看上篇。
2. 路由中间件
在传统的后端框架(Express.js、Koa.js)中,我们可以使用中间件对进入站点的请求进行一些处理,比如身份认证、重定向、日志分析等业务逻辑。
鉴权的方案可以参考笔者的另一篇文章------Next.js 14 踩坑:处理 API 中的 JWT 登录认证问题
3. Fetch API 与数据请求
数据请求是每一个应用都具备的非常关键的功能之一。下面是在不同类型组件中请求数据的方式。
3.1 服务端组件
默认情况下,组件就是服务端组件,得到请求后会在服务端渲染成 HTML,然后给到客户端,然而这些页面不可能永远是静态的,服务器数据是会更新的。
- 使用 fetch
在 Next.js 中,原生的 fetch 得到了扩展,在请求服务端请求数据时,它允许你配置数据缓存(cache)和重新验证时间(revalidate),从而更新最新的页面显示。
下面是一个在服务端组件中使用 fetch 的例子:
tsx
// 请求数据
async function getData() {
const res = await fetch('https://api.example.com/...');
// The return value is *not* serialized
// You can return Date, Map, Set, etc.
if (!res.ok) {
// This will activate the closest `error.js` Error Boundary
throw new Error('Failed to fetch data');
}
return res.json();
}
// 注意:使用了数据请求的服务端组件需要加上 async
export default async function Page() {
const data = await getData(); // 可以将动态参数等传入到 getData 中
return <main></main>;
}
在不同的服务端组件中重复请求相同的数据并不会产生什么性能损耗,这是因为 Next.js 加入了自动记忆功能,这对性能优化来说是一种极大的提升,减少了大量的数据获取。
需要说明的是,在上篇中提到的 cookies、headers 也都可以在这里的请求中去使用,不过这些动态函数会使请求失去缓存性质。
通过设置 revalidate 可以修改缓存数据的时效性:
-
基于时间的重新验证
tsxfetch('https://...', { next: { revalidate: 3600 } });
- 使用第三方库
有的第三方库,例如:数据库、CMS 或是 ORM 客户端,它们并不支持 fetch,cache 和 revalidate 的设置就没有办法直接在 fetch 上配置了。
在 Next.js 中允许通过 React 的 cache
函数或 Route Segment Config Option 来配置,下面是一个示例:
tsx
// @/utils/get-item.ts
import { cache } from 'react';
export const getItem = cache(async (id: string) => {
const item = await db.item.findUnique({ id });
return item;
});
// app/item/[id]/layout.tsx
import { getItem } from '@/utils/get-item';
export const revalidate = 3600; // revalidate the data at most every hour
export default async function Layout({
params: { id },
}: {
params: { id: string };
}) {
const item = await getItem(id);
// ...
}
// app/item/[id]/page.tsx
// 同上一文件
- 用 cache 函数缓存数据请求
- revalidate 配置为 3600s,这意味着数据将会每隔 1 小时才更新一次
注意:数据是否被缓存取决于路由段(route segment)被静态或动态渲染。默认情况下,路由段是静态的,请求的结果将会被缓存。反之,如果是动态渲染(比如使用了动态函数 cookies、header 等),那么结果不会被缓存,每次渲染,都会重新请求。
3.2 客户端组件
当想要使用 use 相关的 hook 或想要添加点击等交互事件时,组件顶部要加上"use client"
,使之成为客户端组件。
- 通过路由处理器(Route Handler)
上篇提到的路由处理器在服务端运行,然后返回数据给客户端。当不想暴露 token 等敏感数据时,就可以用它来请求数据。
tsx
'use client';
import React, { useState, useEffect } from 'react';
export default function Page() {
const [blogs, setBlogs] = useState([]);
useEffect(() => {
fetch('/api/blogs') // 该接口来自路由处理器
.then((res) => res.json())
.then((data) => {
const sortedBlogs = data?.sort(
(a, b) => new Date(b.createdAt) - new Date(a.createdAt)
);
setBlogs(sortedBlogs);
});
}, []);
return <div>// 数据展示</div>;
}
注意:在服务端组件直接使用 fetch 获取数据即可,如上节。
- 使用第三方库
同样可以利用 SWR 来请求数据,就像直接编写 React 那样。
tsx
import useSWR from 'swr';
function Profile() {
const { data, error, isLoading } = useSWR('/api/user', fetcher);
if (error) return <div>failed to load</div>;
if (isLoading) return <div>loading...</div>;
return <div>hello {data.name}!</div>;
}
4. 渲染策略 SSR、SSG、ISR 的请求方式
上面的客户端组件本质上是 CSR,另外在 App Router 中,SSR、SSG、ISR 也换了马甲,差点不认识了。
4.1 SSR(Server-side Rendering,服务端渲染)
服务端请求接口、获取数据,渲染成 HTML 返回给前端。
在 Page Router 中使用 async getServerSideProps 实现 SSR,而 App Router 中是这样的:
tsx
async function getData () {
const res = await fetch(`https://...`, { cache: 'no-store' });
return await res.json();
}
export default async function Page() {
const list = await getData();
return (
// ...
)
}
通过配置 { cache: 'no-store' }
退出缓存,这样每次调用时会重新获取数据。
4.2 SSG(Static Site Generation,静态站点生成)
在构建阶段就将页面编译为静态的 HTML 文件,配合 CDN 缓存直接起飞。
在 Page Router 中的 SSG:
- 不请求数据时就是 SSG,但很多还是需要请求数据的
- 页面内容需要获取数据(单个的静态页面生成)------async getStaticProps
- 页面路径需要获取数据(成组的静态页面生成)------async getStaticPaths
而 App Router 中是这样的:
tsx
async function getData () {
const res = await fetch(`https://...`, { cache: 'force-store' });
return await res.json();
}
export default async function Page() {
const list = await getData();
return (
// ...
)
}
{ cache: 'force-store' }
是 fetch 中默认的缓存方式,每次请求时将从缓存中读取数据。(如果使用了动态函数,则会变为 no-store
,因此不必担心数据被静态化)
4.3 ISR(Incremental Static Regeneration,递增式静态再生)
在一个主体内容不变的博客页面中,点赞、评论等功能则是动态变化的,这种场景下就可以用 ISR。
在 Page Router 中使用 async getStaticProps + revalidate 实现 ISR,可以理解为定时更新的静态页面。
而 App Router 中是这样的:
tsx
async function getData () {
const res = await fetch(`https://...`, { next: { revalidate: 10 } });
return await res.json();
}
export default async function Page() {
const list = await getData();
return (
// ...
)
}
通过设置 { next: { revalidate: 10 } }
使之定时更新,从而实现 ISR。
5. Meta 元数据
动态设置网页标题并且为了更好的 SEO。
5.1 静态元数据
tsx
// layout.tsx | page.tsx
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: '...',
description: '...',
};
export default function Page() {}
5.2 动态元数据
tsx
// app/blogs/[id]/page.tsx
export async function generateMetadata({
params,
searchParams,
}: {
params: { id: string };
searchParams: { [key: string]: string | string[] | undefined };
}): Promise<Metadata> {
const response = await getData(params.id);
const blog = response.data;
return {
title: '博客详情-' + blog.name,
};
}
// 请求异步数据
async function getData(id: string) {
const res = await fetch(`${api}/blog/${id}`, { cache: 'no-store' });
return await res.json();
}
// ...
6. <Image>
组件
tsx
import Image from 'next/image';
// ...
<Image src={blog.src} width={500} height={500} alt="image" />;
假设 blog.src
不是本站的图片,则需要在 next.config.js 中添加外部图片地址:
tsx
// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.unsplash.com',
// port: '',
// pathname: '',
},
],
},
};
export default nextConfig;
7. 总结
总之,组件被分为服务端组件和客户端组件,辅以数据缓存和验证,当然不是说缓存越多越好,有些需要根据查询参数动态获取的内容当然不能固执地去使用缓存。
作为开发全栈应用的 React 框架,Next.js 一直在不断优化和提高中,体验感还是很好的。除了本文提到的内容,Next.js 还有一些大家可以自由探索,比如:Server Actions、身份认证、国际化等。
非要说瑕疵,可能就是文档比较复杂,没有中文文档,笔者根据路由和数据请求两条线挖掘文档,才有这些内容,如果掌握了这些去开发一个应用完全没问题。