服务端标记

使用了 SSR 技术的 React 应用在开发的时候,与传统的 React 应用没有什么两样。虽然在之前的章节中也提到了有些组件是运行在客户端的,有些组件是运行在服务端的,但是从整体上说,所有的组件还都是要在服务端运行生成所需的 HTML 片段,然后传递给客户端进行水合处理。

从这个原理来看,每个组件实际上都需要在服务端运行一遍。但是对于需要在客户端运行的一些功能来说,可能它们并不需要在服务端运行。这时就需要使用 React 提供的服务端标记来对应用中的组件和函数进行标记,显式声明它们所需要运行的环境。

React 提供的服务端标记都是一个普通的字符串字面量,将其当做一个语句直接放置在所需要的位置即可。

'use client'标记

React 项目在编译的时候,往往会被分割成数个独立的文件,React 提供的服务端标记'use client'就构建了这样一个服务端和客户端之间的边界。

'use client'标记一般放置在一个 JSX 文件的顶部,被'use client'标记的文件,其中的所有import都会被作为客户端模块对待,在渲染的时候会使用客户端渲染的方式进行。

例如有这样一个简单的应用,由App.tsxComments.tsxComment.tsxfixed-comments.ts几个文件组成,以下是它们的源码。

// App.tsx
import { Comments } from "./Comments";

export function App() {
  return <Comments />;
}
// Comment.tsx
export function Comment({ content }) {
  return <div>{content}</div>;
}
// fixed-comments.ts
export const commentContents = [
  "Comment 1",
  "Comment 2",
  "Comment 3",
  "Comment 4",
];
// Comments.tsx
"use client";

import { Comment } from "./Comment";
import { commentContents } from "./fixed-comments";

export function Comments() {
  return (
    <ul>
      {commentContents.map((content, index) => (
        <li key={index}>
          <Comment content={content} />
        </li>
      ))}
    </ul>
  );
}

在上述四个示例文件中,Comments.tsx文件的首行使用'use client'进行了标记,这就意味着它这个文件中的所有import进来的内容都将在客户端完成渲染。React 对待客户端渲染和服务端渲染分界可以使用以下示意图来说明。

服务端和客户端之间的分界

图里蓝色的部分是在服务端完成渲染的部分,绿色的部分则是在客户端完成渲染的部分。经过Comments.tsx文件中的标记'use client',其引入的所有内容都将在客户端处理。

Caution

在这个简单的示例里组件Comment既可以在服务端完成渲染,也可以在客户端完成渲染,那么对于这种组件的处理,React也是会遵循一定的规律的。对于这样的组件在何处完成渲染是通过其定义位置和引用位置来决定的。如果一个组件在使用'use client'标记的文件中被引用,那么它将变成一个客户端渲染的组件,否则它就还是一个服务端组件。

一定要牢记'use client'标记的是模块之间的依赖树,不是React的渲染树。如果一个组件是利用children参数传入一个客户端组件里使用的,而不是通过import在客户端组件中使用的,那么这个组件就依旧是一个服务端组件,在服务端完成渲染。

'use server'标记与服务端函数

'use server'标记和用于标记服务端和客户端分界的'use client'标记是不一样的。'use server'是用来标记需要在服务端运行的函数的。

那么一个函数在客户端运行和在服务端运行有什么区别呢?

在一个 React 应用中,往往有大量的函数,这些函数往往是以事件处理函数、数据获取函数等身份存在的,而且其中大部分都是异步函数。对于一个应用来说,这些函数之间完全没有区别,都是应用的组成部分,获取或处理了应用的数据。但是对于开发者来说,这些函数都是不相同的,例如有些函数处理的都是客户端的数据,有些则需要与远程服务交互完成数据的处理。

在开始使用 SSR 之后,之前需要与远程服务交互的函数可能会变身成为直接访问服务器资源的函数。这样的函数就完全无法在客户端运行了。但是 React 对它们是无法分辨的。'use server'标记就是被提出来用来标记这些不能在客户端运行的函数的。使用了 SSR 的 React 应用在编译的时候遇到'use server'标记,就会自动将其放入服务端程序中,从而避免其在客户端运行。

'use server'标记的使用非常简单,跟'use client'标记一样,都是独自占据一行并且要在函数体的第一行书写。例如现在编写一个允许直接在服务端处理表单的示例。

async function handleFormSubmit(data: FormData) {
  "use server";

  const mark = data.get("mark");
  const result = await db.marks.insertOne({ mark });
  // 省略其他需要访问服务端资源,例如数据库的操作
}

function MarkForm() {
  return (
    <form action={handleFormSubmit}>
      <input type="text" name="mark" />
      <button type="submit">Submit</button>
    </form>
  );
}

'use server'标记除了可以在函数体的第一行书写,也可以书写到文件的第一行,但是这样一来,就要求这个文件里包括的内容只能是在服务端运行的函数。例如上面的示例可以拆分成两个文件。

// server-function.ts
"use server";

export async function handleFormSubmit(data: FormData) {
  const mark = data.get("mark");
  const result = await db.marks.insertOne({ mark });
  // 省略其他需要访问服务端资源,例如数据库的操作
}
// form.tsx
import { handleFormSubmit } from "./server-function";

export function MarkForm() {
  return (
    <form action={handleFormSubmit}>
      <input type="text" name="mark" />
      <button type="submit">Submit</button>
    </form>
  );
}

Caution

在SSR应用中编写函数功能时,一定要注意函数运行环境的区分,尽可能使用服务端标记标记所有需要在服务端运行的函数,第一是为了加速编译,第二也是为了确保SSR应用的运行正确。