服务端渲染
Next.js 与 Nuxt 都开箱支持:服务端直出的 HTML 里已经有组件标签与文本, 不是等 JS 到了才出现的空壳(少数光 DOM 元素除外,见下面的例外)。两条链路目前不完全等价,如实记录:
| React(Next.js) | Vue(Nuxt) | |
|---|---|---|
| 服务端直出组件标签与文案 | 是 | 是 |
| Declarative Shadow DOM(影子树进 HTML) | 是 | 否,刻意关闭 |
| hydration 无报错 | 是 | 是 |
Next.js
组件在服务端渲染成 Declarative Shadow DOM, 样式和结构在 JS 到达之前就已经在页面上 —— 这是「不闪烁」的关键。
// app/page.tsx —— Server Component
import { PtBadge } from '@ptengine/ui-react/ssr';
export default function Page() {
return <PtBadge variant="ai">服务端渲染</PtBadge>;
}需要交互的部分从 @ptengine/ui-react 导入,放在 'use client' 组件里。
next.config.mjs 里要声明 transpile —— 这几个包发的是未编译的 ESM, Next 默认不会把 node_modules 里的 ESM 过一遍编译,不声明就会在 server bundle 里报语法错:
export default {
transpilePackages: ['@ptengine/ui', '@ptengine/ui-react']
};Nuxt
// nuxt.config.ts
export default defineNuxtConfig({
css: ['@ptengine/ui/dist/ptengine-ui/ptengine-ui.css'],
build: {
transpile: ['@ptengine/ui-vue', '@ptengine/ui']
}
});同样的原因:这几个包是未编译的 ESM,不声明的话 Nitro 打服务端包时会原样搬进去,运行时报语法错。
Vue 侧为什么没有 Declarative Shadow DOM
Vue 侧当前刻意不启用 DSD(stencil.config.ts 里的 vueOutputTarget 没有配 hydrateModule)。 试过,不能用:@stencil/vue-output-target 0.14 的 SSR 实现会把插槽内容交给一个嵌套的 createSSRApp 渲成字符串,产出里带着 Vue 的 fragment 锚点注释,而客户端 vdom 里是纯文本节点 —— 每个带插槽的组件都报 hydration mismatch;同一个实现还会把嵌套的包装组件(如 <PtOption>)整个丢掉。
不配这一项时包装层退回普通的 vnode 渲染:服务端照常直出 <pt-button …>文案</pt-button>, 首屏 HTML 里仍有标签与文本(SEO 与首屏文本都在),影子树改为在浏览器 upgrade 时建立, hydrated 标记的 visibility 兜底避免闪烁。上游修好后会打开,e2e 里那条 DSD 断言就是验收标准。
例外
几个元素不参与服务端渲染,或在 React 侧由手写的包装替换了生成的导出。都是为了水合不出错,使用方写法不变:
- 不参与服务端渲染:
pt-kbd、pt-kbd-group、pt-breadcrumb-item、pt-pagination、pt-pagination-item。 它们是光 DOM 元素,Stencil 服务端渲染会把直接文本换成注释占位,与框架的水合冲突,所以排除在外 (@ptengine/ui-react/ssr导出的是它们的客户端版本)。前三个的文字由使用方写在标签里,首屏照常可见; 分页的链接由组件按属性渲染,首屏只有空标签,升级后才出现。 - 推迟写 ARIA:
PtCollapsible、PtMenu、PtPopover、PtTooltip会往使用方的触发元素上补aria-expanded等属性。 React 包装给它们带上data-pt-hydrating标记,等 React 水合完这棵树再写,否则 Next dev 会报属性不一致。 - 不比对子节点:
PtPaginationItem的子节点全由组件自己渲染,React 包装让 React 不比对它的子节点,避免 #418。
只在客户端渲染的降级写法
某个组件在 SSR 下出了问题时,可以先把它降级成仅客户端渲染,不必阻塞整页:
const PtSelect = dynamic(() => import('@ptengine/ui-react').then(m => m.PtSelect), { ssr: false });<ClientOnly>
<PtSelect />
</ClientOnly>代价是那一块会有一次内容跳入。遇到需要这么做的组件请提 issue —— 这属于我们的缺陷。
我们怎么保证它一直能用
每次发版前跑 SSR 冒烟测试:起真实的 Next.js 与 Nuxt 应用,断言
- 服务端返回的 HTML 里确实有组件标签,不是空壳
- 首屏 HTML 里包含
shadowrootmode="open"(目前只对 Next.js 断言,原因见上) - hydration 期间控制台没有任何报错
- 接管后交互立刻可用
另外还在 Next dev / Nuxt dev 下各起一次(pnpm test:e2e:dev,test:e2e 的最后一段):dev 模式会报生产构建不报的 属性级水合告警,整页水合完控制台不能有任何 hydration 告警。