Why not Nuxt
大佬們進(jìn)來一定會(huì)有一個(gè)疑問:為什么已經(jīng)有vue-ssr了(如nuxt框架)還需要用go來渲染呆奕?vue-ssr提供的前后端同構(gòu)、單頁應(yīng)用加上vue的數(shù)據(jù)綁定功能,能少寫很多代碼婴程,它不香嗎?
筆者的回答是香,也不香,確實(shí)Vue用它簡(jiǎn)單易上手的特性得到了很多人喜愛阳啥,也包括我,所以當(dāng)我需要服務(wù)端渲染的時(shí)候财喳,也自然的使用了vue-ssr察迟,選用的nuxtjs.org框架,但事物總有好有壞耳高,很快我就發(fā)現(xià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)就是首屏直出误证,是不是就能解決首屏慢的問題了呢继薛?并不能。在vue-ssr渲染過程中雷厂,服務(wù)端渲染只是其中一半,當(dāng)首屏數(shù)據(jù)到達(dá)瀏覽器之后叠殷,為了能夠?qū)崿F(xiàn)vue的響應(yīng)式數(shù)據(jù)改鲫,則還需要一步操作:客戶端激活,這一步的性能將影響什么呢林束?
如果客戶端激活速度過慢會(huì)發(fā)生以下問題:
- 用戶將先看到頁面內(nèi)容像棘,但是會(huì)卡一小會(huì)沒響應(yīng)(如沒辦法滑動(dòng)),這是因?yàn)榭蛻舳思せ钍且粋€(gè)很耗cpu的操作壶冒。
- 業(yè)務(wù)js執(zhí)行變慢缕题,如懶加載、動(dòng)效代碼都會(huì)在客戶端激活完成之后才會(huì)執(zhí)行胖腾,這會(huì)導(dǎo)致用戶首先看不到圖片或者動(dòng)效烟零,給用戶卡頓的感覺,在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,不過也許客戶端激活的性能還不是重點(diǎn)记罚,因?yàn)榻酉聛磉€有Node端渲染的性能問題墅诡。
在我參與的項(xiàng)目中,由于頁面功能復(fù)雜桐智,一個(gè)頁面需要500ms左右的渲染時(shí)間末早,也由于有動(dòng)態(tài)路由參數(shù)的功能存在,沒辦法像靜態(tài)頁面一樣加上緩存说庭,就導(dǎo)致了在并發(fā)稍微高一點(diǎn)之后然磷,響應(yīng)速度越來越慢。
可擴(kuò)展性低
大量的代碼被封裝到了nuxt里, 過多的配置項(xiàng)被放在了nuxt.config.js中, 不夠靈活就導(dǎo)致了很多特性沒辦法實(shí)現(xiàn):
- 如要修改head必須修改meta, 但vue-meta配置是有限的, 比如不支持meta標(biāo)簽閉合(可惡的搜狗站長(zhǎng)認(rèn)證需要閉合的meta標(biāo)簽).
- 如publicPath無法動(dòng)態(tài)修改.
當(dāng)你想做一個(gè)更復(fù)雜的網(wǎng)站時(shí), nuxt雖然開箱即用但卻又像一個(gè)盒子一樣讓你四處碰壁.
所以我決定放棄龐大笨重(對(duì)于我們的項(xiàng)目來說)的nuxt, 回歸字符串渲染.
思考
也許在面臨更為致命的性能問題時(shí)刊驴,什么響應(yīng)式样屠、數(shù)據(jù)綁定功能也不再重要,我們開始考慮傳統(tǒng)模板引擎缺脉。
我們知道傳統(tǒng)模板引擎的性能很好痪欲,因?yàn)樗麄兪腔谧址唇佣皇翘摂M節(jié)點(diǎn)再轉(zhuǎn)dom,但美中不足的是他們都不如vue模板美觀好用(就不對(duì)比JSX了攻礼,抱歉我對(duì)JSX不熟悉)业踢,可以預(yù)見當(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)潔、專業(yè)雇锡、現(xiàn)代化)逛钻,何樂而不為?
難點(diǎn)
使用Go來渲染Vue模板并不容易實(shí)現(xiàn)锰提,隨便一想便知道其中的難點(diǎn):
- 解析vue各種語法(如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(抽象語法樹) 這是可行的旺订,但工作量也很大。 - 生成Go代碼超燃,為了減少運(yùn)行時(shí)損耗耸峭,和webpack打包原理一樣,我們需要提前對(duì)代碼進(jìn)行處理淋纲,也就是生成中間代碼劳闹。和vue-loader類似,在這個(gè)項(xiàng)目中洽瞬,需要我們從Vue模板生成render函數(shù)本涕,不同的是我們的render函數(shù)是Golang語言的伙窃。
不過既然都是可行的晦闰,不妨試試呻右。
制作
從構(gòu)建一個(gè)最小化模型開始声滥,我們要渲染的模板是這個(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)樹
解析html比我想象中復(fù)雜纽疟,這是因?yàn)橛凶蚤]合和不閉合的標(biāo)簽污朽,如<meta charset="UTF-8">
,如果使用xml的處理邏輯的話需要做很多額外判斷颓芭,為了不重復(fù)造輪子官紫,最終選用golang.org/x/net/html
包來解析html,不過值得注意的是正規(guī)的html格式有一些要求:如select里只能包含option子節(jié)點(diǎn)束世,但Vue模板由于有自定義組件和slot語法等,可能不滿足html的要求,這會(huì)讓html包無法正確解析出節(jié)點(diǎn),由于沒有更好的解析包作為代替,無奈只好魔改一點(diǎn)html包了,改好的代碼在項(xiàng)目里熄云,可以翻到文末查閱妙真。
2. 解析vue模板語法
這一步十分簡(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)樹生成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è)庫可以解析JS AST: https://github.com/robertkrimen/otto, 唯一不足就是只支持ES5, 不過ES5在模板中足夠了.
得到Js AST之后就需要將AST翻譯成Golang,難度不大疟赊,多寫幾個(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)開源,希望能讓喜愛Vue和Go的伙伴們多一個(gè)可嘗試的東西近哟,同時(shí)也感謝你的ISSUE驮审。
- https://github.com/zbysir/go-vue-ssr :預(yù)編譯成go語言運(yùn)行。
- https://github.com/zbysir/vpl :直接運(yùn)行模板,更方便疯淫。
目前已經(jīng)運(yùn)行在公司項(xiàng)目中地来,你可以訪問http://zhuzi.com.cn查看渲染效果。