服務端渲染:
簡單說:好比說一個模板,數據是從後臺獲取的,若是用客戶端渲染那麼瀏覽器會先渲染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
就能夠不斷地讀取到組件的數據,而後輸出,這樣就實現了流式服務端渲染技術。