今天來讀點vue-server-renderer吧~?

目標?

  1. 今天要讀的是vue-server-renderer。(當前的vue版本是2.5.17-beta.0)

為什麼要讀這玩意兒?

  1. 因為好奇心和閑的沒事幹!(逃

讀了這篇文章能夠獲得什麼?

  1. 又浪費了半到一個小時的時間
  2. 好奇心得到了不怎麼充分的滿足

vue-server-renderer的用法?

// 第 1 步:創建一個 Vue 實例const Vue = require(vue)const app = new Vue({template: `<div>Hello World</div>`})// 第 2 步:創建一個 rendererconst renderer = require(vue-server-renderer).createRenderer()// 第 3 步:將 Vue 實例渲染為 HTMLrenderer.renderToString(app, (err, html) => {if (err) throw errconsole.log(html)// => <div data-server-rendered="true">Hello World</div>})

簡單來說就是,你給我一個Vue實例對象,我給你HTML字元串。

實現的大體思路是?

傳入vdom之後,根據vnode的不同類型,調用不同的render,輸出字元串,然後拼接在一起。對於具有children的vnode,render自身的上下文中維護了一個renderState棧,用於處理各種嵌套結構。(直白些說,就是補上</xxx>這樣的標籤尾)

核心代碼其實就是這樣一段(在/server/render.js中):

function renderNode(node, isRoot, context) { if (node.isString) { renderStringNode(node, context) // 渲染字元串節點 } else if (isDef(node.componentOptions)) { renderComponent(node, isRoot, context) // 渲染Vue組件 } else if (isDef(node.tag)) { renderElement(node, isRoot, context) // 渲染HTML Element } 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 ) }}

這之中,renderStringNoderenderComponentRenderElement以及下面的renderAsyncComponent、和直接寫上去的html注釋等情況,就是用來往SSR輸出時的字元串上直接拼的。write函數長這樣(截取自/server/create-renderer.js::createRenderer函數返回的renderToString函數):

let result = const write = createWriteFunction(text => { result += text return false}, cb)

於是,雖然整段SSR的代碼似乎有些複雜還很繞,但是終究還是拼字元串的遊戲。

為了了解renderStringNoderenderComponentRenderElement三個函數都幹了什麼,我在源碼中加了一些log。不過,在看這三個函數之前,我們需要先了解一下SSR模塊中的上下文。

server/render-context.js

主要需要關注的是 context.renderStates這個數組,以及contex.next()函數。

export class RenderContext { renderStates: Array<RenderState>; write: (text: string, next: Function) => void; next: () => void; done: (err: ?Error) => void; /* ...省略一部分代碼... */ constructor (options: Object) { /* ...省略一部分代碼... */ } /** * 渲染上下文的next里會調用write,向html str里寫入內容 */ next () { const lastState = this.renderStates[this.renderStates.length - 1] if (isUndef(lastState)) { return this.done() } switch (lastState.type) { case Element: case Fragment: const { children, total } = lastState const rendered = lastState.rendered++ if (rendered < total) { // 對子節點的渲染 this.renderNode(children[rendered], false, this) } else { this.renderStates.pop() if (lastState.type === Element) { // 子節點渲染完畢,補個標籤尾上去 this.write(lastState.endTag, this.next) } else { this.next() } } break case Component: this.renderStates.pop() this.activeInstance = lastState.prevActive this.next() break /* 省略一部分代碼... */ } }}

在renderNode過程中,有些情況下,會往renderStates裡面放進去一些renderState對象。然後就可以調用next()來消費掉這些renderState。

比如,一個這樣的HTML標籤

<div> some text <my-vue-component></my-vue-component> <span>some text </span> </div>

第一個div寫完之後,要放n個子節點進去,走其他的renderNode邏輯,最後再放自己的</div>進去,這種時候,就需要用到renderState了。renderState是棧結構,也是因為這個原因。

接下來,就看看之前提到的renderStringNode的函數。

renderStringNode

首先是renderStringNode,需要注意的是,這裡的StringNode並不是HTML裡面的TextNode。而是那種並非由自定義vue組件生成的HTML文本,是允許有標籤在裡面的。甚至還允許有children,children裡面有各種element和component。

所以你會見到,對於是否存在children,代碼里有兩種分支。

function renderStringNode(el, context) { const {write, next} = context if (isUndef(el.children) || el.children.length === 0) { // 如果不存在children write(el.open + (el.close || ), next) } else { // 如果存在children const children: Array<VNode> = el.children // 這時會把children塞進renderStates裡面, // 寫標籤頭,並調用next()去渲染children,以及標籤尾 context.renderStates.push({ type: Element, children, rendered: 0, total: children.length, endTag: el.close }) write(el.open, next) }}

renderElement

讀過renderStringNode之後,RenderElement就很好理解了。就是寫個標籤頭,然後根據情況看是不是把children塞進renderStates去,是不是寫標籤尾之類的。

function renderElement(el, isRoot, context) { const {write, next} = context if (isTrue(isRoot)) { // 根節點需要加一個SSR_ATTR的設定 if (!el.data) el.data = {} if (!el.data.attrs) el.data.attrs = {} el.data.attrs[SSR_ATTR] = true } if (el.fnOptions) { registerComponentForCache(el.fnOptions, write) } // 渲染出 element 的頭部tag // 就是處理各種class attrs style之類的東西,被稱作是modules,和snabbdom有點像? const startTag = renderStartingTag(el, context) const endTag = `</${el.tag}>` if (context.isUnaryTag(el.tag)) { // 如果是br之類的tag,是不需要標籤尾的 write(startTag, next) } else if (isUndef(el.children) || el.children.length === 0) { // 如果沒有children,帶上endTag直出字元串吧 write(startTag + endTag, next) } else { // 如果存在children,就要塞進renderStates隊列了。 const children: Array<VNode> = el.children context.renderStates.push({ type: Element, children, rendered: 0, total: children.length, endTag }) write(startTag, next) }}

renderComponentInner

renderComponent裡面會考慮一些和組件緩存相關的內容,不過如果沒有緩存的話,其實就是調用這個函數。

function renderComponentInner(node, isRoot, context) { const prevActive = context.activeInstance node.ssrContext = context.userContext const child = context.activeInstance = createComponentInstanceForVnode( node, context.activeInstance ) normalizeRender(child) const childNode = child._render() childNode.parent = node context.renderStates.push({ type: Component, prevActive }) renderNode(childNode, isRoot, context)}

activeInstance存儲了當前組件的一些信息。如果沒有當前組件的話,它就是Vue的根對象了。(嗯,這段代碼的意思,其實就是,沒有vnode就用_render()創造一個vnode繼續渲染。> <)

其他

vue-server-render雖然看上去是個單獨的庫,但是實際上代碼卻是在vue項目下的。

入口是這裡:/src/platforms/web/entry-server-renderer.js

但是這個文件其實沒什麼鳥用,就是把/src/server/create-renderer.js包裹了一下。

create-renderer這個文件顧名思義,就是用來create一個/src/server/renderer.js裡面的render的。真正的核心邏輯就是在這個文件裡面。(抖

/src/platform/web/server/modules裡面放了一些處理attr/class/style/dom-props的module,裡面大都也是拼字元串放進標籤頭的邏輯。

沒什麼鳥用的結尾

總之就是看了一些關於花式拼字元串的代碼……

恭喜您,在這裡又浪費了半個多小時。

不過,實際上vue-server-renderer的源碼中,有一部分是和組件的緩存有關的。比較複雜,感覺自己沒信心和耐心能把這些東西的分析條理清晰準確無誤地寫出來……所以就有技巧性地跳過去了。當然,我也不是說現在寫的這些東西就準確無誤了……(逃

以後有精力了再加個DLC補上吧……

說到緩存,順帶一提,當初我也是沒看完vue-server-render的文當就直接看代碼的猛士。結果遇到緩存邏輯就不知道他在幹啥,內心中充滿了各種"卧槽這是啥"的呼喊。後來發現自己竟然漏掉了一部分文檔沒看……

說真的,想去看源碼的話,一定要先搞明白這個庫是怎麼用的……否則真的開始讀之後簡直事倍功半……(

最後……emmm,最近在沉迷超次元遊戲海王星系列。我和你說,這瓜超甜。

接下來的目標是想看看HTML parser什麼的……然而又很想玩遊戲。

附錄:做測試時用的template和log

試圖做伺服器端渲染時用的template:

<div>hello, <br> <span>world</span><div>this is a block <br> this is url: <show-url></show-url></div><div>{{greeting}}</div><show-url></show-url></div>

打出來的log。我覺得,比起讀代碼來,這種log說不定可以更加直接地表現出vue-server-renderer的工作方式……

┌┄┄┄┄│ current render states: []│ render element // 調用RenderElement函數會做這個log│ write with children: <div data-server-rendered="true"> // 當有children時的log拼接字元串: <div data-server-rendered="true"> //拼接結果: <div data-server-rendered="true"> // 這一句和上一句,都是在write函數裡面加上的log│ ┌┄┄┄┄│ │ current render states: [ Element ] // 這裡輸出renderStates棧的內容│ │ render string node // 調用renderStringNode時的log│ │ write: hello, <br> <span>world</span> // 看到了吧,stringNode不等同於HTML的TextNode……其實是和「是不是Vue的組件」有關的拼接字元串: hello, <br> <span>world</span>拼接結果: <div data-server-rendered="true">hello, <br> <span>world</span>│ │ ┌┄┄┄┄│ │ │ current render states: [ Element ]│ │ │ render string node│ │ │ write string node with children. <div>拼接字元串: <div>拼接結果: <div data-server-rendered="true">hello, <br> <span>world</span><div>│ │ │ ┌┄┄┄┄│ │ │ │ current render states: [ Element, Element ]│ │ │ │ render string node│ │ │ │ write: this is a block <br> this is url:拼接字元串: this is a block <br> this is url:拼接結果: <div data-server-rendered="true">hello, <br> <span>world</span><div>this is a block <br> this is url:│ │ │ │ ┌┄┄┄┄│ │ │ │ │ current render states: [ Element, Element ]│ │ │ │ │ renderComponentInner -> vue-component-1-show-url│ │ │ │ │ ┌┄┄┄┄│ │ │ │ │ │ current render states: [ Element, Element, Component ]│ │ │ │ │ │ render element│ │ │ │ │ │ write with children: <div>拼接字元串: <div>拼接結果: <div data-server-rendered="true">hello, <br> <span>world</span><div>this is a block <br> this is url: <div>│ │ │ │ │ │ ┌┄┄┄┄│ │ │ │ │ │ │ current render states: [ Element, Element, Component, Element ]│ │ │ │ │ │ │ render string node│ │ │ │ │ │ │ write: /demo拼接字元串: /demo拼接結果: <div data-server-rendered="true">hello, <br> <span>world</span><div>this is a block <br> this is url: <div>/demoelement end with tag: </div>拼接字元串: </div>拼接結果: <div data-server-rendered="true">hello, <br> <span>world</span><div>this is a block <br> this is url: <div>/demo</div>element end with tag: </div>拼接字元串: </div>拼接結果: <div data-server-rendered="true">hello, <br> <span>world</span><div>this is a block <br> this is url: <div>/demo</div></div>│ │ │ │ │ │ │ ┌┄┄┄┄│ │ │ │ │ │ │ │ current render states: [ Element ]│ │ │ │ │ │ │ │ render string node│ │ │ │ │ │ │ │ write: <div>welcomes you</div>拼接字元串: <div>welcomes you</div>拼接結果: <div data-server-rendered="true">hello, <br> <span>world</span><div>this is a block <br> this is url: <div>/demo</div></div><div>welcomes you</div>│ │ │ │ │ │ │ │ ┌┄┄┄┄│ │ │ │ │ │ │ │ │ current render states: [ Element ]│ │ │ │ │ │ │ │ │ renderComponentInner -> vue-component-1-show-url│ │ │ │ │ │ │ │ │ ┌┄┄┄┄│ │ │ │ │ │ │ │ │ │ current render states: [ Element, Component ]│ │ │ │ │ │ │ │ │ │ render element│ │ │ │ │ │ │ │ │ │ write with children: <div>拼接字元串: <div>拼接結果: <div data-server-rendered="true">hello, <br> <span>world</span><div>this is a block <br> this is url: <div>/demo</div></div><div>welcomes you</div><div>│ │ │ │ │ │ │ │ │ │ ┌┄┄┄┄│ │ │ │ │ │ │ │ │ │ │ current render states: [ Element, Component, Element ]│ │ │ │ │ │ │ │ │ │ │ render string node│ │ │ │ │ │ │ │ │ │ │ write: /demo拼接字元串: /demo拼接結果: <div data-server-rendered="true">hello, <br> <span>world</span><div>this is a block <br> this is url: <div>/demo</div></div><div>welcomes you</div><div>/demoelement end with tag: </div>拼接字元串: </div>拼接結果: <div data-server-rendered="true">hello, <br> <span>world</span><div>this is a block <br> this is url: <div>/demo</div></div><div>welcomes you</div><div>/demo</div>element end with tag: </div>拼接字元串: </div> // 上面這些log,就是在寫el.close了,在next()時調用的。拼接結果: <div data-server-rendered="true">hello, <br> <span>world</span><div>this is a block <br> this is url: <div>/demo</div></div><div>welcomes you</div><div>/demo</div></div>│ │ │ │ │ │ │ │ │ │ │ write end. <- /demo│ │ │ │ │ │ │ │ │ │ └┄┄┄┄│ │ │ │ │ │ │ │ │ │ write end. <- <div>│ │ │ │ │ │ │ │ │ └┄┄┄┄│ │ │ │ │ │ │ │ │ renderComponentInner end. <- vue-component-1-show-url│ │ │ │ │ │ │ │ └┄┄┄┄│ │ │ │ │ │ │ │ write end. <- <div>welcomes you</div>│ │ │ │ │ │ │ └┄┄┄┄│ │ │ │ │ │ │ write end. <- /demo│ │ │ │ │ │ └┄┄┄┄│ │ │ │ │ │ write end. <- <div>│ │ │ │ │ └┄┄┄┄│ │ │ │ │ renderComponentInner end. <- vue-component-1-show-url│ │ │ │ └┄┄┄┄│ │ │ │ write end. <- this is a block <br> this is url:│ │ │ └┄┄┄┄│ │ │ write end. <- <div>│ │ └┄┄┄┄│ │ write end. <- hello, <br> <span>world</span>│ └┄┄┄┄│ write end. <- <div data-server-rendered="true">└┄┄┄┄

推薦閱讀:

聽說你想寫代碼。
前端日刊-2018.01.06
移動端頁面的樣式
解密Vue SSR
Node手把手構建靜態文件伺服器

TAG:前端開發 | ServerSideRendering伺服器端渲染 | Vuejs |