服务端衬着:
简单说:比如说一个模板,数据是从背景猎取的,假如用客户端衬着那末浏览器会先衬着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
就可以够不停地读取到组件的数据,然后输出,如许就完成了流式服务端衬着手艺。