服务端渲染:
简单说:比如说一个模板,数据是从后台获取的,如果用客户端渲染那么浏览器会先渲染html
和css
,然后再通过js
的ajax
去向后台请求数据再更改渲染。就是在前端再用Node
建个后台,把首屏数据加载成一个完整的页面在node
建的后台渲染好,浏览器拿到的就是一个完整的dom
树。根据项目打开地址,路由指到哪个页面就跳到哪。
服务端比起客户端渲染页面的优点:
- 首屏渲染速度更快
客户端渲染的一个缺点是,用户第一次访问页面,此时浏览器没有缓存,需要先从服务端下载js
,然后再通过js
操作动态添加dom
并渲染页面,时间较长。而服务端渲染的规则是,用户第一次访问浏览器可以直接解析html
文档并渲染页面,并屏渲染速度比客户端渲染更快。
SEO
服务端渲染可以让搜索引擎更容易读取页面的meta
信息,以及其它SEO
相关信息,大大增加了网站在搜索引擎中的速度。
- 减少
HTTP
请求
服务端渲染可以把一些动态数据在首次渲染时同步输出到页面,而客户端渲染需要通过AJAX
等手段异步获取这些数据,这样就相当于多了一次HTTP
请求。
普通服务端渲染
vue
提供了renderToString
接口,可以在服务端把vue
组件渲染成模板字符串,我们先看下用法:
benchmarks/ssr/renderToString.js
const Vue = require('../../dist/vue.runtime.common.js')
const createRenderer = require('../../packages/vue-server-renderer').createRenderer
const renderToString = createRenderer().renderToString
const gridComponent = require('./common.js') // vue支行时的代码,不包括编译部分
console.log('--- renderToString --- ')
const self = (global || root)
self.s = self.performance.now()
renderToString(new Vue(gridComponent), (err, res) => {
if (err) throw err
// console.log(res)
console.log('Complete time: ' + (self.performance.now() - self.s).toFixed(2) + 'ms')
console.log()
})
这段代码是支行在node.js
环境中的,主要依赖vue.common.js
,vue-server-render
.其中vue.common.js
是vue
运行时代码,不包括编译部分:vue-server-render
对外提供createRenderer
方法,renderToString
是createRenderer
方法返回值的一个属性,它支持传入vue
实例和渲染完成后的回调函数,这里要注意,由于引用的是只包括运行时的vue
代码,不包括编译部分,所以其中err
表示是否出错,result
表示dom
字符串。在实际应用中,我们可以将回调函数拿到的result
拼接到模板中,下面看下renderToString
的实现:
src/server/create-renderer.js
const render = createRenderFunction(modules, directives, isUnaryTag, cache)
return {
renderToString (
component: Component,
context: any,
cb: any
): ?Promise<string> {
if (typeof context === 'function') {
cb = context
context = {}
}
if (context) {
templateRenderer.bindRenderFns(context)
}
// no callback, return Promise
let promise
if (!cb) {
({ promise, cb } = createPromiseCallback())
}
let result = ''
const write = createWriteFunction(text => {
result += text
return false
}, cb)
try {
// render:把component转换模板字符串str ,write方法不断拼接模板字符串,用result做存储,然后调用next,当component通过render完毕,执行done传入resut,
render(component, write, context, () => {
if (template) {
result = templateRenderer.renderSync(result, context)
}
cb(null, result)
})
} catch (e) {
cb(e)
}
return promise
}
}
renderToString
方法支持传入vue
实例component
和渲染完成后的回调函数done
。它定义了result
变量,同时定义了write
方法,最后执行render
方法。整个过程比较核心的就是render
方法:
src/server/render.js
return function render (
component: Component,
write: (text: string, next: Function) => void,
userContext: ?Object,
done: Function
) {
warned = Object.create(null)
const context = new RenderContext({
activeInstance: component,
userContext,
write, done, renderNode,
isUnaryTag, modules, directives,
cache
})
installSSRHelpers(component)
normalizeRender(component)
renderNode(component._render(), true, context)
}
/**
* // render实际上是执行了renderNode方法,并把component._render()方法生成的vnode对象作为参数传入。
* @param node 先判断node类型,如果是component Vnode,则根据这个Node创建一个组件的实例并调用_render方法作为当前node的childVnode,然后递归调用renderNode
* @param isRoot 如果是一个普通dom Vnode对象,则调用renderElement渲染元素,否则就是一个文本节点,直接用write方法。
* @param context
*/
function renderNode (node, isRoot, context) {
if (node.isString) {
renderStringNode(node, context)
} else if (isDef(node.componentOptions)) {
renderComponent(node, isRoot, context)
} else if (isDef(node.tag)) {
renderElement(node, isRoot, context)
} else if (isTrue(node.isComment)) {
if (isDef(node.asyncFactory)) {
// async component
renderAsyncComponent(node, isRoot, context)
} else {
context.write(`<!--${node.text}-->`, context.next)
}
} else {
context.write(
node.raw ? node.text : escape(String(node.text)),
context.next
)
}
}
/**主要功能是把VNode对象渲染成dom元素。
* 先判断是不是根元素,然后渲染开始开始标签,如果是自闭合标签<img/>直接写入write,再执行next方法
* 如果没有子元素,又不是闭合标签,通过write写入开始-闭合标签。再执行next.dom渲染完毕
* 否则就通过write写入开始标签,接着渲染所有的子节点,再通过write写入闭合标签,最后执行next
* @param context
*/
function renderElement (el, isRoot, context) {
const { write, next } = context
if (isTrue(isRoot)) {
if (!el.data) el.data = {}
if (!el.data.attrs) el.data.attrs = {}
el.data.attrs[SSR_ATTR] = 'true'
}
if (el.functionalOptions) {
registerComponentForCache(el.functionalOptions, write)
}
const startTag = renderStartingTag(el, context)
const endTag = `</${el.tag}>`
if (context.isUnaryTag(el.tag)) {
write(startTag, next)
} else if (isUndef(el.children) || el.children.length === 0) {
write(startTag + endTag, next)
} else {
const children: Array<VNode> = el.children
context.renderStates.push({
type: 'Element',
rendered: 0,
total: children.length,
endTag, children
})
write(startTag, next)
}
}
流式服务端渲染
普通服务器有一个痛点——由于渲染是同步过程,所以如果这个app
很复杂的话,可能会阻塞服务器的event loop
,同步服务器在优化不当时甚至会给客户端获得内容的速度带来负面影响。vue
提供了renderToStream
接口,在渲染组件时返回一个可读的stream
,可以直接pipe
到HTTP Response
中,流式渲染能确保在服务端响应度,也能让用户更快地获得渲染内容。renderToStream
源码:
benchmarks/ssr/renderToStream.js
const Vue = require('../../dist/vue.runtime.common.js')
const createRenderer = require('../../packages/vue-server-renderer').createRenderer
const renderToStream = createRenderer().renderToStream
const gridComponent = require('./common.js')
console.log('--- renderToStream --- ')
const self = (global || root)
const s = self.performance.now()
const stream = renderToStream(new Vue(gridComponent))
let str = ''
let first
let complete
stream.once('data', () => {
first = self.performance.now() - s
})
stream.on('data', chunk => {
str += chunk
})
stream.on('end', () => {
complete = self.performance.now() - s
console.log(`first chunk: ${first.toFixed(2)}ms`)
console.log(`complete: ${complete.toFixed(2)}ms`)
console.log()
})
这段代码也是同样运行在node
环境中的,与rendetToString
不同,它会把vue
实例渲染成一个可读的stream
。源码演示的是监听数据的读取,并记录读取数据的时间
,而在实际应用中,我们也可以这样写:
const Vue = require('../../dist/vue.runtime.common.js')
const createRenderer = require('../../packages/vue-server-renderer').createRenderer
const renderToStream = createRenderer().renderToStream
const gridComponent = require('./common.js')
const stream = renderToStream(new Vue(gridComponent))
app.use((req,res)=>{
stream.pipe(res)
})
如果代码运行在Express
框架中,则可以通过app.use
方法创建middleware
,然后直接把stream pipe
到res
中,这样客户端就能很快地获得渲染内容了,下面看下renderToStream
的实现:
src/server/create-renderer.js
const render = createRenderFunction(modules, directives, isUnaryTag, cache)
return {
...
renderToStream (component: Component,context?: Object): stream$Readable {
if (context) {
templateRenderer.bindRenderFns(context)
}
const renderStream = new RenderStream((write, done) => {
render(component, write, context, done)
})
if (!template) {
return renderStream
} else {
const templateStream = templateRenderer.createStream(context)
renderStream.on('error', err => {
templateStream.emit('error', err)
})
renderStream.pipe(templateStream)
return templateStream
}
}
renderToStream
传入一个Vue
对象实例,返回的是一个RenderStream
对象的实例,我们来看下RenderStream
对象的实现:
src/server/create-stream.js
// 继承了node的可读流stream.Readable;必须提供一个_read方法从底层资源抓取数据。通过Push(chunk)调用_read。向队列插入数据,push(null)结束
export default class RenderStream extends stream.Readable {
buffer: string; // 缓冲区字符串
render: (write: Function, done: Function) => void; // 保存传入的render方法,最后分别定义了write和end方法
expectedSize: number; // 读取队列中插入内容的大小
write: Function;
next: Function;
end: Function;
done: boolean;
constructor (render: Function) {
super() // super调用父类的构造函数
this.buffer = ''
this.render = render
this.expectedSize = 0
// 首先把text拼接到buffer缓冲区,然后判断buffer.length,如果大于expecteSize,用this.text保存
//text,同时调用this.pushBySize把缓冲区内容推入读取队列中。
this.write = createWriteFunction((text, next) => {
const n = this.expectedSize
this.buffer += text
if (this.buffer.length >= n) {
this.next = next
this.pushBySize(n)
return true // we will decide when to call next
}
return false
}, err => {
this.emit('error', err)
})
// 渲染完成后;我们应该把最后一个缓冲区推掉.
this.end = () => {
this.done = true // 标志组件的渲染已经完毕,然后调用push将缓冲区剩余内容推入读取队列中
this.push(this.buffer) //把缓冲区剩余内容推入读取队列中
}
}
//截取buffer缓冲区前n个长度的数据,推入到读取队列中,同时更新buffer缓冲区,删除前n条数据
pushBySize (n: number) {
const bufferToPush = this.buffer.substring(0, n)
this.buffer = this.buffer.substring(n)
this.push(bufferToPush)
}
tryRender () {
try {
this.render(this.write, this.end) // 开始渲染组件,在初始化RenderStream方法时传入。
} catch (e) {
this.emit('error', e)
}
}
tryNext () {
try {
this.next() // 继续渲染组件
} catch (e) {
this.emit('error', e)
}
}
_read (n: number) {
this.expectedSize = n
// 可能最后一个块增加了缓冲区到大于2 n,这意味着我们需要通过多次读取调用来消耗它
// down to < n.
if (isTrue(this.done)) { // 如果为true,则表示渲染完毕;
this.push(null) //触发结束信号
return
}
if (this.buffer.length >= n) { // 缓冲区字符串长度足够,把缓冲区内容推入读取队列。
this.pushBySize(n)
return
}
if (isUndef(this.next)) {
this.tryRender() //false,开始渲染组件
} else {
this.tryNext() //继续渲染组件
}
}
}
回顾一下,首先调用renderToStream(new Vue(option))
创建好stream
对象后,通过stream.pipe()
方法把数据发送到一个WritableStream
中,会触发RenderToStream
内部_read
方法的调用,不断把渲染的组件推入读取队列中,这个WritableStream
就可以不断地读取到组件的数据,然后输出,这样就实现了流式服务端渲染技术。