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>
</>
);
}
这里要注意的是,
- 所有服务器组件都带有后缀
server.{js,jsx,ts,tsx)
(至少目前如此)。 - 由于它们没有发送到客户端,我们可以拥有访问服务器资源(如数据库、内部 API 等)的代码。
- 由于所有这些都在服务器端进行,因此您为渲染 Markdown 而导入的包不会被发送到客户端,而只会发送渲染后的内容。这显著减少了客户端 JS 包的大小。
组件本身很简单,它从数据库中获取数据并呈现内容。
渲染格式
如果你注意到,我说过内容被渲染了,而不是HTML
。这是因为服务器组件不是渲染为 HTML,而是渲染为中间格式。
如果上述组件是您的应用程序中唯一的组件,那么这就是从服务器返回的内容。
J0: [
["$", "h1", null, {
"children": "Blog 1"
}],
["$", "p", null, {
"children": "unt aut..."
}]
]
如您所见,只有渲染的 markdown 被发送到客户端,而不是库本身。
现在你可能会想,为什么不用 HTML 和这种格式?(我不知道这个格式叫什么……🙃)。下一节我们来解释一下原因。
状态以及与SSR的区别
让我们看看服务器组件和 SSR 之间的主要区别。SSR 在服务器上生成 HTML,然后将其发送到客户端由浏览器渲染。这意味着内容本身是静态的,并且无法使用交互式标记。
然而,由于服务器组件使用这种中间格式而非 HTML,因此允许它们拥有具有交互行为的客户端组件。毫无疑问,服务器组件本身不能拥有状态或事件处理程序,换句话说,它们不能使用等useState
。useEffect
但是,它们可以拥有客户端组件,而客户端组件又可以拥有状态。
让我们在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.
</>
);
}
// 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>;
}
服务器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.
))}
</>
);
}
我们来更新组件,用props 中的LikeButton
替换状态变量。我们还添加一个回调函数,它会访问服务器来更新特定博客文章的数量。likes
likes
likes
// 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>;
}
当你点击“赞”按钮时,会调用服务器来更新点赞计数,然后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)}
/>
);
}
在某篇博文的评论文本框中输入一些内容。现在,点击任意一个点赞按钮。您可以看到,尽管点赞计数更新会渲染整个树,但客户端组件的状态在更新过程中会保留。因此,您在评论框中输入的内容将保持不变,不会被清除。这是服务器组件的最大优势之一,也是与传统服务器端渲染 (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>
);
}
// 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}>
))}
</>
);
}
原因很简单,客户端组件会被发送到客户端。如果它包含一个访问某些内部 API 的服务器组件,那么客户端就会失败,因为它无法访问。这只是众多原因中的一个。
相反,我们可以执行以下操作。
可能的
// FancyBlogPost.client.js - A Client component.
export default function FancyBlogPost({ children }) {
return (
<div className="fancyEffects">
{ children }
</div>
);
}
// 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>
))}
</>
);
}
这很好,因为从客户端组件的角度来看,内容已经作为父服务器组件的一部分在服务器中呈现,并且只有呈现的内容作为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