分类
- CSR:Client Side Render
- SSR:Server Side Render
CSR缺点
- 首屏是个空页面,需要加载完vue库,在客户端进行渲染
- 等待的时间较长
- 由于是客户端渲染,爬虫无法直接获取该网页的信息
SSR使用
npm init -y
npm install @vue/server-renderer vue express --save
创建一个服务
index.js
const express = require('express')
const app = express()
const Vue = require('vue')
const renderer3 = require('@vue/server-renderer')
const vue3Compile= require('@vue/compiler-ssr')
const vueapp = {
template: `<div>
<h1 @click="add">点我{{num}}</h1>
<ul >
<li v-for="(todo,n) in todos" >{{n+1}}--{{todo}}</li>
</ul>
</div>`,
data(){
return {
num:1,
todos:['111','222','333']
}
},
methods:{
add(){
this.num++
}
}
}
// @vue/compiler-ssr解析template
vueapp.ssrRender = new Function('require',vue3Compile.compile(vueapp.template).code)(require) // 这里设置了 ssrRender渲染方法
// 路由首页返回结果
app.get('/',async function(req,res){
let vapp = Vue.createSSRApp(vueapp)
let html = await renderer3.renderToString(vapp) // 这里会获取 ssrRender 生成的html,先插入到 <div id="app">之中
const title = "test SSR"
let ret = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>${title}</title>
</head>
<body>
<div id="app">
${html}
</div>
</body>
</html>`
res.send(ret)
})
app.listen(10086,()=>{
console.log('listen 10086')
})
可以看到请求服务的时候直接输出的vue渲染后的html,但是此时还无法触发事件响应,因为vue没有正常加载
可以看到主要是通过 _push 函数拼接内容
_push(`<!--[--><h1>${
_ssrInterpolate($setup.msg)
}</h1><input${
_ssrRenderAttr("value", $setup.msg)
}><!--]-->`)
SSR源码分析
packages/compiler-ssr/src/index.ts
compile
- 使用baseParse转化成ast
- transform 优化ast
- generate生成 render 代码
export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions
export function compile(
source: string | RootNode,
options: CompilerOptions = {},
): CodegenResult {
options = {
...options,
...parserOptions,
ssr: true,
inSSR: true,
scopeId: options.mode === 'function' ? null : options.scopeId,
// always prefix since compiler-ssr doesn't have size concern
prefixIdentifiers: true,
// disable optimizations that are unnecessary for ssr
cacheHandlers: false,
hoistStatic: false,
}
const ast = typeof source === 'string' ? baseParse(source, options) : source // 先转化成ast
// Save raw options for AST. This is needed when performing sub-transforms
// on slot vnode branches.
rawOptionsMap.set(ast, options)
transform(ast, { // transform 优化ast
...options,
hoistStatic: false,
nodeTransforms: [
ssrTransformIf,
ssrTransformFor,
trackVForSlotScopes,
transformExpression,
ssrTransformSlotOutlet,
ssrInjectFallthroughAttrs,
ssrInjectCssVars,
ssrTransformElement,
ssrTransformComponent,
trackSlotScopes,
transformStyle,
...(options.nodeTransforms || []), // user transforms
],
directiveTransforms: {
// reusing core v-bind
bind: transformBind,
on: transformOn,
// model and show have dedicated SSR handling
model: ssrTransformModel,
show: ssrTransformShow,
// the following are ignored during SSR
// on: noopDirectiveTransform,
cloak: noopDirectiveTransform,
once: noopDirectiveTransform,
memo: noopDirectiveTransform,
...(options.directiveTransforms || {}), // user transforms
},
})
// traverse the template AST and convert into SSR codegen AST
// by replacing ast.codegenNode.
ssrCodegenTransform(ast, options)
return generate(ast, options)
}
renderToString
访问路径 packages/server-renderer/src/renderToString.ts
- vnode 调用renderComponentVNode 生成一个buffer流的vnode
- 通过unrollBuffer 把buffer转化成字符串
export async function renderToString(
input: App | VNode,
context: SSRContext = {},
): Promise<string> {
if (isVNode(input)) {
// raw vnode, wrap with app (for context)
return renderToString(createApp({ render: () => input }), context)
}
// rendering an app
const vnode = createVNode(input._component, input._props)
vnode.appContext = input._context
// provide the ssr context to the tree
input.provide(ssrContextKey, context)
const buffer = await renderComponentVNode(vnode) // 这里生成一个buffer流的vnode
const result = await unrollBuffer(buffer as SSRBuffer)// 这里把buffer 转化为 字符串
await resolveTeleports(context)
if (context.__watcherHandles) {
for (const unwatch of context.__watcherHandles) {
unwatch()
}
}
return result
}
renderComponentVNode
packages/server-renderer/src/render.ts
- renderComponentVNode内部通过renderComponentSubTree 调用子树的渲染
- renderComponentSubTree 内部通过 ssrRender 进行
- const { getBuffer, push } = createBuffer() 获取传入的参数
- createBuffer内部 定义了 const buffer: SSRBuffer = [] 闭包,在调用push的时候,自动添加到 buffer 上面
export function renderComponentVNode(
vnode: VNode,
parentComponent: ComponentInternalInstance | null = null,
slotScopeId?: string,
): SSRBuffer | Promise<SSRBuffer> {
const instance = createComponentInstance(vnode, parentComponent, null)
const res = setupComponent(instance, true /* isSSR */)
const hasAsyncSetup = isPromise(res)
const prefetches = instance.sp /* LifecycleHooks.SERVER_PREFETCH */
if (hasAsyncSetup || prefetches) {
let p: Promise<unknown> = hasAsyncSetup
? (res as Promise<void>)
: Promise.resolve()
if (prefetches) {
p = p
.then(() =>
Promise.all(
prefetches.map(prefetch => prefetch.call(instance.proxy)),
),
)
// Note: error display is already done by the wrapped lifecycle hook function.
.catch(NOOP)
}
return p.then(() => renderComponentSubTree(instance, slotScopeId))
} else {
return renderComponentSubTree(instance, slotScopeId)
}
}
function renderComponentSubTree(
instance: ComponentInternalInstance,
slotScopeId?: string,
): SSRBuffer | Promise<SSRBuffer> {
const comp = instance.type as Component
const { getBuffer, push } = createBuffer() // 这里创建传入的参数
if (isFunction(comp)) {
let root = renderComponentRoot(instance)
// #5817 scope ID attrs not falling through if functional component doesn't
// have props
if (!(comp as FunctionalComponent).props) {
for (const key in instance.attrs) {
if (key.startsWith(`data-v-`)) {
;(root.props || (root.props = {}))[key] = ``
}
}
}
renderVNode(push, (instance.subTree = root), instance, slotScopeId)
} else {
if (
(!instance.render || instance.render === NOOP) &&
!instance.ssrRender &&
!comp.ssrRender &&
isString(comp.template)
) {
comp.ssrRender = ssrCompile(comp.template, instance)
}
// perf: enable caching of computed getters during render
// since there cannot be state mutations during render.
for (const e of instance.scope.effects) {
if (e.computed) {
e.computed._dirty = true
e.computed._cacheable = true
}
}
const ssrRender = instance.ssrRender || comp.ssrRender
if (ssrRender) {
// optimized
// resolve fallthrough attrs
let attrs = instance.inheritAttrs !== false ? instance.attrs : undefined
let hasCloned = false
let cur = instance
while (true) {
const scopeId = cur.vnode.scopeId
if (scopeId) {
if (!hasCloned) {
attrs = { ...attrs }
hasCloned = true
}
attrs![scopeId] = ''
}
const parent = cur.parent
if (parent && parent.subTree && parent.subTree === cur.vnode) {
// parent is a non-SSR compiled component and is rendering this
// component as root. inherit its scopeId if present.
cur = parent
} else {
break
}
}
if (slotScopeId) {
if (!hasCloned) attrs = { ...attrs }
const slotScopeIdList = slotScopeId.trim().split(' ')
for (let i = 0; i < slotScopeIdList.length; i++) {
attrs![slotScopeIdList[i]] = ''
}
}
// set current rendering instance for asset resolution
const prev = setCurrentRenderingInstance(instance)
try {
ssrRender( //通过ssrRender方法进行渲染
instance.proxy,
push,
instance,
attrs,
// compiler-optimized bindings
instance.props,
instance.setupState,
instance.data,
instance.ctx,
)
} finally {
setCurrentRenderingInstance(prev)
}
} else if (instance.render && instance.render !== NOOP) {
renderVNode(
push,
(instance.subTree = renderComponentRoot(instance)),
instance,
slotScopeId,
)
} else {
const componentName = comp.name || comp.__file || `<Anonymous>`
warn(`Component ${componentName} is missing template or render function.`)
push(`<!---->`)
}
}
return getBuffer()
}
export function createBuffer() {
let appendable = false
const buffer: SSRBuffer = [] // 定义了闭包
return {
getBuffer(): SSRBuffer {
// Return static buffer and await on items during unroll stage
return buffer
},
push(item: SSRBufferItem) {
const isStringItem = isString(item)
if (appendable && isStringItem) {
buffer[buffer.length - 1] += item as string
} else {
buffer.push(item) // 满足的时候一直push
}
appendable = isStringItem
if (isPromise(item) || (isArray(item) && item.hasAsync)) {
// promise, or child buffer with async, mark as async.
// this allows skipping unnecessary await ticks during unroll stage
buffer.hasAsync = true
}
},
}
}
unrollBuffer
packages/server-renderer/src/renderToString.ts
- 传入buffer 数组,依次遍历数组,循环拼接 得到ret
async function unrollBuffer(buffer: SSRBuffer): Promise<string> {
if (buffer.hasAsync) {
let ret = ''
for (let i = 0; i < buffer.length; i++) { //遍历所有 buffer数组
let item = buffer[i]
if (isPromise(item)) {
item = await item
}
if (isString(item)) {
ret += item // 把字符串 拼接在一起
} else {
ret += await unrollBuffer(item)
}
}
return ret
} else {
// sync buffer can be more efficiently unrolled without unnecessary await
// ticks
return unrollBufferSync(buffer)
}
}
function unrollBufferSync(buffer: SSRBuffer): string {
let ret = ''
for (let i = 0; i < buffer.length; i++) {
let item = buffer[i]
if (isString(item)) {
ret += item
} else {
// since this is a sync buffer, child buffers are never promises
ret += unrollBufferSync(item as SSRBuffer)
}
}
return ret
}
同构代码
刚才的例子无法触发事件,需要要实现的就是同构代码的操作
vue的项目结构,router + store + compoment
根据上图,我们通过两个不同的入口文件打包项目
- server-entry
- client-entry
首次通过server-entry入口文件进行加载资源。在后续点击路由后,使用client-entry结构客户端的渲染
流程
- 打包生成
- ./dist/vue-ssr-server-bundle.json 服务端入口文件
- ../dist/vue-ssr-client-manifest.json 客户端入口文件
- 使用 VueServerRender.createBundleRenderer 创建render ,传入 index.ssr.html , vue-ssr-server-bundle.json vue-ssr-client-manifest.json
- 通过 render.renderToString({ url: ctx.url }) 输出字符串html
const serverBundle = require("./dist/vue-ssr-server-bundle.json");
const serverTemplate = fs.readFileSync(
resolve("./dist/index.ssr.html"),
"utf8"
);
const clientManifest = require("./dist/vue-ssr-client-manifest.json");
const render = VueServerRender.createBundleRenderer(serverBundle, {
template: serverTemplate,
clientManifest, // 注入前端打包好的 js 文件
});
router.get("/(.*)", async (ctx) => {
// 在 src/entry-server.js 导出的函数中可获取传递的 ctx.url
try {
ctx.body = await render.renderToString({ url: ctx.url });
} catch (e) {
if (e.code == 404) {
ctx.body = "page not found";
}
}
});
实现参考Vue2 SSR学习
缺点
- 多了个服务要管理
- 页面变得极其复杂能以维护
- 大并发时候的性能问题
优化方案
- 降级处理,当请求到达一定峰值,回归CSR模式。
- 针对不高频变化的页面,提前渲染静态资源 - (Static Site Generation,SSG) 推荐nuxt
- 当在手机客户端,可以把大数据量渲染交给客户端 - 客户端渲染(Native Side Rendering,NSR)
- 利用CDN把访问的页面内容,固化成CDN资源 - 增量渲染(Incremental Site Rendering,ISR)
- 利用CDN支持的Node服务能力进行渲染 - 边缘渲染(Edge Side Rendering,ESR)
- 在浏览器上直接运行node webcontainer stackblitz.com/edit/stackb…
源码
mjsong07/vue3-ssr-study: vue3-ssr学习 (github.com)
create-vite-extra/template-ssr-vue at master · bluwy/create-vite-extra (github.com)