React 服务器组件
React 服务器组件是一种新型组件,可以在服务器端或构建时提前渲染。Parcel v2.14.0 及更高版本开箱即用地支持 React 服务器组件。
¥React Server Components are a new type of component that renders ahead of time, on the server or at build time. Parcel v2.14.0 and newer supports React Server Components out of the box.
React 服务器组件支持目前处于测试阶段。如果你遇到错误,请报告 在 GitHub 上。
¥React Server Components support is currently in beta. If you experience bugs, please report them on GitHub.
示例
#¥Examples
rsc-examples 代码库包含使用 React 服务器组件和 Parcel 构建的完整示例应用。
¥The rsc-examples repo includes complete example apps built with React Server Components and Parcel.
安装依赖
#¥Install dependencies
npm install react react-dom @parcel/rsc
注意:服务器组件需要 react
和 react-dom
v19.1.0 或更高版本。
¥Note: Server Components require react
and react-dom
v19.1.0 or later.
客户端渲染
#¥Client rendering
React 服务器组件可以集成到现有的客户端渲染的 SPA 中。例如,你可以渲染服务器组件,而不是从 API 服务器返回 JSON 数据来实现新功能。通过仅发送渲染请求数据所需的组件,并从客户端包中完全省略繁重的非交互式组件(例如 Markdown 渲染器),可以帮助减少客户端包的大小。
¥React Server Components can be integrated into an existing client-rendered SPA. For example, instead of returning JSON from an API server for a new feature, you can render Server Components. This can help reduce client bundle sizes by sending only the components needed to render the requested data, and omitting heavy non-interactive components (e.g. Markdown renderers) from the client bundle entirely.
设置目标
#¥Setup targets
首先,在 package.json
中创建两个 targets。client
目标将指向你应用现有的 index.html
。server
目标将指向你的服务器。
¥First, create two targets in your package.json
. The client
target will point at your app's existing index.html
. The server
target will point at your server.
{
"client": "dist/index.html",
"server": "dist/server.js",
"targets": {
"client": {
"source": "src/index.html",
"context": "react-client"
},
"server": {
"source": "server/server.js",
"context": "react-server"
}
},
"scripts": {
"start": "parcel",
"build": "parcel build"
}
}
Parcel 将同时构建客户端和服务器。
¥Parcel will build both the client and server together.
创建服务器
#¥Create a server
接下来,创建一个服务器。你可以使用任何 Node.js 库或框架来执行此操作。在本例中,我们将使用 Express。
¥Next, create a server. You can use any Node.js libraries or frameworks to do this. In this example we'll use Express.
当请求 /comments
路由时,我们会将服务器组件渲染到 RSC Payload,它是 React 组件树的序列化表示。你可以将其视为 JSON,但它专门用于组件。
¥When the /comments
route is requested, we'll render a Server Component to an RSC Payload, which is a serialized representation of the React component tree. You can think of this like JSON, but specialized for components.
import express from "express";
import cors from "cors";
import { renderRSC } from "@parcel/rsc/node";
import { Comments } from "./Comments";
const app = express();
app.use(cors());
app.get("/comments", (req, res) => {
// Render the server component to an RSC payload.
let stream = renderRSC(<Comments />);
res.set("Content-Type", "text/x-component");
stream.pipe(res);
});
app.listen(3000);
上面使用的 @parcel/rsc
库是对底层 React API 的小型封装器。
¥The @parcel/rsc
library used above is a small wrapper around lower-level React APIs.
服务器入口
#¥Server entries
现在我们需要实现上面渲染的 Comments
组件。这是一个 React 服务器组件。它仅在服务器上运行(而不是在浏览器中),并且具有对服务器资源(例如文件系统或数据库)的完全访问权限。
¥Now we need to implement the Comments
component rendered above. This is a React Server Component. It only runs on the server (not in the browser), and has full access to server resources like the file system or a database.
"use server-entry"
是一个特定于 Parcel 的指令,它将服务器组件标记为页面或路由的入口点,从而创建代码拆分边界。此路由引用的任何依赖都将被最佳地打包在一起,包括客户端组件、CSS 等。页面之间的共享依赖(例如公共库)将自动放置在 共享包 中。
¥"use server-entry"
is a Parcel-specific directive that marks a server component as the entry point of a page or route, creating a code splitting boundary. Any dependencies referenced by this route will be optimally bundled together, including client components, CSS, etc. Shared dependencies between pages, such as common libraries, will be automatically placed in a shared bundle.
"use server-entry";
export async function Comments() {
// Load data from a database...
let comments = await db.getComments();
return comments.map((comment) => (
<article key={comment.id}>
<p>Posted by: {comment.user}</p>
{renderMarkdown(comment.body)}
</article>
));
}
在本例中,我们从数据库加载评论,并将每条评论渲染到 React 组件树中。在服务器上渲染 Markdown 意味着我们不需要在浏览器中加载大型解析库。
¥In this example, we load comments from a database and render each comment to a React component tree. Rendering Markdown on the server means we don't need to load a large parsing library in the browser.
从客户端获取 RSC
#¥Fetch RSC from the client
要在客户端加载服务器组件,请从服务器获取 RSC 负载并将其渲染到 Suspense 边界内。@parcel/rsc
包含一个小的 fetch
封装器,使此操作变得简单。
¥To load Server Components on the client, fetch the RSC Payload from the server and render it in a Suspense boundary. @parcel/rsc
includes a small fetch
wrapper to make this easy.
import {Suspense} from 'react';
import {fetchRSC} from '@parcel/rsc/client';
export function App() {
return (
<>
<h1>Client rendered</h1>
<Suspense fallback={<>Loading comments...</>}>
<Comments />
</Suspense>
</>
);
}
let request = null;
function Comments() {
// Simple cache to make sure we only fetch once.
request ??= fetchRSC('http://localhost:3000/comments');
return request;
}
客户端组件
#¥Client components
服务器组件可以导入客户端组件来增加交互性,并使用 React Hooks(例如 useState
)来更新 UI。客户端组件使用标准的 React "use client"
指令标记。此示例为每个评论添加了一个“赞”按钮。
¥Server Components can import Client Components to add interactivity, using React Hooks such as useState
to update the UI. Client components are marked using the standard React "use client"
directive. This example adds a like button to each comment.
"use client";
import {useState} from "react";
export function LikeButton({likes = 0}) {
let [count, setCount] = useState(likes);
return (
<button onClick={() => setCount(count + 1)}>{count} likes</button>
);
}
"use server-entry";
import {LikeButton} from './LikeButton';
export async function Comments() {
// ...
return comments.map(comment => (
<article key={comment.id}>
<p>Posted by: {comment.user}</p>
{renderMarkdown(comment.body)}
<LikeButton likes={comment.likes} />
</article>
));
}
代码分割
#¥Code splitting
服务器组件允许服务器告知客户端渲染 RSC 负载所需的资源。这包括客户端组件和 CSS 等资源。服务器组件允许资源与数据并行加载,而不是加载所有可能的客户端组件来预先渲染任何类型的数据,或在获取数据后按需加载其他组件。
¥Server Components allow the server to tell the client what resources will be needed to render the RSC Payload. This includes both Client Components and resources like CSS. Instead of loading all possible Client Components to render any kind of data up front, or loading additional components on demand after fetching the data, Server Components enable resources to load in parallel with the data.
代码拆分在服务器组件中的工作方式与在客户端组件中的工作方式相同。使用 React.lazy 和动态 import()
按需加载组件。由于这发生在服务器上,客户端将开始与数据并行加载必要的资源。
¥Code splitting works the same way in Server Components as in Client Components. Use React.lazy with dynamic import()
to load components on demand. Since this happens on the server, the client will start loading the necessary resources in parallel with the data.
此示例根据评论内容是文本、图片还是视频来渲染不同的组件。仅加载渲染响应中评论类型所需的资源。
¥This example renders different components depending whether it is a text, image, or video comment. Only the resources needed to render the comment types in the response will be loaded.
import { lazy } from "react";
const TextComment = lazy(() => import("./TextComment"));
const ImageComment = lazy(() => import("./ImageComment"));
const VideoComment = lazy(() => import("./VideoComment"));
function Comment({ comment }) {
switch (comment.type) {
case "text":
return <TextComment comment={comment} />;
case "image":
return <ImageComment comment={comment} />;
case "video":
return <VideoComment comment={comment} />;
}
}
服务器函数
#¥Server functions
React 服务器函数 允许客户端组件调用服务器上的函数,例如更新数据库或调用后端服务。
¥React Server Functions allow Client Components to call functions on the server, for example, updating a database or calling a backend service.
服务器函数标有标准的 React "use server"
指令。目前,Parcel 支持在文件顶部使用 "use server"
,而不是在函数内内联使用。
¥Server functions are marked with the standard React "use server"
directive. Currently, Parcel supports "use server"
at the top of a file, and not inline within a function.
服务器函数可以从客户端组件导入,并像普通函数一样调用,或者传递给 <form>
元素的 action
属性。
¥Server functions can be imported from Client Components and called like normal functions, or passed to the action
prop of a <form>
element.
在本例中,我们将更新 LikeButton
组件,将点赞计数存储在数据库中。
¥In this example, we'll update the LikeButton
component to store the like count in a database.
"use server";
export async function likeComment(id) {
let newLikeCount = await db.incrementLikeCount(id);
return newLikeCount;
}
"use client";
import {useState, startTransition} from "react";
import {likeComment} from './actions';
export function LikeButton({id, likes = 0}) {
let [count, setCount] = useState(likes);
let onClick = () => {
startTransition(async () => {
let newLikeCount = await likeComment(id);
setCount(newLikeCount);
});
};
return (
<button onClick={onClick}>{count} likes</button>
);
}
最后一步是在调用操作时通过发出 HTTP 请求来 "connecting" 客户端和服务器。@parcel/rsc/client
中的 setServerCallback
函数定义了一个函数,当客户端调用服务器函数时会调用该函数。每个服务器函数都有一个由 Parcel 生成的 ID 以及调用它的参数。这些引用应作为 HTTP 请求的一部分发送到服务器。
¥The last step is "connecting" the client and server by making an HTTP request when an action is called. The setServerCallback
function in @parcel/rsc/client
defines a function to be called when a Server Function is called from the client. Each Server Function has an id generated by Parcel, and arguments that it was called with. These should be sent as part of an HTTP request to the server.
此设置只需完成一次,之后所有其他服务器功能都将以相同的方式处理。
¥This setup needs to be done once, and then all additional Server Functions will be handled the same way.
import { setServerCallback, fetchRSC } from "@parcel/rsc/client";
// ...
// Setup a callback to perform server actions.
// This sends a POST request to the server and updates the page.
setServerCallback(async (id, args) => {
let result = await fetchRSC("/action", {
method: "POST",
headers: {
"rsc-action-id": id,
},
body: args,
});
return result;
});
在服务器上,我们需要处理 POST 请求并调用原始服务器函数。这将读取作为 HTTP 标头传递的服务器操作的 ID,并调用关联的操作。它将以函数的结果作为响应,并序列化为 RSC 有效负载。
¥On the server, we'll need to handle POST requests and call the original server function. This will read the id of the server action passed as an HTTP header, and call the associated action. It will respond with the function's result, serialized as an RSC payload.
import { renderRSC, callAction } from "@parcel/rsc/node";
// ...
app.post("/action", async (req, res) => {
let id = req.get("rsc-action-id");
let { result } = await callAction(req, id);
let stream = renderRSC(result);
res.set("Content-Type", "text/x-component");
stream.pipe(res);
});
现在,当用户点击点赞按钮时,服务器将被调用来更新数据库,客户端将使用更新后的点赞数量重新渲染。
¥Now, when a user clicks the like button, the server will be called to update the database, and the client will re-render with the updated like count.
此设置还可以自定义,以更改你调用服务器的方式,例如添加身份验证标头,甚至使用不同的传输机制。你可以通过从使用 "use server"
的文件中导出异步函数来添加其他服务器操作,它们都将经历相同的服务器回调。
¥This setup can also be customized to change how you call the server, for example, adding authentication headers, or even using a different transport mechanism. You can add additional server actions by exporting async functions from a file with "use server"
, and they will all go through the same server callback.
服务器渲染
#¥Server rendering
在客户端渲染的 React 应用中,Parcel 构建的入口点通常是一个 HTML 文件。构建的输出可能会上传到静态文件服务器或 CDN。HTML 和 JavaScript 加载后,你可以从 API 服务器请求数据,并使用客户端上的组件进行渲染。在渲染数据的过程中,你可能会动态加载其他组件或数据。这是一个称为网络瀑布的性能问题。
¥In a client-rendered React app, the entry point for your Parcel build is typically an HTML file. The output of the build might be uploaded to a static file server or CDN. After the HTML and JavaScript loads, you might request data from an API server and render it with components on the client. In the process of rendering the data, you might dynamically load additional components or data. This is a performance problem called a network waterfall.
React 服务器组件可以通过将渲染为 HTML 作为初始请求的一部分来优化网络瀑布流。这避免了加载数据的额外 API 请求,并允许渲染数据所需的组件并行加载而不是串行加载。
¥React Server Components can optimize network waterfalls by rendering to HTML as part of the initial request. This avoids additional API requests to load data, and allows components needed to render the data to be loaded in parallel instead of in series.
使用服务器渲染时,Parcel 构建的入口点是服务器的源代码,而不是静态 HTML 文件。
¥When using server rendering, the entry point for your Parcel build is the source code for your server instead of a static HTML file.
快速入门
#¥Quick start
要使用 React 服务器组件和 Parcel 搭建一个新的服务器渲染应用,请运行以下命令:
¥To scaffold a new server-rendered app with React Server Components and Parcel, run the following commands:
npm create parcel react-server my-rsc-app
cd my-rsc-app
npm start
将 npm
替换为 yarn
或 pnpm
以使用你首选的包管理器。深入了解下文。
¥Replace npm
with yarn
or pnpm
to use your preferred package manager. See below for a deep dive.
创建服务器
#¥Create a server
你可以使用任何 Node.js 库或框架来创建你的服务器。在本例中,我们将使用 Express。这与 以上示例 类似,但使用 @parcel/rsc
中的 renderRequest
而不是 renderRSC
。这将渲染 HTML 而不是 RSC 负载。服务器入口 渲染页面的根 <html>
元素。
¥You can use any Node.js libraries or frameworks to create your server. In this example we'll use Express. This is similar to the example above but uses renderRequest
from @parcel/rsc
instead of renderRSC
. This renders HTML instead of an RSC Payload. The server entry renders the root <html>
element for the page.
{
"server": "dist/server.js",
"targets": {
"server": {
"source": "src/server.js",
"context": "react-server"
}
},
"scripts": {
"start": "parcel",
"build": "parcel build"
}
}
import express from "express";
import { renderRequest } from "@parcel/rsc/node";
import { Page } from "./Page";
// Create an Express app and serve the dist folder.
const app = express();
app.use("/client", express.static("dist/client"));
// Create a route for the home page.
app.get("/", async (req, res) => {
await renderRequest(req, res, <Page />, { component: Page });
});
app.listen(3000);
"use server-entry";
export function Page() {
return (
<html>
<head>
<title>Parcel React Server App</title>
</head>
<body>
<h1>Hello world!</h1>
</body>
</html>
);
}
使用 npm start
启动开发服务器,然后打开 http://localhost:3000 查看渲染后的页面。
¥Start the development server with npm start
, and open http://localhost:3000 to see the rendered page.
客户端入口
#¥Client entry
React 服务器组件将客户端和服务器代码无缝集成到一个统一的组件树中。但到目前为止,我们的应用只渲染静态 HTML。要添加交互性,我们首先需要在浏览器中对页面进行“hydrate”操作。
¥React Server Components seamlessly integrate client and server code in one unified component tree. But so far, our app only renders static HTML. To add interactivity, we first need to hydrate the page in the browser.
要使页面保持动态,请创建一个新的 src/client.js
文件,并使用 Parcel 特定的 "use client-entry"
指令将其标记为客户端入口。这会告诉 Parcel 它应该只在浏览器中运行,而不是在服务器上运行,并且应该在页面加载时立即运行。@parcel/rsc/client
库可用于使用服务器上 @parcel/rsc/node
注入到 HTML 中的数据来补充页面。
¥To hydrate the page, create a new src/client.js
file, and mark it as a client entry with the Parcel-specific "use client-entry"
directive. This tells Parcel that it should run only in the browser, and not on the server, and that it should run immediately on page load. The @parcel/rsc/client
library can be used to hydrate the page, using data injected into the HTML by @parcel/rsc/node
on the server.
"use client-entry";
import { hydrate } from "@parcel/rsc/client";
hydrate();
最后,从页面组件导入 client.js
以及所有客户端组件:
¥Finally, import client.js
from the Page component, along with any Client Components:
"use server-entry";
import './client';
import {Counter} from './Counter';
export function Page() {
return (
<html>
<body>
{/* ... */}
<Counter />
</body>
</html>
);
}
"use client";
import {useState} from "react";
export function Counter() {
let [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
);
}
页面现在加载包含 Counter
组件的 <script>
,该组件会在点击时更新客户端状态。
¥The page now loads a <script>
including the Counter
component, which updates client state on click.
路由
#¥Routing
到目前为止,我们只有一个页面。要添加其他操作,请在服务器代码中创建一个新的路由,以及一个要渲染的新组件。
¥So far, we only have one page. To add another, create a new route in the server code, along with a new component to render.
import {About} from './About';
// ...
app.get('/about', async (req, res) => {
await renderRequest(req, res, <About />, {component: About});
});
"use server-entry";
import "./client";
export function About() {
return (
<html>
<head>
<title>About</title>
</head>
<body>
<h1>About</h1>
<a href="/">Home</a>
</body>
</html>
);
}
现在你应该能够加载 http://localhost:3000/about 了。
¥Now you should be able to load http://localhost:3000/about.
但是,你可能会注意到,点击 "首页" 链接时,浏览器会刷新整个页面。要提高导航的响应速度,你可以从服务器获取新的 RSC 负载,并就地更新组件树。
¥However, you may notice that when clicking the "Home" link, the browser does a full page refresh. To improve the responsiveness of navigation, you can fetch a new RSC payload from the server and update the component tree in place instead.
@parcel/rsc/client
包含一个 fetchRSC
函数,它是对 fetch
API 的一个小封装器,返回一个新的 React 树。将此传递给 hydrate
返回的 updateRoot
函数将使用新内容更新页面。
¥@parcel/rsc/client
includes a fetchRSC
function, which is a small wrapper around the fetch
API that returns a new React tree. Passing this to the updateRoot
function returned by hydrate
will update the page with the new content.
举个简单的例子,我们可以拦截链接上的 click
事件来触发客户端导航。页面加载完成后,可以使用浏览器 history.pushState
API 更新浏览器的 URL 栏。
¥As a simple example, we can intercept the click
event on links to trigger client side navigation. The browser history.pushState
API can be used to update the browser's URL bar once the page is finished loading.
"use client-entry";
import { hydrate, fetchRSC } from "@parcel/rsc/client";
let updateRoot = hydrate();
async function navigate(pathname, push = false) {
let root = await fetchRSC(pathname);
updateRoot(root, () => {
if (push) {
history.pushState(null, "", pathname);
}
});
}
// Intercept link clicks to perform RSC navigation.
document.addEventListener("click", (e) => {
let link = e.target.closest("a");
if (link) {
e.preventDefault();
navigate(link.pathname, true);
}
});
// When the user clicks the back button, navigate with RSC.
window.addEventListener("popstate", (e) => {
navigate(location.pathname);
});
此示例在客户端导航期间重新渲染整个页面。在实际应用中,仅加载页面中发生变化的部分可能会有所帮助(例如,排除侧边栏等常见部分)。支持嵌套路由的路由库使这更容易。
¥This example re-renders the entire page during client navigation. In a real app, it might be beneficial to load only the part of the page that changed (e.g. excluding common parts such as a sidebar). Router libraries with support for nested routes make this easier.
服务器函数
#¥Server functions
在服务器渲染的应用中,React 服务器函数的工作方式与 以上示例 类似。@parcel/rsc/client
中的 hydrate
函数接受 callServer
函数作为选项,该函数负责向服务器发出请求。
¥In a server-rendered app, React Server Functions work similarly to the example above. The hydrate
function in @parcel/rsc/client
accepts a callServer
function as an option, which is responsible for making a request to the server.
服务器可能还需要在调用服务器函数时重新渲染页面。在本例中,它会返回新页面以及函数的返回值。客户端会相应地更新页面。
¥The server may also want to re-render the page when a Server Function is called. In this example, it returns the new page alongside the function's return value. The client updates the page accordingly.
"use client-entry";
import { hydrate, fetchRSC } from "@parcel/rsc/client";
let updateRoot = hydrate({
// Setup a callback to perform server actions.
// This sends a POST request to the server and updates the page.
async callServer(id, args) {
let { result, root } = await fetchRSC("/", {
method: "POST",
headers: {
"rsc-action-id": id,
},
body: args,
});
updateRoot(root);
return result;
},
});
// ...
import { renderRequest, callAction } from "@parcel/rsc/node";
// ...
app.post("/", async (req, res) => {
let id = req.get("rsc-action-id");
let { result } = await callAction(req, id);
let root = <Page />;
if (id) {
root = { result, root };
}
await renderRequest(req, res, root, { component: Page });
});
静态渲染
#¥Static rendering
Parcel 支持在构建时将 React 服务器组件预渲染为完全静态的 HTML。例如,营销页面或博客文章通常是静态的,不包含针对用户个性化的动态数据。预渲染允许这些页面直接从 CDN 提供服务,而无需服务器。
¥Parcel supports pre-rendering React Server Components to fully static HTML at build time. For example, a marketing page or blog post is often static, and does not contain dynamic data personalized for the user. Pre-rendering allows these pages to be served directly from a CDN rather than requiring a server.
快速入门
#¥Quick start
要设置一个完全静态渲染的新项目,请运行以下命令:
¥To set up a new project with fully static rendering, run the following commands:
npm create parcel react-static my-static-site
cd my-static-site
npm start
将 npm
替换为 yarn
或 pnpm
以使用你首选的包管理器。深入了解下文。
¥Replace npm
with yarn
or pnpm
to use your preferred package manager. See below for a deep dive.
设置
#¥Setup
使用 "react-static"
目标名称将条目预渲染为静态 HTML。
¥Use the "react-static"
target name to pre-render entries to static HTML.
{
"targets": {
"react-static": {
"source": "pages/**/*.{js,tsx,mdx}",
"context": "react-server"
}
}
}
使用此配置,pages
目录中的组件将被渲染为 dist
目录中的 HTML 文件。静态渲染的组件接收页面列表作为属性,允许你渲染导航列表。
¥With this configuration, components in the pages
directory will be rendered to HTML files in the dist
directory. Statically rendered components receive a list of pages as a prop, which allows you to render a navigation list.
import type { PageProps } from "@parcel/rsc";
import "../src/client";
export default function Index({ pages, currentPage }: PageProps) {
return (
<html>
<body>
<nav>
<ul>
{pages.map((page) => (
<li key={page.url}>
<a
href={page.url}
aria-current={
page.url === currentPage.url ? "page" : undefined
}
>
{page.name.replace(".html", "")}
</a>
</li>
))}
</ul>
</nav>
</body>
</html>
);
}
对于每个页面,Parcel 输出两个文件:
¥For each page, Parcel outputs two files:
-
一个
.html
文件,用于从头加载页面。¥A
.html
file, which is used when loading the page from scratch. -
一个
.rsc
文件,可用于执行客户端导航。这会加快后续导航速度,类似于单页应用。¥A
.rsc
file, which can be used to perform client side navigation. This speeds up subsequent navigation similar to a single page app.
要启用客户端导航,请实现类似于 以上示例 的 client.js
文件。在这种情况下,请在获取数据时将 .html
替换为 .rsc
。
¥To enable client side navigations, implement a client.js
file similar to the example above. In this case, replace .html
with .rsc
when fetching.
"use client-entry";
import {hydrate, fetchRSC} from '@parcel/rsc/client';
let updateRoot = hydrate();
async function navigate(pathname, push = false) {
let root = await fetchRSC(pathname.replace('.html', '.rsc'));
updateRoot(root, () => {
if (push) {
history.pushState(null, '', pathname);
}
});
}
// Intercept link clicks to perform RSC navigation.
document.addEventListener('click', e => {
let link = e.target.closest('a');
if (link) {
e.preventDefault();
navigate(link.pathname, true);
}
});
// When the user clicks the back button, navigate with RSC.
window.addEventListener('popstate', e => {
navigate(location.pathname);
});
MDX
#MDX 是 Markdown 的一个变体,可编译为 JSX。Parcel 开箱即用地支持 MDX。
¥MDX is a variant of Markdown that compiles to JSX. Parcel supports MDX out of the box.
通过 pages
和 currentPage
属性中每个页面的 exports
属性,可以从 MDX 布局中进行静态导出。例如,你可以导出 title
属性,以便在 <title>
元素中使用,或者在渲染所有页面的导航列表时使用。
¥Static exports are available from MDX layouts via the exports
property of each page in the pages
and currentPage
props. For example, you could export a title
property for use in the <title>
element, or when rendering a navigation list of all pages.
此外,还会生成一个 tableOfContents
属性。这是 MDX 文件中所有标题的树状结构,你可以使用它在 MDX 布局中渲染目录。
¥In addition, a tableOfContents
property is also generated. This is a tree of all of the headings in the MDX file, which you can use to render a table of contents in an MDX layout.
import Layout from '../src/MDXLayout';
export default Layout;
export const title = 'Static MDX';
# Hello, MDX!
This is a static MDX file.
import type { ReactNode } from "react";
import type { PageProps, TocNode } from "@parcel/rsc";
import "./client";
interface LayoutProps extends PageProps {
children: ReactNode;
}
export default function Layout({ children, pages, currentPage }: LayoutProps) {
return (
<html lang="en">
<head>
<title>{currentPage.exports!.title}</title>
</head>
<body>
<main>{children}</main>
<aside>
<Toc toc={currentPage.tableOfContents!} />
</aside>
</body>
</html>
);
}
function Toc({ toc }: { toc: TocNode[] }) {
return toc.length > 0 ? (
<ul>
{toc.map((page, i) => (
<li key={i}>
{page.title}
<Toc toc={t.children} />
</li>
))}
</ul>
) : null;
}
有关更多详细信息,请参阅 Parcel 的 MDX 文档。
¥See Parcel's MDX documentation for more details.
混合静态和动态
#¥Mixing static and dynamic
你可以在同一个应用中混合使用静态渲染的页面和服务器渲染的动态页面。这可以通过创建多个目标来实现。
¥You can mix statically rendered pages with server rendered dynamic pages within the same app. This can be done by creating multiple targets.
{
"server": "dist/server.js",
"targets": {
"server": {
"source": "src/server.js",
"context": "react-server"
},
"react-static": {
"source": "pages/**/*.js",
"distDir": "dist/static",
"context": "react-server"
}
}
}
使用此配置,Parcel 将静态渲染 pages
目录中的组件,并将 HTML 文件输出到 dist/static
。
¥With this configuration, Parcel will statically render components in the pages
directory and output HTML files into dist/static
.
接下来,更新你的服务器以响应静态渲染页面的请求。此示例在请求 text/html
时响应 .html
文件,在请求 text/x-component
时(客户端导航期间)响应 .rsc
文件。
¥Next, update your server to respond to requests for statically rendered pages. This example responds with a .html
file when text/html
is requested, and a .rsc
file when text/x-component
is requested (during client navigations).
import express from "express";
const app = express();
app.use("/client", express.static("dist/client"));
// Respond to requests for statically rendered pages.
app.get("/*", (req, res, next) => {
res.format({
"text/html": () => sendFile(req.url + ".html", res, next),
"text/x-component": () => sendFile(req.url + ".rsc", res, next),
default: next,
});
});
function sendFile(path, res, next) {
res.sendFile(path, { root: "dist/static" }, (err) => {
if (err) next();
});
}
app.listen(3000);
export default function StaticPage() {
return (
<html>
<body>
<p>This page is statically rendered at build time!</p>
</body>
</html>
);
}
现在 http://localhost:3000/static 将显示一个静态渲染的页面。
¥Now http://localhost:3000/static will display a statically rendered page.
部署时,你还可以将 dist/client
和 dist/static
目录上传到 CDN,并将 dist/server
目录部署到服务器。
¥When deploying, you could also upload the dist/client
and dist/static
directories to a CDN, and deploy the dist/server
directory to a server.