服务器端渲染(SSR)
PreffX 在服务器端将你的组件渲染为 HTML 字符串,然后在客户端水合同一棵树。服务器会输出标记以及一个包含已解析异步数据的、每个根节点的预加载脚本——客户端读取该脚本,将值注入树中,而不是重新获取。
SSR 工具函数从 preffx/server 导入。
服务器上的 createRoot
服务器导出自己的 createRoot,它与客户端的镜像,但返回的是 renderToString 方法而不是 mount。
ts
import { createRoot } from 'preffx/server';
import { App } from './App';
const server = createRoot();
const { preload, serialize } = server.renderToString(App, {
props: { name: 'PreffX' }
});
await preload(); // resolve every async resource
const html = serialize(); // markup + preload data script通常每个请求都会创建一个新根,因为应用可能依赖于请求 URL(路由、语言)或请求作用域的上下文:
ts
import { createRoot } from 'preffx/server';
export async function render(url: URL) {
const root = createRoot({
defaultURL: url,
defaultLang: 'en',
context: { tenant: 'acme' }
});
const { preload, serialize } = root.renderToString(App, {});
await preload();
return { appHtml: serialize() };
}API
createRoot(config?)
接受与客户端根相同的根选项,因此配置保持同步:
| 选项 | 类型 | 描述 |
|---|---|---|
defaultURL | string | URL | 路由的初始 URL |
defaultLang | string | i18n 的初始语言 |
prefix | string | 每个根的标志前缀(必须与客户端根一致) |
context | object | 与树共享的请求作用域上下文 |
返回一个只包含单一方法的对象:
renderToString(component, config?)
component— 根组件(PC或APC)。config— 可选,包含:props— 根组件的 props。serializer— 用于序列化预加载数据的自定义函数(默认为JSON.stringify)。
返回 { preload, serialize }:
| 方法 | 签名 | 描述 |
|---|---|---|
preload | preload(timeout?) | 解析所有已注册的异步资源。可选超时将慢资源变成「尽力而为」——页面仍会渲染,客户端会重新获取。 |
serialize | serialize(): string | 生成 HTML 字符串(标记 + 每个根的预加载数据脚本)。 |
服务器上的资源
在组件内部,resource 在服务器上会透明地工作:它不会运行副作用(effect),而是为 preload() 注册自己的获取器。服务器和客户端复用同一个组件即可——无需分支。
tsx
import type { PC } from 'preffx';
export const UserCard: PC = (_props, { resource, computed }) => {
const [user] = resource(async () => {
const res = await fetch('/api/user');
return res.json();
});
const display = computed(() => user.state.value?.name ?? 'loading');
return (
<article className="user">
<h1>{display}</h1>
</article>
);
};- 在
preload()运行之前,user.state.value为null,pending为true,因此会渲染回退值('loading')。 - 在
preload()之后,解析出的值会被嵌入标记中,serialize()也会将其写入预加载数据脚本。 - 在客户端水合期间,资源会读取预加载的值并跳过获取,使
pending保持为false。
客户端的水合
客户端的 createRoot().mount() 会检测服务器输出的预加载数据脚本,将其注入树中,并水合现有的 DOM 而不是重新创建——无需额外调用。
tsx
import { createRoot } from 'preffx';
import { App } from './App';
createRoot().mount(App, {
node: document.getElementById('app')!
});成功水合的要求:
- 客户端的
createRoot选项(prefix、defaultLang、defaultURL、context)必须与服务器端一致。 - 渲染出的标记必须准确地插入客户端期望的位置(即
mount引用的容器)。
示例:完整的请求流程
tsx
// entry-server.tsx
import { createRoot } from 'preffx/server';
import { App } from './App';
export async function render(url: URL) {
const server = createRoot({ defaultURL: url });
const { preload, serialize } = server.renderToString(App, {});
await preload();
return { appHtml: serialize() };
}tsx
// entry-client.tsx
import { createRoot } from 'preffx';
import { App } from './App';
createRoot().mount(App, { node: document.getElementById('app')! });Node 服务器将 appHtml 渲染进 HTML 外壳(例如 index.html),并发送给浏览器;然后客户端脚本会对同一棵树进行水合。