Skip to content

服务器端渲染(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?)

接受与客户端根相同的根选项,因此配置保持同步:

选项类型描述
defaultURLstring | URL路由的初始 URL
defaultLangstringi18n 的初始语言
prefixstring每个根的标志前缀(必须与客户端根一致)
contextobject与树共享的请求作用域上下文

返回一个只包含单一方法的对象:

renderToString(component, config?)

  • component — 根组件(PCAPC)。
  • config — 可选,包含:
    • props — 根组件的 props。
    • serializer — 用于序列化预加载数据的自定义函数(默认为 JSON.stringify)。

返回 { preload, serialize }

方法签名描述
preloadpreload(timeout?)解析所有已注册的异步资源。可选超时将慢资源变成「尽力而为」——页面仍会渲染,客户端会重新获取。
serializeserialize(): 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.valuenullpendingtrue,因此会渲染回退值('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 选项(prefixdefaultLangdefaultURLcontext)必须与服务器端一致。
  • 渲染出的标记必须准确地插入客户端期望的位置(即 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),并发送给浏览器;然后客户端脚本会对同一棵树进行水合。

根据 Apache-2.0 许可证发布。