跳到主要内容

Next.js App Router 经验总结

· 阅读需 8 分钟
Skyone
科技爱好者

写了很多 Next.js 的代码,总结了一下 Server Component 和 Client Component 的规律,希望对后来者有用。如果有错误,欢迎大佬指正!

如何区分Client Component?

我总结了以下规律,可以快速区分一个组件是否为服务端组件:

  1. 默认导出(export default)为 async 函数的组件为 Server Component。
  2. 标记了 use client; 的源文件里的组件都是 Client Component。
  3. 其余情况下,组件既是 Server Component 也是 Client Component。也就是说,既可以在服务端运行,又可以在客户端运行。为了方便描述,下面我将这种组件称为 Server/Client Component

我们知道 Next.js 官方说的是 “在 App Router 下不标明 use client 的为 Server Component”,但是我强烈建议**任何不应该被发送到客户端的 Server Component 都加上 async ,即使该组件没有用到异步逻辑!**原因下一节提到。

正确使用 Server Component

我们都知道,Server Component 不能访问浏览器 API(如 window 对象),也不能使用常规 React Hook,这一点理所当然,不在这里赘述。

顾名思义,Server Component 就是在服务端运行的组件,可以访问浏览器端不存在的Node API,例如连接数据库等。

这些服务端操作通常是异步的,所以 Next.js 允许我们的 Server Component 返回 Promise,而 Client Component 则不行。因此,加上 async 可以保证我们的组件(以及组件内部的变量)永远不会被发送到 Client。如下面的例子:

interface User {
name: string;
email: string;
}

interface Props {
user: User;
content: string;
}

async function UserComment({comment}: Props) {
const {user, content} = comment;
return (
<dir>
<p>{user.name}</p>
<img src={gravatarUrl + tools.md5(user.avatar)}/>
<p>{content}</p>
</dir>
);
}

显然,这个函数没有用到异步逻辑,但是,我将其标为 async ,为什么呢?我们不能把评论者的 email 暴露出来,只是通过 email 的 md5 显示 gravatar 头像。

如果不添加 async ,而这个组件因为某种原因(后面会将)被当作 Client Component,这个组件的参数当然也会一并被发送到浏览器,评论者的邮箱就暴露了!

而一旦添加了 async ,这个组件就只能是 Server Component,里面的数据不存在泄漏到客户端的风险。

还有一点,上面的例子中,tools.md5 一般都是 Node API 实现的,显然只能在 Server Component 中运行,添加 async 也能帮助你排除错误。一旦你试图在 Client Component 里使用 Server Component,Next.js 就会立刻向你报错。而当你在 Client Component 里使用 Server/Client Component (既可以作为 Server Component 又可以作为 Client Component 的组件)时,Next.js 只会在运行时向你报错“我在浏览器里找不到 crypto 包啊”。

正确使用 Browser API

何时使用浏览器API?Next.js 官方告诉我们,只有在标记了 "useclient" 的组件里才能使用浏览器 API。然而我觉得这句话并不十分正确,就像下面的代码一定会报错:

"use client";

function Demo() {
// 找不到 window 对象
window.alert("233")

return <p>233</p>
}

为什么报错?所谓 Client Component 并不是一定在浏览器里运行的,实际上,Client Component的首次渲染在服务端,Node当然找不到 window.alert。Next.js 的对 Client 的处理是:只运行一遍主要逻辑,不运行Hook。也就是说,useEffect 里的函数是确确实实在浏览器运行的,因此,这段代码应该改成:

"use client";

function Demo() {
useEffect(() => {
window.alert("233");
});

return <p>233</p>
}

我相信下面这个例子一定能帮你理解:

"use client";

function Counter() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => setCount(prev => prev + 1), []);

useEffect(() => {
console.log("始终在浏览器终端");
});

console.log(`count = ${count}`);

return (
<div>
<p>{count}</p>
<button onClick={}>Click me</button>
</div>
);
}

直接放到 /src/app/page.tsx ,打开网页并点几下按钮,观察一下控制台。

其中服务端控制台会显示一次 count = 0,只会的都显示在浏览器控制台。而 "始终在浏览器终端" 则只会在浏览器控制台显示。

除了 Hook,浏览器 API 还能在一切”副作用“中使用。什么是副作用呢?比如说用户点击按钮产生的事件、监听用户鼠标移动产生的事件等等。也就是说,onClickonChange 等里面也可以使用浏览器 API。

相互调用

Server Component 能不能调用 Client Component?反过来行不行?答案是:Server Component 能调用 Client Component,Client Component 不能直接调用 Server Component。

还是以例子来说明,例如下面的 Server Component 能调用 Client Component:

async function SomeServerComponent() {
// ...
return <SomeClientComponet props={props}/>;
// props 不能传递 function 等不可序列化的数据
}

下面的 Client Component 不能调用 Server Component:

"use client";

function SomeClientComponent() {
// ...
return <SomeServerComponet/>; // ❌ 错误
}

而Client Component 能使用作为参数传递的已经实例化的 Server Component:

// SomeServerComponent.tsx
export default async function SomeServerComponent() {
return <p>SomeServerComponent</p>;
}

// SomeClientComponent.tsx
"use client";

interface Props {
children: ReactNode;
}

export default function SomeClientComponent({children}: Props) {
return (
<DataProvider>
{children}
</DataProvider>
);
}

// somepage/layout.tsx
export default async function SomePageLayout() {
return (
<SomeClientComponent> {/* SomePageLayout 使用 Client Component */}
<SomeServerComponent/>
{/* SomePageLayout 使用 Server Component 并将结果传递给 Client Compoent */}
</SomeClientComponent>
)
}

例子中 SomePageLayout 使用 Server Component 并将结果传递给 Client Compoent,因为ReactNode本身是可以序列号的,所以 SomePageLayout 作为 Server Component 可以调用 Server Component 并将其结果序列化后传递给 Client Compoent。

Server/Client Component

最后来讲讲我第一节定义的 Server/Client Component,它是既可以作为 Server Component 又可以作为 Client Component 的组件,可以实现服务端客户端的逻辑共用,但代价是什么呢?Server/Client Component 继承了 Server Component 和 Client Component 的全部限制

具体来说:Server/Client Component 不能使用 Browser API 和 Hook(因为可以在服务端运行),不能使用 Node API(因为可以在客户端运行)。

也就是说,Server/Client Component 只能做从数据到 dom 树的转化,不能获取数据,不能拥有状态,也不能持有包含隐私的数据。

正是因为这么多限制,Server/Client Component 可以在 Server Component 和 Client Component 中随意使用。它就是个纯函数,或者说从 data 到 ReactNode 的 Map。

【完】