Vue SSR 從入門到 Case Study

三水清發表於2017-04-16

最近兩個專案同時開發,使用了Vue2的SSR,這樣後端渲染頁面首屏可以加快頁面呈現,增加SEO和使用者體驗,但是專案上線後卻發現了嚴重的效能問題,於是在三天內兩次重大調整,最後只能放棄Vue SSR,本文從Vue SSR實現開始,逐漸覆盤整個事件。
兩週前就預告了要寫一篇Vue SSR的文章,但是沒想到上週四上線之後,週六放量之後發現效能問題,這週一到週三,做了兩次重大調整,最終還是放棄了SSR,並且做了這次事件覆盤。

技術選型

調研Vue已經很久了,隨著Vue2正式釋出,使用Vue來做專案又燃起了希望,不是為了一時的技術理想和情懷(瞭解我的人都知道,我不是這樣的人),主要是出於下面幾方面考慮:

  1. 用artTemplate+Sass+JS做的components方案已經做了很久了,沉澱了很多元件,隨著Node服務開始上線,一直想在此基礎上做同構,而公司Node框架Yog2的view層選擇偏向於Smarty模板的Swig,修改比較麻煩
  2. 既然改不了,那麼要換不如直接選擇新的components方案,這次最強烈需求是:元件化和支援SSR,而Vue2之後支援SSR
  3. 這次兩個專案同時進行,而且僅僅給兩週的開發時間,元件化有效提高工作效率,可以把通用的元件抽象出來,多個頁面之間業務元件複用率也很高,而且業務元件在後續的運營活動也可以直接複用
  4. 手百產品形態複雜,頁面即在手百內使用又有手百外使用,手百內頁面被多個Webview隔開,不適合SPA形式,而手百外適合SPA形式,所以一套程式碼需要適配兩種情況,Vue 可以適應這兩種方式
  5. Vue的SPA形式可以方便進行PWA和Hybrid改造(繼續關注本公眾號Hybrid系列)

所以,最後決定:上Vue!技術棧:Vue2+Yog2

再介紹下兩個專案:

  • 專案A是老專案進行重構,產品需求要跟功能全部保留,架構跑通使用的是Vue2.1,所以A專案程式碼相對複雜,一直沒有使用Vue2.2
  • 專案B是新專案,開始使用Vue2.1,上線後發現已經有Vue2.2,於是升級Vue2.2,並且把專案目錄結構調整一番,Webpack config等都可配
    Vue SSR入門到上線

先看下Vue SSR的實現流程圖:

Vue SSR 從入門到 Case Study

簡單解釋一下:

  1. app.js是Server和Client公用的
  2. webpack會根據server-entry.js和client-entry.js打包出來兩個檔案:server-bundle和client-bundle
  3. server-bundle用於後端渲染(2.1是js檔案,2.2變成json,引入更加方便)

但是這張圖沒有說明在呼叫API介面方面,前後端是怎麼公用程式碼的。前端走的是Ajax請求,後端走的是http請求(百度內部是RAL介面服務管理),結合上圖補充完整的程式碼執行流程圖如下:

Vue SSR 從入門到 Case Study

webpack區分介面請求方式

在瀏覽器內使用ajax請求,而在服務端需要呼叫內部API請求或者直接讀取儲存(RAL)。ajax請求到達服務端依次經過:action層、model層,最後走到還是API請求或者讀取資料(這裡重點讀三遍。。)。

這裡我們將服務端和客戶端API的請求方法寫在不同的檔案內,但是封裝暴漏的介面都是一樣的(介面模式)。在webpack裡面,針對server和client提供不同的alias:

Vue SSR 從入門到 Case Study

Vue SSR 從入門到 Case Study

這樣 require('api/demo') 的時候,會區分開server和client。

server內直接使用yog2 modal內的獲取資料方法,比如:

Vue SSR 從入門到 Case Study

而client內,直接使用ajax請求:

Vue SSR 從入門到 Case Study

Vue內使用Vuex來獲取資料

即下圖的流程:

Vue SSR 從入門到 Case Study

在渲染的時候,prefetch階段通過dispatch觸發Store的Action(Action內允許非同步),Action內呼叫 api/demo 獲取資料成功後commit mutation,這樣整個資料就跑通了。

server.js

server.js是第一次渲染使用的入口action,核心程式碼如下:

//vue2.2
const vueServerRender = require('vue-server-renderer');
const bundle = require('../vue-ssr-bundle.json');
const renderer = vueServerRender.createBundleRenderer(bundle, {
    template: '<!--vue-ssr-outlet-->',
    cache: require('lru-cache')({
        max: 1000,
        maxAge: 1000 * 60 * 15
    })
})
// 先渲染tpl(swig模板),內容類似vue ssr demo的index.html
// 這裡渲染使用chunk,先輸出不依賴資料的頭部html
res.render('page/index.tpl', { isSendSpeedCode }, (err, html) => {
    if (!err) {
        var htmls = html.split('<!--vue-ssr-outlet-->')
        //先渲染頭部html
        res.write(htmls[0])

        // swig渲染時間
        var time1 = Date.now()
        const renderStream = renderer.renderToStream(context)
        renderStream.on('data', chunk => {
            // 邊解析,邊渲染html
            res.write(chunk)
        })
        renderStream.on('end', () => {

            if (isSendSpeedCode) {
                // 統計vue 渲染時間
                var time2 = Date.now()
                var code = `
                    <script>if(window.alog){
                        alog('speed.set', 'p_swig', ${time1 - time0});
                        alog('speed.set', 'p_vue', ${time2 - time1});
                    }</script>
                `
                res.write(code)
            }
            // 渲染尾部html
            res.end(htmls[1])
        }).on('error', errorHandler)
    } else {
        errorHandler(err)
    }
})複製程式碼

Webpack和FIS3兩次編譯

webpack是vue「全家桶」的後遺症,專案太急沒辦法去掉。我們專案的目錄結構如下:

Vue SSR 從入門到 Case Study

專案需要兩次打包:

  1. 第一次是webpack,webpack把 vue-src資料夾內容根據 server-entryclient-entry打包出來,分別放進yog2的client和server對應的檔案,之後 vue-src在執行環境就不需要了
  2. 第二次是FIS3的打包,會按照Yog2的規範release出來可以上線的內容

這裡有個細節:webpack打包出來的靜態資源路徑需要跟FIS3打包的靜態資源路徑一致,不然就沒法通過FIS3進行靜態資源定位,比如替換為CDN地址。
由於vue2.2打出來的server-bundle是json格式檔案,所以FIS無法將json內的靜態資源進行統一管理,需要webpack判斷生產環境直接替換為CDN地址

遇見的其他問題和技巧

client程式碼在server上跑

手百的通用庫Bdbox是client程式碼,程式碼中有一些window全域性變數的使用,而我們知道Node是沒有 window的,在Node執行SSR的時候,會報錯,比如下面的程式碼:

// 自執行
isAndroid: /(Android);?[\s\/]+([\d.]+)?/.test(navigator.userAgent)複製程式碼

有兩種改法:

  1. .isAndroid由屬性變成方法:.isAndroid(),放到mount內執行
  2. 給vue-server-renderer傳入帶有navigator.userAgent的context

利用resolve.alias

目錄結構深了,尤其是Vue裡面還需要呼叫yog model的程式碼,會各種../../很蛋疼,可以利用alias簡化寫法:

Vue SSR 從入門到 Case Study

需要注意的是static的寫法是:<img src=“~static/img/logo.png”

利用Yog2 的 Mock功能進行測試

訂好介面請求引數和返回資料格式之後,後端RD進行API的編寫同時,我們可以利用Yog2的Mock功能,對ral返回的資料進行假資料測試,實現後端和前端RD解耦,大大提高開發效率。

Vue SSR從上線到Case Study

現在來複盤下整個事件:

  1. 4月5日,完成程式碼開發,全功能提測,開始倒騰上線,晚上第一次上線成功,基本功能迴歸沒問題,
  2. 緊接著幾次bug修復上線,6號週四上線日,基本沒有問題了
  3. 4月7日開始APP稽核通過,放量開始,這時候發現隨著流量上升,伺服器扛不住了
  4. 8日(週六)緊急新增例項,週末算是硬扛過去了
  5. 10(週一)排查原因,發現記憶體可能存在洩漏和效能問題,增加打點統計後端渲染時間,但是VM相對來說是黑盒,效能不好排查
  6. 11日(週二)增加lru-cache,細化元件快取,下午上線後,晚上發現記憶體曲線更加嚴重,於是晚上10點回滾lru和元件快取程式碼,隨版本收斂影響,流量繼續上漲,增加機器例項
  7. 12日(週三)採取降級方案,第一次進入頁面將API資料放到以變數形式放到頁面,然後增加beforeCreate階段程式碼,將頁面資料直接commit給mutation進行渲染,曲線開始平緩
  8. 13日(週四)觀察一晚曲線沒有問題,中午開始縮容(下線例項)

從週一到週三經過兩次大的調整,終於服務穩定了,其中程式碼review階段,我們也發現了很多程式碼不規範的現象。下面來說下我們使用vue ssr的一些壓測等資料。

單例項QPS、記憶體和CPU資料

從上線之後,記憶體積累到一定時間就飆升,記憶體飆升同時,CPU也進行飆升,具體曲線如下:

Vue SSR 從入門到 Case Study

Vue SSR 從入門到 Case Study

從12日(週三19點)上線之後,就開始平穩了,13日中午縮容後,CPU稍有上揚。

同期QPS的資料如下:

Vue SSR 從入門到 Case Study

檢視全文,關注微信公眾號:「三水清」(sanshuiqing123)

Vue SSR 從入門到 Case Study

相關文章