Why not Nuxt
大佬們進(jìn)來(lái)一定會(huì)有一個(gè)疑問(wèn):為什么已經(jīng)有vue-ssr了(如nuxt框架)還需要用go來(lái)渲染?vue-ssr提供的前后端同構(gòu)、單頁(yè)應(yīng)用加上vue的數(shù)據(jù)綁定功能,能少寫(xiě)很多代碼,它不香嗎?
筆者的回答是香,也不香,確實(shí)Vue用它簡(jiǎn)單易上手的特性得到了很多人喜愛(ài),也包括我,所以當(dāng)我需要服務(wù)端渲染的時(shí)候,也自然的使用了vue-ssr,選用的nuxtjs.org框架,但事物總有好有壞,很快我就發(fā)現(xiàn)了它的問(wèn)題。
性能低
如果項(xiàng)目是一個(gè)后臺(tái)管理系統(tǒng),那么首屏渲染速度和運(yùn)行時(shí)的性能可能不怎么重要,但如果是一個(gè)面向C端的網(wǎng)站,響應(yīng)速度卻十分總要,因?yàn)檫@直接影響到用戶體驗(yàn)。
SSR有一個(gè)優(yōu)點(diǎn)就是首屏直出,是不是就能解決首屏慢的問(wèn)題了呢?并不能。在vue-ssr渲染過(guò)程中,服務(wù)端渲染只是其中一半,當(dāng)首屏數(shù)據(jù)到達(dá)瀏覽器之后,為了能夠?qū)崿F(xiàn)vue的響應(yīng)式數(shù)據(jù),則還需要一步操作:客戶端激活,這一步的性能將影響什么呢?
如果客戶端激活速度過(guò)慢會(huì)發(fā)生以下問(wèn)題:
- 用戶將先看到頁(yè)面內(nèi)容,但是會(huì)卡一小會(huì)沒(méi)響應(yīng)(如沒(méi)辦法滑動(dòng)),這是因?yàn)榭蛻舳思せ钍且粋€(gè)很耗cpu的操作。
- 業(yè)務(wù)js執(zhí)行變慢,如懶加載、動(dòng)效代碼都會(huì)在客戶端激活完成之后才會(huì)執(zhí)行,這會(huì)導(dǎo)致用戶首先看不到圖片或者動(dòng)效,給用戶卡頓的感覺(jué),在cpu更慢的手機(jī)端尤為明顯。
客戶端激活的性能也是有辦法調(diào)優(yōu)的,比如這篇文章提到的懶激活vue-lazy-hydration:How to Drastically Reduce Estimated Input Latency and Time to Interactive of SSR Vue.js Applications,不過(guò)也許客戶端激活的性能還不是重點(diǎn),因?yàn)榻酉聛?lái)還有Node端渲染的性能問(wèn)題。
在我參與的項(xiàng)目中,由于頁(yè)面功能復(fù)雜,一個(gè)頁(yè)面需要500ms左右的渲染時(shí)間,也由于有動(dòng)態(tài)路由參數(shù)的功能存在,沒(méi)辦法像靜態(tài)頁(yè)面一樣加上緩存,就導(dǎo)致了在并發(fā)稍微高一點(diǎn)之后,響應(yīng)速度越來(lái)越慢。
可擴(kuò)展性低
大量的代碼被封裝到了nuxt里, 過(guò)多的配置項(xiàng)被放在了nuxt.config.js中, 不夠靈活就導(dǎo)致了很多特性沒(méi)辦法實(shí)現(xiàn):
- 如要修改head必須修改meta, 但vue-meta配置是有限的, 比如不支持meta標(biāo)簽閉合(可惡的搜狗站長(zhǎng)認(rèn)證需要閉合的meta標(biāo)簽).
- 如publicPath無(wú)法動(dòng)態(tài)修改.
當(dāng)你想做一個(gè)更復(fù)雜的網(wǎng)站時(shí), nuxt雖然開(kāi)箱即用但卻又像一個(gè)盒子一樣讓你四處碰壁.
所以我決定放棄龐大笨重(對(duì)于我們的項(xiàng)目來(lái)說(shuō))的nuxt, 回歸字符串渲染.
思考
也許在面臨更為致命的性能問(wèn)題時(shí),什么響應(yīng)式、數(shù)據(jù)綁定功能也不再重要,我們開(kāi)始考慮傳統(tǒng)模板引擎。
我們知道傳統(tǒng)模板引擎的性能很好,因?yàn)樗麄兪腔谧址唇佣皇翘摂M節(jié)點(diǎn)再轉(zhuǎn)dom,但美中不足的是他們都不如vue模板美觀好用(就不對(duì)比JSX了,抱歉我對(duì)JSX不熟悉),可以預(yù)見(jiàn)當(dāng)項(xiàng)目復(fù)雜之后傳統(tǒng)模板的代碼將一團(tuán)糟。
正好筆者熟悉Golang和Vue,如果能讓Golang在后端發(fā)揮它的優(yōu)點(diǎn)(并發(fā)、性能),讓Vue(模板)發(fā)揮它的優(yōu)點(diǎn)(簡(jiǎn)潔、專(zhuān)業(yè)、現(xiàn)代化),何樂(lè)而不為?
難點(diǎn)
使用Go來(lái)渲染Vue模板并不容易實(shí)現(xiàn),隨便一想便知道其中的難點(diǎn):
- 解析vue各種語(yǔ)法(如slot、v-if、v-for)并一一實(shí)現(xiàn),這可能不復(fù)雜,但工作量很大。
- 解析js表達(dá)式,在模板中會(huì)大量使用到j(luò)s表達(dá)式,
如v-if = "a != 0",現(xiàn)在需要使用Go去計(jì)算這些表達(dá)式,雖然知道有AST(抽象語(yǔ)法樹(shù)) 這是可行的,但工作量也很大。 - 生成Go代碼,為了減少運(yùn)行時(shí)損耗,和webpack打包原理一樣,我們需要提前對(duì)代碼進(jìn)行處理,也就是生成中間代碼。和vue-loader類(lèi)似,在這個(gè)項(xiàng)目中,需要我們從Vue模板生成render函數(shù),不同的是我們的render函數(shù)是Golang語(yǔ)言的。
不過(guò)既然都是可行的,不妨試試。
制作
從構(gòu)建一個(gè)最小化模型開(kāi)始,我們要渲染的模板是這個(gè)樣子的
<template>
<div>
<span class="bg-gray" :class="cus_class" :style="{'font-size': fontSize+'px'}"> {{msg}} </span>
</div>
</template>
我們將這個(gè)組件命名為消息提示組件,它可能是這個(gè)樣子

1. 解析html成節(jié)點(diǎn)樹(shù)
解析html比我想象中復(fù)雜,這是因?yàn)橛凶蚤]合和不閉合的標(biāo)簽,如<meta charset="UTF-8">,如果使用xml的處理邏輯的話需要做很多額外判斷,為了不重復(fù)造輪子,最終選用golang.org/x/net/html包來(lái)解析html,不過(guò)值得注意的是正規(guī)的html格式有一些要求:如select里只能包含option子節(jié)點(diǎn),但Vue模板由于有自定義組件和slot語(yǔ)法等,可能不滿足html的要求,這會(huì)讓html包無(wú)法正確解析出節(jié)點(diǎn),由于沒(méi)有更好的解析包作為代替,無(wú)奈只好魔改一點(diǎn)html包了,改好的代碼在項(xiàng)目里,可以翻到文末查閱。
2. 解析vue模板語(yǔ)法
這一步十分簡(jiǎn)單,我們只需要遞歸遍歷html節(jié)點(diǎn)數(shù)中的節(jié)點(diǎn),根據(jù)節(jié)點(diǎn)的attr,再生成一個(gè)vue節(jié)點(diǎn)結(jié)構(gòu)體,其中包含如porps,v-if等信息。這一步是為了方便的從節(jié)點(diǎn)樹(shù)生成Golang代碼。
3. 生成Go代碼
遞歸節(jié)點(diǎn)
我們需要根據(jù)節(jié)點(diǎn)生成Go代碼,特別要處理的是vue的各個(gè)指令,如v-if需要生成如下的Go代碼
var s = ""
if xxx {
s = "<div></div>"
} else {
s = "text"
}
retun s
v-for如下
var s = ""
for i, v := range arr{
s+= "<div></div>"
}
return s
這里不難,唯一難點(diǎn)是v-if/v-else/v-else-if的關(guān)聯(lián)關(guān)系,我也是參考vue官方的模板處理方法才實(shí)現(xiàn)的。
解析 Js AST
在v-if或者{{}}中需要使用一些js表達(dá)式,如 v-if="a!=b && a!=c",幸運(yùn)的是Golang有一個(gè)庫(kù)可以解析JS AST: https://github.com/robertkrimen/otto, 唯一不足就是只支持ES5, 不過(guò)ES5在模板中足夠了.
得到Js AST之后就需要將AST翻譯成Golang,難度不大,多寫(xiě)幾個(gè)switch case就好。代碼在此
最終生成的Go代碼會(huì)像這樣:
// Code generated by go-vue-ssr: https://github.com/zbysir/go-vue-ssr
// src_hash:535087cd1e2031e7772d0d62e5390830
package main
func (r *Render) Component_info(options *Options) string {
this := extendMap(r.Prototype, options.Props)
_ = this
return r.tag("div", true, &Options{
Style: map[string]string{"text-align": "center"},
Slot: map[string]NamedSlotFunc{"default": func(props map[string]interface{}) string {
return "<p style=\"padding: 10px 0; \"" + mixinAttr(nil, nil, map[string]interface{}{"height": interfaceAdd(lookInterface(this, "height"), 1)}) + ">" + interfaceToStr(lookInterface(this, "slogan"), true) + "</p><img" + mixinAttr(nil, map[string]string{"alt": "todo logo", "height": "50px"}, map[string]interface{}{"src": lookInterface(this, "logo")}) + "></img>"
}},
P: options,
Data: this,
})
}
現(xiàn)在只需要調(diào)用則可以返回html字符串
r := NewRender()
htmlStr := r.Component_info(&Options{
Props: map[string]interface{}{
"title": "go-vue-ssr",
"slogan": "Hey vue go",
"info": map[string]interface{}{
"author": "bysir",
"Hey vue go":"Hey vue go",
},
"logo": "https://avatars2.githubusercontent.com/u/13434040?s=88&v=4",
"height": 100.1,
},
})
結(jié)果
項(xiàng)目已經(jīng)開(kāi)源,希望能讓喜愛(ài)Vue和Go的伙伴們多一個(gè)可嘗試的東西,同時(shí)也感謝你的ISSUE。
- https://github.com/zbysir/go-vue-ssr :預(yù)編譯成go語(yǔ)言運(yùn)行。
- https://github.com/zbysir/vpl :直接運(yùn)行模板,更方便。
目前已經(jīng)運(yùn)行在公司項(xiàng)目中,你可以訪問(wèn)http://zhuzi.com.cn查看渲染效果。