vuessr关键流程
流程有了我们再来看下一些过程中的点
vite启动
在 dev 下,我们需要 SSR 提供和非 SSR 模式一致的极速的 HMR 体验。
下面例子是一个启动 vite SSR dev Node.js 端的用例。主要用到了 ssrLoadModule
SSR 运行的 API。
javascript
import express from 'express'
import { createServer } from 'vite'
const app = express()
const vite = createServer({
root,
server: {
middlewareMode: true,
watch: {
// During tests we edit the files too fast and sometimes chokidar
// misses change events, so enforce polling for consistency
usePolling: true,
interval: 100
},
hmr: {
port: hmrPort
}
},
appType: 'custom'
})
app.use(vite.middlewares)
app.use('*', async (req, res) => {
try {
let template = fs.readFileSync(resolve('index.html'), 'utf-8')
template = await vite.transformIndexHtml(url, template)
const render = (await vite.ssrLoadModule('/src/entry-server.js')).render
const [appHtml, preloadLinks] = await render(url, manifest)
const html = template
.replace(`<!--preload-links-->`, preloadLinks)
.replace(`<!--app-html-->`, appHtml)
res
.status(200)
.set({ 'Content-Type': 'text/html' })
.end(html)
} catch (e) {
res.status(500).end(e.stack)
}
})
ssrLoadModule
这个 API 的作用是在 Node.js 环境下加载模块及其依赖,并且在 Vite 同一个 Node.js 环境下执行,并且让这个模块经过 Vite 所有插件 SSR 模式转换,让同一份代码在 SSR 模式下也支持 Vite 提供的语法糖。
下面介绍下这个方法的 happy path。
- ensureEntryFromUrl,解析加载的 URL,并在模块图上创建这个模块。
- ssrTransform,将所有的 esm
import
,export
都转换成vite_ssr_*
函数。 - 执行模块。使用
AsyncFunction
提供vite_ssr_*
相关函数,对原来的 import 语句将会执行ssrLoadModule
继续加载,对 export 语句则在ssrModule
对象上添加返回对象的引用。
❓ 循环依赖问题
- 因为模块对象在加载之前就已经注册到模块图上了,如果这个模块也正在初始化就会直接返回这个模块
ssrModule
引用。 - 如果加载中的模块还是被再次调用
ssrLoadModule
去加载,也是直接从模块图上取这个模块ssrModule
引用,避免模块二次加载。
vite 在加载模块的时候,避免模块进行二次加载,循环依赖获取到的是这个模块没有初始化完成的ssrModule
引用。
❓ 为什么快
SSR dev 处理逻辑和纯 dev 处理逻辑是一致的。SSR 还是根据运行时需要加载的模块进行实时编译然后放到 Node.js 环境下执行,纯 dev 环境根据浏览器加载模块进行请求 vite 然后实时编译返回到浏览器执行。他们都还是保持着 vite 细颗粒度的更新和编译模块,所以在 hmr 的场景下还是很快。
vue ssr虚拟 dom相关
vue-server-renderer 包提供了 createBundleRenderer 的 api,可以传入编译打包后的 bundle 代码来创建一个 renderer。renderer 有 renderToString 和 renderToStream 的 api。
内部会通过 vue.createVNode 来执行 bundle 的代码,产生 Vue 实例,之后把 Vue 实例的 vdom 渲染成 html 字符串。返回这个 html 字符串就实现了 SSR
javascript
async function renderToString(input, context = {}) {
// 输入的内容是一个虚拟节点(vnode),则会将其包装在一个带有上下文的应用程序中,并再次调用renderToString函数进行处理
if (isVNode(input)) {
// raw vnode, wrap with app (for context)
return renderToString(vue.createApp({ render: () => input }), context);
}
// rendering an app
// 输入的内容是一个应用程序(app),则会创建一个虚拟节点,并将输入的组件和属性赋值给该虚拟节点。然后,将上下文信息提供给组件树中的所有组件,以便在渲染过程中使用。
const vnode = vue.createVNode(input._component, input._props);
vnode.appContext = input._context;
// provide the ssr context to the tree
input.provide(vue.ssrContextKey, context);
// 函数会调用renderComponentVNode函数渲染该虚拟节点,并将结果作为一个缓冲区(buffer)返回。这个缓冲区需要经过unrollBuffer函数的处理,最终得到结果
const buffer = await renderComponentVNode(vnode);
const result = await unrollBuffer(buffer);
// 函数会调用resolveTeleports函数解析组件中的传送门(teleports),并返回最终的结果。
await resolveTeleports(context);
return result;
}
SSR的运作过程V3
编译(集中在vite)
json
"build:client": "vite build --ssrManifest --outDir dist/client",
在构建客户端代码的时候需要额外生成ssrManifest
,ssrManifest
的作用是告知 Node.js 端渲染这个模块下所有的依赖。相关代码,
json
"build:server": "vite build --ssr src/entry-server --outDir dist/server",
Vite 需要对 SSR 应用需要使用同一份代码构建在 Node.js 和浏览器运行的代码。以 Vue.js 插件为例,当 Vue 编译器接受到ssr: true
参数后,就会将模版生成的代码从生成浏览器运行代码 转换成 拼接字符串。这个时候主要做的是代码聚合以及esmodule到cjs转换
初始化
@vue/server-renderer
中的 renderToString
方法通过调用 createRenderContext
函数创建渲染上下文,并将渲染上下文和渲染函数(通过 createApp().component.render
生成)传递给 ssrRender
函数进行渲染。
渲染
ssrRenderComponent
、ssrRenderList
、ssrRenderSuspense
等。这些函数用于在服务器端渲染不同类型的组件和指令
ssrGetDirectiveProps
和ssrGetDynamicModelProps
。这些函数用于在服务器端渲染过程中获取指令的属性