vue服務端渲染 同構渲染

引言

自JavaScript誕生以來,前端技術發展很是迅速。移動端白屏優化是前端界面體驗的一個重要優化方向,Web 前端誕生了 SSR 、CSR、預渲染等技術。css

十年前,幾乎全部網站都使用 ASP、Java、PHP 這類作後端渲染,但後來隨着 jQuery、Angular、React、Vue 等 JS 框架的崛起,開始轉向了前端渲染。2014年起又興起了同構渲染,號稱是將來,集成了先後端渲染的優勢,當真如此?html

咱們先明確三個概念:前端

後端渲染:後端渲染指傳統的 ASP、Java 或 PHP 的渲染機制;vue

CSR:前端渲染 指使用 JS 來渲染頁面大部份內容,表明是如今流行的 SPA 單頁面應用;node

同構渲染:指先後端共用 JS,首次渲染時使用 Node.js 來直出 HTML。通常來講同構渲染是介於先後端中的共有部分webpack

以及經常使用性能測試時間點ios

  • 首次內容繪製FCP(frist conntentful paint) 瀏覽器將第一個dom渲染到屏幕的時間,也就是一般所說的白屏時間
  • 文檔加載時間DCL(domContentLoaded)
  • 首次有意義的繪製FMP(frist meaningful paint) ,就是說主要內容出如今頁面上的時間,用戶但願看到的主要內容出如今屏幕上的時間

在前端渲染領域,主要有如下幾種方式可供選擇:git

    CSR   預渲染     SSR 同構
優勢
  • 不依賴數據
  • 局部刷新,無需每次都進行完整頁面請求
  • FP 時間最快
  • 客戶端用戶體驗好
  • 內存數據共享
  • 懶加載
  • 富交互,可以使用JS實現各類炫酷效果
  • 節約服務器成本
  • 不依賴數據
  • FCP 時間比 CSR 快
  • 客戶端用戶體驗好
  • 內存數據共享
  • SEO 友好
  • 首屏性能高,FMP 比 CSR 和預渲染快
  • 兼容性問題較優秀
  • SEO 友好
  • 首屏性能高,FMP 比 CSR 和預渲染快
  • 客戶端用戶體驗好
  • 內存數據共享
  • 客戶端與服務端代碼公用,開發效率高
缺點
  • SEO 不友好
  • FCP 、FMP 慢
  • 首屏白屏問題
  • SEO 不友好
  • FMP 慢
  • 開發成本高(構建)
  • 客戶端數據共享成本高
  • 模板維護成本高
  • Node 容易造成性能瓶頸
  • 實施難度大

 由上表格可得知,各類渲染模式優缺點各異,看起來並無那麼完美的解決方案,咱們只能依據自身產品需求,選擇最適合咱們的渲染方案。github

應用場景 

一、一個優秀的產品 ,離不開SEO,它包含了不少方面的知識,通常公司都會配置專門的SEO職位, 畢竟有太多事情要作了。而咱們做爲一個有追(破 )求(不)完(得)美(已)的前端,在開發的時候,須要作好頁面結構科學、標籤語義化、站點路由精簡、提供必要的爬蟲信息(如 Robot.txt,Sitemap,標籤 role,ref 等等);然而在現實中CSR大行其道,SPA站點更是數不勝數,用戶體驗獲得了質的提高,然而,然而,爲了兼顧SEO,開發者不得不爲爬蟲提供專門的shadowsite,或者本身頂起下載本身的頁面提供給爬蟲,或者使用SSR,刀耕火種的年代。。。web

二、首屏渲染速度

目前先後端的分離的前端項目須要先加載靜態資源,再異步獲取數據,最後渲染頁面,在這個過程當中的前兩部頁面都是沒有數據的,影響了首屏的渲染速度,也就影響了用戶的體驗。 目前對於首屏渲染速度的提高有許多方案,在ssr以外還有龍骨,墓碑,數據直出。相比於這些方案ssr方案實現是最複雜的,但效果也是最好的。

三、方案選擇

vue 中作同構,有兩種,一種是基於官方Vue SSR指南推薦的SSR,一種是vueJS通用應用框架 NUXT.

官網方案,能夠更直接的控制應用程序,更深刻底層,也比較靈活。NUXT,你懂得,就是那種你拿來即用的效果,提供了部分額外功能,e.g. 靜態站點,可用於快速實現Vue SSR

SSR和預渲染的使用場景仍是有比較明顯的區別的,預渲染的使用場景更多的是咱們所說的靜態頁面的形式。而SSR適用於大型的。頁面數據處理較多且較爲複雜、與服務端有數據交互的功能性網站,一個明顯的使用場景就是電商網站

今天爲你們介紹的這個,是部分同構方案,開始。。。

使用簡介

   

從官網借來的圖可知,SSR有兩個入口文件,entry-client和entry-server兩個文件,都包含了應用代碼,webpack會根據這兩個入口文件分別打包成給服務端用的,Server Bundle及客戶端使用的Client Bundle ,當服務器接收到了來自客戶端的請求以後,會建立一個渲染器 bundleRenderer,這個 bundleRenderer 會讀取上面生成的 server bundle 文件,而且執行它的代碼, 而後發送一個生成好的 html 到瀏覽器,等到客戶端加載了 client bundle 以後,會和服務端生成的DOM 進行 Hydration (判斷這個 DOM 和本身即將生成的 DOM 是否相同,若是相同就將客戶端的Vue實例掛載到這個 DOM 上, 不然會提示警告)。

   而在vue服務器端渲染時,就不能只是使用web-dev-server和web-hot-middle了,由於咱們須要添加服務器渲染的node代碼邏輯,這樣,咱們能夠本身開一個node服務器,使用webpack-dev-middle中間件進行打包、使用webpack-hot-middle中間件進行熱更新,並添加服務器端渲染邏輯,即node端經過引入vue-serverer-renderer插件來渲染服務器端打包的bundle文件到客戶端。

如何用?

因此在項目開發期間,其實咱們是運行着兩個node服務,一個作代理服務,一個提供ssr和其餘的一些服務。

 一、client-server的做用:

  • 客戶端打包bundle,而後提供給瀏覽器進行混合靜態標記,在默認狀況下,能夠在瀏覽器端輸出vue組件,進而生成、操做DOM,然而,服務端渲染時將同一個組件渲染成瀏覽器端的Html字符串,可是這些個只是字符串,而非應用程序,好比沒有CSS等;該html字符串而後發送給瀏覽器,而後結合客戶端打包,最後將靜態標記‘混合’爲客戶端上,生成徹底交互的應用程序
  • 數據同步的問題,在掛在到客戶端應用程序以前,須要獲取到與服務端應用程序徹底相同的數據,即爲window.__INITIAL_STATE__,而後經過store.replaceState(window.__INITIAL_STATE__),這樣,就會替代本地store中的state,不然,客戶端會由於使用與服務器端不一樣的狀態,致使混合失敗。
  • 服務器端打包代碼是爲了提供html靜態頁面,客戶端代碼打包時爲了後期的交互與數據處理,以及服務端打包失敗的的異常處理

二、entry-server的做用:

  • 前端請求過來的時候,根據getMatchedComponents方法 服務端匹配對應的URL,而後會以此尋找對應的組件或者靜態資源
  • 該配置主要用於生成 bundle   vue-ssr-server-bundle.json ,生成以後,傳遞給createBundleRenderer ,這裏會檢查組件是否有 asyncData 方法,而後下一步
  • pre-fetch,服務器端的數據預獲取:若是組件須要預獲取數據,咱們就調用本身約定好的asyncData 方法,獲取到數據,並存儲到服務端的store中,而後將解析完成的狀態附加給上下文,而且 `template` 選項用於 renderer 時,狀態將自動序列化爲 `window.__INITIAL_STATE__`,並注入 HTML。下面是一個頁面簡單請求的例子
  •  

  • 異常處理,若是在預獲取數據過程當中出錯了,咱們給一個標識  serverError,並注入HTML中,而後瀏覽器端能夠根據此狀態進行從新渲染,或者異常展現均可以。

二、因爲在node環境中,沒法用webpack進行CSS文件處理,因此在配置文件中咱們須要對服務端渲染時,進行CSS白名單處理 

1 externals: TARGET_NODE? nodeExternals({whitelist: [/\.css$/] }):undefined

三、咱們從打包出來的代碼中能夠看到,是vue-ssr-server-bundle.json,而很是規的JS文件。這是由於node環境下,每次打包成js文件,你得考慮node的熱部署,而且node環境也不支持sourcemap,因此引入了  vue-server-renderer 組件,這種方式相似於常規的render,支持熱部署,支持source map ,在配置文件中,咱們配置了該組件下的服務端渲染插件,可使得服務端渲染輸出的是json文件。

四、

在CSR模式下,咱們開發過程當中會進行熱部署,這樣會大大提升工做效率。 

 1 let bundle
 2 serverCompiler.watch({}, (err, stats) => {
 3   if (err) {
 4     throw err
 5   }
 6   stats = stats.toJson()
 7   stats.errors.forEach(error => console.error(error))
 8   stats.warnings.forEach(warn => console.warn(warn))
 9   const bundlePath = path.join(
10     webpackConfig.output.path,
11     'vue-ssr-server-bundle.json'
12   )
13   bundle = JSON.parse(mfs.readFileSync(bundlePath, 'utf-8'))
14   console.log('new bundle generated')
15 })

 注意事項

  1.  代碼執行環境不一樣引發的bug
    1. 服務端是沒有window,document對象的,storage的實現類也甭想了。。。location更不用說了。。若是加了這些個對象的引用和操做,在服務器端會引發報錯,node若報錯,可想而知。。
    2. 生命週期的鉤子函數  beforeCreate和created會在SSR過程當中調用,可是也同時會在客戶端執行。。。寫代碼前最好判斷一下你的執行環境。
  2.   接口代理問題:能夠採用DevServer進行代理轉發請求,或者用axios也能夠,否則存在跨域問題
  3. 服務端渲染 的時候必須路由必須使用history模式,處理好請求不在的回調
  4. cookie穿透:由於http請求是先到SSR服務器,再有SSR服務器去後端服務器器扭曲想要的數據接口,客戶端請求的時候是帶着cookie數據的,而SSR服務器請求後端接口的時候,卻沒有相應的cookie數據,所以在SSR服務器進行接口請求的時候,咱們須要手動拿到客戶端的cookie傳給後端服務器。咱們使用是axios,就能夠手動設置axios請求的headers字段,達到cookie穿透的目的。
  5. 數據泄露:由於使用了vuex,若是不使用惰性加載,容易形成數據泄露的狀況發生,任何須要登陸獲取數據的狀況,建議在客戶端進行。

異常處理

  一、服務器預獲取數據的異常處理,參考上面的entry-server中所說,讓客戶端主動獲取數據,再次嘗試渲染

  二、在服務端數據預獲取的生命週期結束後的渲染頁面過程當中出現的異常,包括各類操做數據的語法錯誤等,如對undefined取屬性。編寫過程要注意全局環境下的代碼,是否試用,作環境判斷。

緩存策略 

雖然 Vue 的服務器端渲染(SSR)至關快速,可是因爲建立組件實例和虛擬 DOM 節點的開銷,沒法與純基於字符串拼接(pure string-based)的模板的性能至關。在 SSR 性能相當重要的狀況下,明智地利用緩存策略,能夠極大改善響應時間並減小服務器負載,這是一個取捨的過程。vue服務區緩存分爲頁面緩存、組建緩存和接口緩存;

 頁面緩存

將渲染完成的頁面緩存到內存中,同時設置最大緩存數量和緩存時間。 優點:大幅度提升頁面的訪問速度 代價:增長服務器內存的使用

 1 const microCache = LRU({
 2   max: 100, // 最大緩存的數目
 3   maxAge: 1000 // 重要提示:條目在 1 秒後過時。
 4 })
 5 const isCacheable = req => {
 6   // 判斷是否須要頁面緩存
 7   if (req.url && req.url === '/') {
 8     return req.url
 9   } else {
10     return false
11   }
12 }
13 const handleRequest = async (ctx, next) => {
14   const req = ctx.req
15   const res = ctx.res
16   const cacheable = isCacheable(req)
17   if (cacheable) {
18     const hit = microCache.get(res.url)
19     if (hit) {
20       return res.end(hit)
21     }
22   }
23 ...
24 ...
25 }

 

組件緩存

使用較少,本次並未涉及

接口緩存

將通用的接口緩存到內存,減小服務端接口請求的時間

 get (url, params = {}) {
        // url = baseUrl + '/sa' + url
        url = baseUrl + url
        const key = md5(url + JSON.stringify(params))
        // 判斷是否有緩存,直接返回緩存結果
        if (params.cache && microCache.get(key)) {
          return Promise.resolve(microCache.get(key))
        }
        let Cookie = ''
        if (params.req && params.req.headers && params.req.headers.cookie) {
          Cookie = params.req.headers.cookie
        }
        return new Promise((resolve, reject) => {
          axios({
            url,
            params,
            headers: {
              'X-Requested-With': 'XMLHttpRequest',
              'Cookie': Cookie
            },
            method: 'get'
          }).then(res => {
            // 判斷是否須要緩存 若是須要緩存緩存數據
            if (params.cache && microCache) {
              microCache.set(key, res.data)
            }
            resolve(res.data)
          }).catch(error => {
            reject(error)
          })
        })
      },

 

  

結論

咱們是否真正的須要作同構渲染,這取決因素有不少,主要有,SEO,首屏時間比較重要的產品可考慮。

運用該技術並無想象中的輕鬆,對前端開發有必定的要求,編寫過程要謹慎,涉及構建設置和部署的更多要求,還須要更多的服務器端負載,總之,咱們可能須要的支持會更多一點。

代碼呈現

 Demo已上傳至github,以供參考,如若對您有幫助,給個小星星仍是很開心的,傳送門

相關文章
相關標籤/搜索