摘要:在實際應用中,我們可以將回調函數拿到的拼接到模板中,下面看下的實現把轉換模板字符串方法不斷拼接模板字符串,用做存儲,然后調用當通過完畢,執行傳入方法支持傳入實例和渲染完成后的回調函數。
服務端渲染:
簡單說:比如說一個模板,數據是從后臺獲取的,如果用客戶端渲染那么瀏覽器會先渲染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{ 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(``, context.next) } } else { context.write( node.raw ? node.text : escape(String(node.text)), context.next ) } }
/**主要功能是把VNode對象渲染成dom元素。 * 先判斷是不是根元素,然后渲染開始開始標簽,如果是自閉合標簽直接寫入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流式服務端渲染= 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就可以不斷地讀取到組件的數據,然后輸出,這樣就實現了流式服務端渲染技術。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/93079.html
摘要:公司的招聘要求都提到了至少熟悉其中一種前端框架,有前端工程化與模塊化開發實踐經驗相關字眼。我們主要從端公眾號移動端小程序三大平臺進行前端的技術選型,并來說說選其技術的幾大優勢。技術的優勢互聯網前端大潮后,前端出現了大框架,分別是與。 1、技術選型的背景前端技術發展日新月異,互聯網上出現的新型框架也比較多,如何讓新招聘的人員...
摘要:所以,這次就來聊聊組件的服務器端渲染。這種模式下,后端只提供接口,傳統的服務器端路由模板渲染則都有層接管。這樣,前端開發人員可以自由的決定哪些組件需要在服務器端渲染,哪些組件可以放在客戶端渲染,前后端完全解耦,但又保留了服務器端渲染的功能。 細說 Vue 組件的服務器端渲染 聲明:需要讀者對 NodeJs、Vue 服務器端渲染有一定的了解 現在,前后端分離與客戶端渲染已經成為前端開發的...
摘要:有目錄結構書寫方式組件集成項目構建等的約束,整個應用中是沒有文件的,所有的響應都是動態渲染的,包括里面的元信息路徑等。更多參考細說后端模板渲染客戶端渲染中間層服務器端渲染開發工具開發時主要會用到的工具。 vue 前端項目技術選型、開發工具、周邊生態 聲明:這不是一篇介紹 Vue 基礎知識的文章,需要熟悉 Vue 相關知識 主架構:vue, vue-router, vuex UI 框...
摘要:有目錄結構書寫方式組件集成項目構建等的約束,整個應用中是沒有文件的,所有的響應都是動態渲染的,包括里面的元信息路徑等。更多參考細說后端模板渲染客戶端渲染中間層服務器端渲染開發工具開發時主要會用到的工具。 vue 前端項目技術選型、開發工具、周邊生態 聲明:這不是一篇介紹 Vue 基礎知識的文章,需要熟悉 Vue 相關知識 主架構:vue, vue-router, vuex UI 框...
摘要:平臺主要功能如下支持客戶端渲染和服務端渲染微信登錄鑒權頁面組件增刪改查,復制移動等圖片上傳微信文章一鍵復制等等動態組件的配置原理之后專門用一篇文章詳細寫吧持續集成這個其實也不算是項目,算是前端的工具。 2017年算是踏入真正的前端的一年,從實習到去年,說是前端的崗位,但卻因為實習生的身份、公司技術不夠等原因,一直停留在傳統的html+css+jq,那時候感覺前端的世界在翻天覆地地變化,...
閱讀 1248·2021-11-22 13:54
閱讀 1435·2021-11-22 09:34
閱讀 2712·2021-11-22 09:34
閱讀 4024·2021-10-13 09:39
閱讀 3349·2019-08-26 11:52
閱讀 3370·2019-08-26 11:50
閱讀 1538·2019-08-26 10:56
閱讀 1920·2019-08-26 10:44