React 的未来 - Server Components





0/5 (0投票)
一项新的实验性功能 React Server Components 允许在服务器上渲染组件
在最近的一次演讲中,React 团队宣布了一项名为 React Server Components (RSC) 的新功能。这究竟是什么,我们如何利用它来编写更好的应用程序?
如果您熟悉 React,您就会知道它是一个客户端库,它在 JavaScript 之上提供了一套抽象,可以快速有效地将用户界面写入 Web 应用程序。客户端库意味着使用 JavaScript 在客户端的浏览器中完成视图在 DOM 中的渲染。在这种情况下,服务器仅负责提供包含 HTML、CSS 和 JavaScript 的应用程序包,而不执行任何渲染。
服务器会发送一个 HTML 响应,其中包含一个空的 body
和 script
标签,这些标签引用 head 中的 JavaScript 包。这意味着在页面其余部分开始加载之前,必须先将 JavaScript 文件下载到用户的浏览器。这有两个显著的缺点:
- 初始加载时间增加,性能下降
- SEO 效果差,因为许多网络爬虫无法解析和读取 JavaScript 文件中的内容
加载初始 JavaScript 文件后,可以异步加载内容。首先加载关键内容,然后稍后加载非关键内容,但这仍然会导致性能问题。为了解决这些性能问题,开发人员会通过最小化、代码拆分、消除死代码等方法来减小 React 应用程序的包大小。然而,这通常还不够。
在本文中,我们将深入探讨 React Server Components,这是一项实验性功能,可以帮助您克服这些性能障碍。
React Server Components
根据 Google 的研究,如果网页未能在三秒内加载,53% 的移动网站访问者将会离开。您可以明白为什么这对于使用 React 或 Angular、Vue 等现代前端框架构建的应用程序来说是一个问题。
然而,存在一种有效的解决方案。借助 服务器端渲染 (SSR),我们可以在服务器上将 React 组件渲染为 HTML。服务器端渲染的概念并不新鲜。它随着现代客户端 JavaScript 密集型库和框架的出现而出现,这些库和框架在客户端执行大部分渲染工作。
SSR 渲染的工作方式是在服务器上渲染应用程序的一部分,并将其作为 HTML 发送。浏览器会立即开始绘制 UI,而无需等待 JavaScript 算法在显示初始内容给用户之前将视图渲染到 DOM。这通过提高 用户感知的性能 来改善用户体验。
React 是基于组件的。您必须将 UI 编写为一组具有父子关系的组件。这些组件可以是像 React Hooks 这样的函数,也可以是扩展内置 Component
类的类。
React Server Components 是普通的 React 组件,但由服务器而不是客户端渲染。这项技术使开发人员能够从服务器获取已渲染的组件。由于我们已经有了开发人员使用的 SSR 技术,并且有许多出色且易于使用的工具 — 例如 Nest.js、Gatsby 甚至 Express.js — React Server Components 有什么独特之处?
注意:Next.js 是一个流行的框架,可以轻松创建服务器端渲染的 React 应用程序,而无需自行配置。
乍一看,RSC 似乎与常规的服务器端渲染相同,但它为编写具有额外好处的应用程序打开了大门,例如:
- 对最终包大小没有影响
- 直接访问后端资源
- 使用 React IO 库,例如
react-fs
(文件系统)、react-pg
(Postgres)、react-fetch
(Fetch API) - 对客户端必须下载的组件进行精细控制
对最终包大小没有影响意味着 RSC 允许您的 React 应用程序使用第三方实用程序库,而不会影响客户端的包大小。这怎么可能?
让我们以一个 服务器组件 为例:
import marked from 'marked';
import sanitizeHtml from 'sanitize-html';
// [...]
export default function TextWithMarkdown({text}) {
return (
<div
className="text-with-markdown"
dangerouslySetInnerHTML={{
__html: sanitizeHtml(marked(text), {
allowedTags,
allowedAttributes,
}),
\}}
/>
);
}
此组件导入两个外部库:marked
和 sanitize-html
。如果您将其用作客户端组件,最终包也会包含这两个库。它们是 sanitizeHtml(marked(text), {})
调用所必需的,用于清理并转换传入的文本为 Markdown。借助 RSC,服务器执行代码。服务器仅返回最终转换后的文本。在运行时不需要这些库,因此不包含在内!
那么,关于直接访问服务器资源和 React IO 库呢?服务器资源可以从文件到功能齐全的数据库,这对于构建全栈数据驱动的应用程序至关重要。
RSC 仍处于研究阶段,但这表明我们可以使用 React 构建全栈应用程序,其工作方式与传统应用程序相同。您可以使用服务器组件与服务器上的数据库和文件系统进行交互,并将结果返回给客户端。这意味着您可以选择避免使用 REST 或 GraphQL API 来在客户端和服务器之间交换数据!
在构建业务应用程序时,我们通常必须使用数据库。借助 React Server Components,我们可以从服务器上运行的 React 应用程序部分访问此数据库,并将结果连同渲染的组件本身一起返回给客户端,而不是像完全客户端的 React 应用程序那样只发送 JSON 数据。
借助 RSC,我们可以以旧的应用程序架构构建 Web 应用程序,同时仍然拥有现代化的 UI。对于不想学习 REST 或 GraphQL 但仍希望构建不仅仅使用一种语言(JavaScript)而且只使用一个库的完整应用程序的初学者来说,React 使其比过去更容易,那时您必须使用 PHP、HTML 和 JavaScript 来构建全栈应用程序。
React 团队与其他团队合作,通过 webpack 插件将此功能实现到 Next.js 和 Gatsby 等元框架中。但是,如果您愿意,这并不意味着您不能在没有这些工具的情况下使用该功能。
在 SSR 中,我们将组件渲染为 HTML 并将结果发送到客户端。React Server Components 被渲染为 JSON 格式并流式传输到客户端。
{
"id": "./src/App.client.js",
"chunks": ["main"],
"name": ""
}
React Server Components 演示
现在我们已经了解了 React Server Components 的概念及其好处,让我们来创建一个分步演示。请注意,这仍然是一项实验性技术,因此此处介绍的 API 将来可能会发生变化。
由于 RSC 仍是一项实验性功能,我们将手动创建项目,而不是使用 create-react-app。我们将使用这个 项目模板,它是从官方演示派生的。
在新的命令行界面中,首先运行以下命令:
git clone https://github.com/techiediaries/rsc-project-template rsc-demo
cd rsc-demo
现在,您的文件夹中将有一个 package.json 文件和一个 webpack.config.js 文件。
您会注意到我们在 package.json 文件中包含了一些实验版本的依赖项。我们包含了主要的依赖项,即 react
、react-dom
和 react-server-dom-webpack
。我们使用的是支持 React Server Components 的实验版本。
在我们的演示中,我们使用 Webpack 来构建应用程序,使用 Babel 将 React 代码转译为纯 JavaScript。我们使用 Express.js 运行服务器,并使用 concurrently 来同时运行多个命令。nodemon
工具通过在检测到目录中的文件更改时自动重启 Node 应用程序来帮助开发基于 Node.js 的应用程序。
作为开发依赖项,我们包含了 cross-env,它使得为目标平台设置和正确使用环境变量的单个命令变得容易。
最后,我们有一些 npm 脚本,可以使用 concurrently、cross-env
和 nodemon
包以及 Webpack 来启动开发服务器并构建生产包。
"scripts": {
"start": "concurrently \"npm run server:dev\" \"npm run bundler:dev\"",
"start:prod": "concurrently \"npm run server:prod\" \"npm run bundler:prod\"",
"server:dev": "cross-env NODE_ENV=development nodemon -- --conditions=react-server server",
"server:prod": "cross-env NODE_ENV=production nodemon -- --conditions=react-server server",
"bundler:dev": "cross-env NODE_ENV=development nodemon -- scripts/build.js",
"bundler:prod": "cross-env NODE_ENV=production nodemon -- scripts/build.js"
},
现在,运行以下命令来安装这些依赖项:
npm install.
接下来,创建一个 public/index.html 文件并添加以下代码:
<!DOCTYPE html>
<html lang="en">
<head>
<title>React Server Components Demo</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
我们添加了一个带有 root ID 的 <div>
来标记我们可以渲染 React 组件树的位置。
接下来,创建一个 src/index.client.js 文件并添加以下代码:
import { unstable_createRoot } from 'react-dom';
import App from './App.client';
const root = unstable_createRoot(document.getElementById('root'));
root.render(<App />);
首先,导入 unstable_createRoot
方法,用于为整个 < App />
树启用并发模式。createRoot
等并发模式 API 仅存在于 React 的实验版本中。
接下来,调用使用 getElementById
方法检索到的 root ID 的 DOM 元素中渲染 App 组件及其子组件的 unstable_createRoot
方法返回的 root 对象的 render
方法。
App 组件从我们稍后创建的 App.client.js 文件中导入。
接下来,创建一个 src/Cache.client.js 文件并添加以下代码:
import {unstable_getCacheForType} from 'react';
import {createFromFetch} from 'react-server-dom-webpack';
function createResponseCache() {
return new Map();
}
export function useServerResponse(props) {
const key = JSON.stringify(props);
const cache = unstable_getCacheForType(createResponseCache);
let response = cache.get(key);
if (response) {
return response;
}
response = createFromFetch(
fetch('/react?props=' + encodeURIComponent(key))
);
cache.set(key, response);
return response;
}
首先,导入 unstable_getCacheForType
和 createFromFetch
方法。然后,使用 JavaScript Map 数据结构创建一个响应缓存。您可以使用它来存储键值对的集合。使用 Fetch API 获取服务器组件,并将结果传递给 createFromFetch
方法以创建方便的响应对象。使用 Map.set
方法将 response
对象传递给缓存。
接下来,创建一个 src/App.server.js 文件并添加以下代码:
import marked from 'marked';
export default function App(props) {
return (
<div>
<h3>
Markdown content rendered on the server
</h3>
<div
dangerouslySetInnerHTML={{
__html: marked(props.mdText)
\}}>
</div>
</div>
)
}
在此处创建一个 React 组件,该组件接受 mdText
属性,使用 marked 库将其 Markdown 内容转换为 HTML,然后将结果设置为 <div>
的 innerHTML。
由于此组件的文件名以 server.js 结尾,因此此组件是服务器上渲染的 React Server Component。
接下来,创建一个 src/App.client.js 文件并添加以下代码:
import {useState, useRef, Suspense} from 'react';
import {useServerResponse} from './Cache.client';
const title = 'React Server Components Demo';
const RenderedContent = (props) => {
const response = useServerResponse(props)
return response.readRoot()
}
export default function App() {
const [content, setContent] = useState('');
const contentRef = useRef();
const handleSubmit = (e) => {
e.preventDefault();
setContent(contentRef.current.value);
};
return (
<Suspense fallback={<div>Loading...</div>}>
<div>
<h2>{title}</h2>
<form onSubmit={ handleSubmit }>
<textarea ref = { contentRef }
name="content"
>
</textarea>
<br />
<input
type="submit" value="Convert.."
/>
</form>
</div>
<RenderedContent mdText={content}></RenderedContent>
</Suspense>
);
}
创建两个组件:RenderedContent
用于接受 Markdown 文本的属性,并调用 useServerResponse
从返回已渲染 Markdown 文本的应用程序服务器组件获取响应。
通过调用 React.useRef
hook 创建一个新的引用,并将其与表单的 textarea
元素关联,我们将 Markdown 文本提交到该元素以作为属性发送到服务器组件。
我们使用了 Suspense 组件 来异步加载组件并指定一个加载 UI,该 UI 在用户等待时显示加载文本。这使我们能够构建更流畅、响应更快的 UI。
最后,创建一个 server/index.server.js 文件并添加以下代码:
'use strict';
const register = require('react-server-dom-webpack/node-register');
register();
const babelRegister = require('@babel/register');
babelRegister({
ignore: [/[\\\/](build|server|node_modules)[\\\/]/],
presets: [['react-app', {runtime: 'automatic'}]],
plugins: ['@babel/transform-modules-commonjs'],
});
const express = require('express');
const compress = require('compression');
const {readFileSync} = require('fs');
const {pipeToNodeWritable} = require('react-server-dom-webpack/writer');
const path = require('path');
const React = require('react');
const ReactApp = require('../src/App.server').default;
const PORT = 4000;
const app = express();
app.use(compress());
app.use(express.json());
app.use(express.static('build'));
app.use(express.static('public'));
app.listen(PORT, () => {
console.log(`RSC Demo listening at https://:${PORT}`);
});
app.get(
'/',
async (req, res) => {
const html = readFileSync(
path.resolve(__dirname, '../build/index.html'),
'utf8'
);
res.send(html);
}
);
app.get('/react', function(req, res) {
const props = JSON.parse(req.query.props);
res.set('X-Props', JSON.stringify(props));
const manifest = readFileSync(
path.resolve(__dirname, '../build/react-client-manifest.json'),
'utf8'
);
const moduleMap = JSON.parse(manifest);
return pipeToNodeWritable(React.createElement(ReactApp, props), res, moduleMap);
});
在这里,我们设置了一个简单的 Express.js 服务器,并公开了一个 /react 端点,我们的客户端代码会调用该端点将渲染的组件放在服务器上。在端点处理程序中,我们从请求对象中读取传递的属性,然后调用 pipeToNodeWritable
方法来渲染服务器组件并将其流式传输到响应对象。此方法接受两个参数:带有属性的 React 组件和由 Webpack 使用 react-server-dom-webpack/plugin 插件生成的模块映射。
现在,在项目文件夹的根目录下运行以下命令:
npm start.
应用程序将在 https://:4000/ 上收听。这是您看到的屏幕截图。
请注意,我们有三种类型的组件文件扩展名:
- .server.js,表示服务器组件
- .client.js,表示 React 客户端组件
- 普通的 .js 扩展名用于共享组件,它们根据导入它们的组件是在服务器上还是在客户端上运行。
本文介绍了 React Server Components,这是一项允许您在服务器上渲染组件的新实验性功能。与标准的服务器端渲染技术相比,此功能提供了额外的优势,例如对最终包大小没有影响、直接访问服务器资源、使用 React IO 库以及对客户端组件进行精细控制。
访问我们 示例项目 的完整代码,或亲自体验 RSC。
历史
- 2021 年 9 月 1 日:初始版本