Skip to main content

shell 架构

VitePress 内部使用的是 Shell 架构,以 Vue 官网为例: 图片 VitePress 会在 hydrate 的过程中把正文的静态部分排除,具体实现原理如下:

Vue 模板编译阶段,Vue 会对静态虚拟 DOM 节点进行优化,输出 createStaticVNode 的格式

图片 在 Chunk 生成阶段(实现链接),把内容部分用 __VP_STATIC_START____VP_STATIC_END__ 标志位包裹 在生成打包产物前,针对每个页面打包出两份 JS 一份是包含完整内容的 JS,把标志位去掉即可,比如文件名为 recommend.[hash].js 另一份是不包含内容的 JS,把标志位及其里面的内容删掉,文件名为 recommend.[hash].lean.js 由于 VitePress 采用的是 SSG + SPA 模式,其会根据是否为首屏来分发不同的 JS: 首屏使用 .lean.js,不包含正文部分的 JS,实现 Partial Client Bundle + Partial Hydration,跟 Islands 架构一样的效果 二次页面跳转使用完整的 JS,因为走 SPA 路由跳转,需要拿到完整的页面内容,用 JS 渲染出来。 你可能会问了,在 .lean.js 里面,组件的代码都被改了,难道 Vue 在 hydrate 不会发现内容和服务端渲染的 HTML 对应不上进而报错吗?答案是不会,我们可以看看 Vue 里面 createStaticVNode 的实现: 图片 注意第二个传参,里面会记录静态节点的数量,在 hydrate 的过程中对静态节点会特殊处理,直接检查 staticCount 即节点数量而不是内容,那么对于如下的 VNode 节点来讲 hydrate 仍然是可以成功的:

// recommend.[hash].lean.js
const html = ` A <span>foo</span> B`;
const { vnode, container } = mountWithHydration(html, () =>
// 保证第二个参数正确即可
createStaticVNode(``, 3)
);

总之, VitePress 利用 Vue 的编译时优化以及内部定制的 Hydrate 方案足以解决传统 SSG 的全量 hydration 问题,采用 Islands 架构意义并不大。

那进一步讲,像 Vue 这种 Shell 优化方案对于包含编译时的前端框架是否通用?这里我们可以先大概总结出 Shell 方案需要满足的条件:

模板编译阶段,将静态节点进行特殊标记

运行时,支持 hydrate 跳过对静态节点的内容检查

基于上面这两点,其他的代表性编译时框架如 Solid、Svelte 很难实现 Vue 的 Shell 架构(没法标记静态节点),因此 Shell 方案可以理解为在 Vue 框架下的一个特殊优化了。对于 Vue 外的其它框架方案,仍然可以采用 Islands 进行特定场景的优化。