服务端渲染:
简单说:好比说一个模板,数据是从后台获取的,若是用客户端渲染那么浏览器会先渲染html
和css
,而后再经过js
的ajax
去向后台请求数据再更改渲染。就是在前端再用Node
建个后台,把首屏数据加载成一个完整的页面在node
建的后台渲染好,浏览器拿到的就是一个完整的dom
树。根据项目打开地址,路由指到哪一个页面就跳到哪。css
服务端比起客户端渲染页面的优势:
客户端渲染的一个缺点是,用户第一次访问页面,此时浏览器没有缓存,须要先从服务端下载js
,而后再经过js
操做动态添加dom
并渲染页面,时间较长。而服务端渲染的规则是,用户第一次访问浏览器能够直接解析html
文档并渲染页面,并屏渲染速度比客户端渲染更快。html
SEO
服务端渲染可让搜索引擎更容易读取页面的meta
信息,以及其它SEO
相关信息,大大增长了网站在搜索引擎中的速度。前端
HTTP
请求服务端渲染能够把一些动态数据在首次渲染时同步输出到页面,而客户端渲染须要经过AJAX
等手段异步获取这些数据,这样就至关于多了一次HTTP
请求。vue
vue
提供了renderToString
接口,能够在服务端把vue
组件渲染成模板字符串,咱们先看下用法:node
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
的实现:ajax
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
方法:promise
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
就能够不断地读取到组件的数据,而后输出,这样就实现了流式服务端渲染技术。