React - 服务器组件 - 简介和初步想法 服务器组件 客户端组件 共享组件 想法和结论

2025-05-26

React - 服务器组件 - 简介和初步想法

服务器组件

客户端组件

共享组件

思考与结论

就在圣诞节前夕,React 团队提前送出了一份圣诞礼物——服务器组件,也就是零包大小组件。让我们来看看它是什么,它能带来什么,以及我的想法。

在开始之前,我想先告诉大家,想要深入了解 React,最好的资源显然是RFC和React 团队的介绍视频。我整理这些资源是为了方便时间充裕的朋友,也为了分享我的想法和理解。

您可以在这里找到这篇文章的完整源代码。它是 React 团队实际演示代码库的 fork。我只是简化了组件以便于理解。所有荣誉都归于 React 团队。

随着服务器组件的引入,现有组件已重命名为客户端组件。实际上,我们现在有三种类型:

  • 服务器组件
  • 客户端组件
  • 共享组件

服务器组件

让我们看一下服务器组件的一些重要特性。

零捆绑尺寸

它们的包大小为零,因为它们在服务器上渲染,并且只有渲染的内容发送到客户端。这意味着它们不会增加客户端 JS 包的大小。我们来看一个例子:

// BlogPost.server.js - A Server component.

import { renderMarkDown } from '...'; // Server only dependency.
import {getBlogPost} from './blog/blog-api';

export default function BlogPost({blog}) {
  const blog = getBlogPost(blog.id); // Get blog post from database directly.

  return (
    <>
      <h1>{blog.title}</h1>
      <p>{renderMarkdown(blog.markdown)}</p>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

这里要注意的是,

  • 所有服务器组件都带有后缀server.{js,jsx,ts,tsx)(至少目前如此)。
  • 由于它们没有发送到客户端,我们可以拥有访问服务器资源(如数据库、内部 API 等)的代码。
  • 由于所有这些都在服务器端进行,因此您为渲染 Markdown 而导入的包不会被发送到客户端,而只会发送渲染后的内容。这显著减少了客户端 JS 包的大小。

组件本身很简单,它从数据库中获取数据并呈现内容。

渲染格式

如果你注意到,我说过内容被渲染了,而不是HTML。这是因为服务器组件不是渲染为 HTML,而是渲染为中间格式。

如果上述组件是您的应用程序中唯一的组件,那么这就是从服务器返回的内容。

J0: [
    ["$", "h1", null, {
        "children": "Blog 1"
    }],
    ["$", "p", null, {
        "children": "unt aut..."
    }]
]
Enter fullscreen mode Exit fullscreen mode

如您所见,只有渲染的 markdown 被发送到客户端,而不是库本身。

现在你可能会想,为什么不用 HTML 和这种格式?(我不知道这个格式叫什么……🙃)。下一节我们来解释一下原因。

状态以及与SSR的区别

让我们看看服务器组件和 SSR 之间的主要区别。SSR 在服务器上生成 HTML,然后将其发送到客户端由浏览器渲染。这意味着内容本身是静态的,并且无法使用交互式标记。

然而,由于服务器组件使用这种中间格式而非 HTML,因此允许它们拥有具有交互行为的客户端组件。毫无疑问,服务器组件本身不能拥有状态或事件处理程序,换句话说,它们不能使用等useStateuseEffect但是,它们可以拥有客户端组件,而客户端组件又可以拥有状态。

让我们在BlogPost组件中添加一个“赞”按钮,单击该按钮时会增加博客文章的赞数。

// BlogPost.server.js - A Server component.

import {getBlogPost} from './blog/blog-api';
import LikeButton from './LikeButton.client';

export default function BlogPost({blog}) {
  const blog = getBlogPost(blog.id);
  return (
    <>
      <h1>{blog.title}</h1>
      <p>{blog.markdown}</p>
      <LikeButton blog={blog} /> // A Client component.
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode
// LikeButton.client.js - A Client component.

import {likeBlogPost} from './blog/blog-api';
import React from 'react';

export default function LikeButton({blog}) {
  const [likesCount, setLikesCount] = React.useState(blog.likes);

  const handleClick = () => {
    setLikesCount(prev => prev + 1);
  };

  return <span onClick={handleClick}>Likes: {blog.likes}</span>;
}
Enter fullscreen mode Exit fullscreen mode

服务器BlogPost组件有一个子组件LikeButton,它是一个处理用户交互的客户端组件。由于它是一个客户端组件,因此LikeButton可以自由使用useState,并且它还会在点击时更新本地状态。

因此,服务器组件本身不能具有状态,但它可以利用客户端组件来维护状态并处理用户交互。

请注意,该LikeButton组件仅更新本地点赞计数,而不更新服务器中的计数。此示例的目的是展示客户端组件可以具有状态和用户交互。

州树

为了理解这一点,让我们扩展我们的示例,使其拥有一个BlogPostList服务器组件,该组件使用我们的服务器组件呈现博客列表BlogPost

// BlogPost.server.js - A Server component.

import {getBlogPosts} from './blog/blog-api';
import BlogPost from './BlogPost.server';

export default function BlogPostsList() {
  const blogs = getBlogPosts();
  return (
    <>
      {blogs.map((blog) => (
        <BlogPost blog={blog} /> // Uses a server component.
      ))}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

我们来更新组件,用props 中的LikeButton替换状态变量。我们还添加一个回调函数,它会访问服务器来更新特定博客文章的数量。likeslikeslikes

// LikeButton.client.js - A Client component.

import {likeBlogPost} from './blog/blog-api';

import React from 'react';
import {useLocation} from './LocationContext.client'; // Experimental API for POC.

export default function LikeButton({blog}) {
  const [, setLocation] = useLocation();
  const handleClick = async () => {
    await likeBlogPost(blog.id);
    setLocation((loc) => ({
      ...loc,
      likes: blog.likes + 1,
    }));
  };

  const likeBlogPost = async (id) => {
    // Fetch call to update the blog post in the server.
  };

  return <span onClick={handleClick}>Likes: {blog.likes}</span>;
}
Enter fullscreen mode Exit fullscreen mode

当你点击“赞”按钮时,会调用服务器来更新点赞计数,然后setLocation调用。这是 React 团队提供的实验性 API,用于模拟调用服务器来获取 UI 单元。在本例中,我们获取的是当前路由的组​​件树。你可以在“网络”选项卡中看到确实进行了调用,并且返回了当前路由中从根节点开始的所有组件。

替代文本

整个树从根节点开始渲染,更新的部分会进行渲染,在本例中,所有likes显示在屏幕上的部分都会被渲染。请注意,更新调用是从LikeButton组件发出的,但是由于整个树都会更新,likes因此传递给 的计数prop也会LikeButton更新。

客户端组件的状态得到维护

让我们创建一个新Comment组件,用于渲染一个绑定到状态变量的输入文本字段。为了简单起见,我们不会实现评论功能。

// Comment.client.js - A Client component.

import React from 'react';

export default function Comment() {
  const [comment, setComment] = React.useState('');
  return (
    <input
      value={comment}
      onChange={({target: {value}}) => setComment(value)}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

在某篇博文的评论文本框中输入一些内容。现在,点击任意一个点赞按钮。您可以看到,尽管点赞计数更新会渲染整个树,但客户端组件的状态在更新过程中会保留。因此,您在评论框中输入的内容将保持不变,不会被清除。这是服务器组件的最大优势之一,也是与传统服务器端渲染 (SSR) 的主要区别。

替代文本

客户端组件

客户端组件是我们一直以来都在使用的组件。但是,如果使用了服务器组件,你需要记住一件事:

客户端组件无法导入服务器组件,但是它可以渲染从服务器组件作为子组件传入的服务器组件。请注意,这些 props 必须是可序列化的,而 JSX 是可序列化的,因此可以传入。

不可能

// FancyBlogPost.client.js - A Client component.
import React from 'react';
import BlogPost from './BlogPost.server';

export default function FancyBlogPost({ blog }) {
  return (
    <div className="fancyEffects">
      <BlogPost blog={blog} /> // Not OK. Cannot import a Server component inside a Client component.
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode
// BlogPostList.server.js - A Server component.
import {getBlogPosts} from './blog/blog-api';
import BlogPost from './BlogPost.server';

export default function BlogPostsList() {
  const blogs = getBlogPosts();
  return (
    <>
      {blogs.map((blog) => (
        <FancyBlogPost blog={blog}>
      ))}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

原因很简单,客户端组件会被发送到客户端。如果它包含一个访问某些内部 API 的服务器组件,那么客户端就会失败,因为它无法访问。这只是众多原因中的一个。

相反,我们可以执行以下操作。

可能的

// FancyBlogPost.client.js - A Client component.
export default function FancyBlogPost({ children }) {
  return (
    <div className="fancyEffects">
      { children }
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode
// BlogPostList.server.js - A Server component.
export default function BlogPostsList() {
  const blogs = getBlogPosts();
  return (
    <>
      {blogs.map((blog) => (
        <FancyBlogPost>
          <BlogPost blog={blog} /> // Fine. Server component passed as childredn to a Client component.
        </FancyBlogPost>
      ))}
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

这很好,因为从客户端组件的角度来看,内容已经作为父服务器组件的一部分在服务器中呈现,并且只有呈现的内容作为prop客户端组件传递。

关于客户端组件需要记住的其他事项,

  • 它们以扩展结束*.client.{js,jsx,ts,tsx}(至少目前如此)
  • 它们将成为客户端软件包的一部分,因此,您不应进行任何您不想公开的操作。例如:数据库操作等。
  • 他们可以自由使用状态和效果挂钩。
  • 仅使用浏览器 API。

共享组件

共享组件可以渲染为服务器组件或客户端组件。这取决于导入它的组件。由于它既可以用作服务器组件,也可以用作客户端组件,因此限制也最多。

  • 它们没有特定的后缀。
  • 他们不可能拥有state
  • 他们不能利用useEffect等等。
  • 它们无法呈现服务器组件。
  • 他们不能使用特定于浏览器的 API。

由于所有这些限制,这些组件只能用于显示作为 prop 传递给它的内容。

思考与结论

读完这篇文章后,如果你认为服务器组件的功能与 NextJS/SSR 完全相同,那么答案是否定的。对于 NextJS 来说,组件确实在服务器端渲染,但最终,这些组件会成为客户端 bundle 的一部分,并用于数据融合 (hydration)。此外,服务器组件还允许:

  • 维护客户端组件状态。
  • 客户端和服务器组件的更精细集成。例如,在 NextJS 中,您只能通过页面来选择客户端和服务器组件。
  • 代码拆分是根据文件名完成的,现在不再是开发人员需要作为导入执行的额外步骤。

当然,还有一些部分正在开发中,比如路由之类的,但我对服务器组件带来的变化感到非常兴奋。它们为开发人员提供了灵活性,可以根据需求在客户端和服务器组件之间进行选择,从而实现两全​​其美。

希望我能够以一种通俗易懂的方式解释一些概念。祝大家编程愉快,下期再见。:)

在Twitter上关注我或访问我的网站以了解有关我的更多信息..✨

文章来源:https://dev.to/sidthesloth92/react-server-components-initial-thoughts-3lml
PREV
关于我如何使用 DEV.to 和 NextJS 基本规则构建我的投资组合和博客的故事开始设计技术开发结论
NEXT
通知 API:显示来自您的 Web 应用的通知🔔