整合營銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          HTML的兩種渲染方法

          eb 服務(wù)存在兩種 HTML 渲染方法。

          最早的HTML(web 1.0時(shí)代),都是服務(wù)器端渲染的,瀏覽器發(fā)送請求,服務(wù)器端將整個(gè)html作為一個(gè)完整文檔發(fā)送給瀏覽器。最早響應(yīng)瀏覽器請求的被稱為CGI .

          CGI

          Java語言進(jìn)入web 開發(fā)領(lǐng)域后,首先出現(xiàn)的技術(shù)是 servlet,這個(gè)技術(shù)模仿的是CGI.也是在服務(wù)器端渲染好整個(gè)HTML文檔,然后反饋給瀏覽器。

          Servlet能夠很好地組織業(yè)務(wù)邏輯代碼,但是在Java源文件中通過字符串拼接的方式生成動(dòng)態(tài)HTML內(nèi)容會(huì)導(dǎo)致代碼維護(hù)困難、可讀性差。于是產(chǎn)生了JSP技術(shù),JSP在靜態(tài)HTML內(nèi)容中嵌入Java代碼,Java代碼被動(dòng)態(tài)執(zhí)行后生成HTML內(nèi)容,類似的還有ASP,PHP等技術(shù),這些技術(shù)本質(zhì)上都是服務(wù)端渲染好整個(gè)HTML文檔,都屬于服務(wù)器端渲染。

          web2.0時(shí)代 最大的思想革命本質(zhì)不是前后端分離,而是把網(wǎng)頁當(dāng)作獨(dú)立的應(yīng)用程序(app)。前后端分離只是實(shí)現(xiàn)這一新架構(gòu)的必然結(jié)果。web 2.0 時(shí)代最重要的就是ajax技術(shù)。

          使用ajax技術(shù)后,HTTP GET拿到的不是渲染后的網(wǎng)頁,而是一個(gè)由html和Javascript組成的應(yīng)用, 這個(gè)應(yīng)用以瀏覽器為虛擬機(jī)。裝載和顯示數(shù)據(jù)是app啟動(dòng)之后的運(yùn)行邏輯。傳統(tǒng)上應(yīng)用叫什么?叫Client,也就是前端。于是前后端就這么分離了,瀏覽器變成了應(yīng)用的運(yùn)行環(huán)境,后端蛻化成了單純的業(yè)務(wù)邏輯和數(shù)據(jù)接口。最典型的ajax 應(yīng)用就是gmail,gmail實(shí)質(zhì)上就是把過去桌面端的email 應(yīng)用搬到了瀏覽器中。ajax這種技術(shù)也就是客戶端渲染。

          概在昨年下半年,我利用同構(gòu)渲染技術(shù),把公司中一個(gè)需要7、8秒才能打開的vue3項(xiàng)目成功優(yōu)化至秒開(當(dāng)然除了同構(gòu)之外也配合了一些其他手段),由于那段時(shí)間vue3推出不久,很多框架這部分功能還沒有跟上,我便試著用vue和vite本身提供的api來完成同構(gòu),最終取得了令人滿意的效果,自己在這個(gè)過程中也獲益匪淺。

          如今各大框架的功能已經(jīng)完善,如果你現(xiàn)在想做同構(gòu)渲染,我推薦直接使用next.js(react)或nuxt.js(vue)來進(jìn)行開發(fā),而不是像我一樣手動(dòng)進(jìn)行實(shí)現(xiàn)。本文主要是對于同構(gòu)原理的描述,不涉及框架的使用。

          為了讓小白也能看懂,文章會(huì)包含很多特別基礎(chǔ)的理論描述,如果覺得沒必要了解,你可以通過標(biāo)題跳轉(zhuǎn)到自己感興趣的部分。文章中的代碼主要以vue為例,但是原理不局限于任何框架。

          點(diǎn)擊這里查看完整代碼和PPT

          1. 什么是同構(gòu)渲染?為什么使用它?

          1.1 什么是渲染?

          以現(xiàn)在前端流行的react和vue框架為例。react中的jsx和vue里面的模板,都是是無法直接在瀏覽器運(yùn)行的。將它們轉(zhuǎn)換成可在瀏覽器中運(yùn)行的html,這個(gè)過程被稱為渲染。

          1.2 什么是客戶端渲染(client-side-render, 以下簡稱csr)

          CSR是現(xiàn)在前端開發(fā)者最熟悉的渲染方式。利用vue-cli或create-react-app創(chuàng)建一個(gè)應(yīng)用,不作任何額外配置直接打包的出來代碼就是CSR。

          你可以用如下的方法辨別一個(gè)web頁面是否是CSR:打開chrome控制臺(tái) - 網(wǎng)絡(luò)面板,查看第一條請求,就能看到當(dāng)前頁面向服務(wù)器請求的html資源;如果是CSR(如下圖所示),這個(gè)html的body中是沒有實(shí)際內(nèi)容的。

          那么頁面內(nèi)容是如何渲染出來的呢?仔細(xì)看上面的html,會(huì)發(fā)現(xiàn)存在一個(gè)script標(biāo)簽,打包器正是把整個(gè)應(yīng)用都打包進(jìn)了這個(gè)js文件里面。

          當(dāng)瀏覽器請求頁面的時(shí)候,服務(wù)器先會(huì)返回一個(gè)空的html和打包好的js代碼;等到j(luò)s代碼下載完畢,瀏覽器再執(zhí)行js代碼,頁面就被渲染出來了。因?yàn)轫撁娴匿秩臼窃跒g覽器中而非服務(wù)器端進(jìn)行的,所以被稱為客戶端渲染。

          CSR的優(yōu)劣

          CSR會(huì)把整個(gè)網(wǎng)站打包進(jìn)js里,當(dāng)js下載完畢后,相當(dāng)于網(wǎng)站的頁面資源都被下載好了。這樣在跳轉(zhuǎn)新頁面的時(shí)候,不需要向服務(wù)器再次請求資源(js會(huì)直接操作dom進(jìn)行頁面渲染),從而讓整個(gè)網(wǎng)站的使用體驗(yàn)上更加流暢。

          但是這種做法也帶來了一些問題:在請求第一個(gè)頁面的時(shí)候需要下載js,而下載js直至頁面渲染出來這段時(shí)間,頁面會(huì)因?yàn)闆]有任何內(nèi)容而出現(xiàn)白屏。在js體積較大或者渲染過程較為復(fù)雜的情況下,白屏問題會(huì)非常明顯。

          另外,由于使用了CSR的網(wǎng)站,會(huì)先下載一個(gè)空的html,然后才通過js進(jìn)行渲染;這個(gè)空的html會(huì)導(dǎo)致某些搜索引擎無法通過爬蟲正確獲取網(wǎng)站信息,從而影響網(wǎng)站的搜索引擎排名(一般稱之為搜索引擎優(yōu)化Search Engine Optimization,簡稱SEO)。

          總而言之,客戶端渲染就是通過犧牲首屏加載速度和SEO,來獲取用戶體驗(yàn)的一種技術(shù)。

          1.3 什么是服務(wù)器端渲染(server-side-render, 以下簡稱SSR)

          理解了CSR,SSR也很好理解了,其實(shí)就是把渲染過程放在了在服務(wù)器端。以早年比較流行的java服務(wù)器端渲染技術(shù)jsp為例,會(huì)先寫一個(gè)html模板,并用特殊的語法<%...%>標(biāo)記動(dòng)態(tài)內(nèi)容,里面可以寫一些java程序。

          渲染的時(shí)候,jsp會(huì)通過字符串替換的方式,把<%...%>替換為程序執(zhí)行的結(jié)果。最后服務(wù)器將替換完畢的html以字符串的形式發(fā)送給用戶即可。

          同時(shí)我們還可以寫很多個(gè)JSP,根據(jù)用戶的http請求路徑返回相應(yīng)的文件,這樣就完成了一個(gè)網(wǎng)站的開發(fā)。

           // jsp示例
           <body>
            <hr>
            <hr>
            <h2>java腳本1</h2>
            <%
                  Object obj = new Object();
                  System.out.println(obj);
                  out.write(obj.toString()); // 這一行表示把結(jié)果輸出到最終的html中
            %>
            <hr>
            <hr>
            <%
              out.write(obj.toString());
            %>
            </body>
          


          像jsp這類SSR技術(shù),優(yōu)劣勢和客戶端渲染正好相反:因?yàn)閔tml在服務(wù)器端就已經(jīng)渲染好了,所以不存在客戶端的白屏和seo問題;相對應(yīng)地,每次跳轉(zhuǎn)頁面都要向服務(wù)器重新請求,意味著用戶每次切換頁面都要等待一小段時(shí)間,所以用戶體驗(yàn)方面則不如客戶端。

          還有一點(diǎn)顯而易見的問題,就是SSR相比CSR會(huì)占用較多的服務(wù)器端資源。

          總而言之,服務(wù)器端渲染擁有良好的首屏性能和SEO,但用戶體驗(yàn)方面較差。且會(huì)占用較多的服務(wù)器端資源。

          1.4 什么是同構(gòu)(Isomorphic)

          可以看到,CSR和SSR的優(yōu)劣勢是互補(bǔ)的,所以只要把它們二者結(jié)合起來,就能實(shí)現(xiàn)理想的渲染方法,也就是同構(gòu)渲染。

          同構(gòu)的理念十分簡單,最開始的步驟和SSR相同,將生成的html字符串返回給用戶即可;但同時(shí)我們可以將CSR生成的JS也一并發(fā)送給用戶;這樣用戶在接收到SSR生成的html后,頁面還會(huì)再執(zhí)行一次CSR的流程。

          這導(dǎo)致用戶只有請求的第一個(gè)頁面是在服務(wù)器端渲染的,其他頁面則都是在客戶端進(jìn)行的。這樣我們就擁有了一個(gè)同時(shí)兼顧首屏、SEO和用戶體驗(yàn)的網(wǎng)站。

          當(dāng)然這只是最簡單的概念描述,實(shí)際操作起來仍然有不少難點(diǎn)。我將在后面的內(nèi)容一一指出。

          1.5 CSR、SSR、同構(gòu)渲染對比

          以下摘自《vue.js設(shè)計(jì)與實(shí)現(xiàn)》


          CSR

          SSR

          同構(gòu)

          SEO

          不友好

          友好

          友好

          白屏問題

          占用服務(wù)器資源

          用戶體驗(yàn)

          2. 一個(gè)最簡單的同構(gòu)案例

          查看完整的代碼可以點(diǎn)擊這里。

          2.1 服務(wù)器端渲染html字符串

          前面說過,同構(gòu)渲染可以看作把SSR和CSR進(jìn)行結(jié)合。單獨(dú)完成SSR和CSR都很簡單:CSR就不用說了;SSR的話,vue和react都提供了renderToString函數(shù),只要將組件傳入這個(gè)函數(shù),可以直接將組件渲染成html字符串。

          還有一點(diǎn)需要注意的是,在客戶端渲染里我們會(huì)使用createApp來創(chuàng)建一個(gè)vue應(yīng)用實(shí)例,但在同構(gòu)渲染中則需要替換成createSSRApp。如果仍然使用原本的createApp,會(huì)導(dǎo)致首屏頁面先在服務(wù)器端渲染一次,瀏覽器端又重復(fù)渲染一次。

          而使用了createSSRApp,vue就會(huì)在瀏覽器端渲染前先進(jìn)行一次檢查,如果結(jié)果和服務(wù)器端渲染的結(jié)果一致,就會(huì)停止首屏的客戶端渲染過程,從而避免了重復(fù)渲染的問題。

          代碼如下:

          import { renderToString } from 'vue/server-renderer'
          import { createSSRApp } from 'vue'
          
          // 一個(gè)計(jì)數(shù)的vue組件
          function createApp() {
            // 通過createSSRApp創(chuàng)建一個(gè)vue實(shí)例
            return createSSRApp({
              data: () => ({ count: 1 }),
              template: `<button @click="count++">{{ count }}</button>`,
            });
          }
          
          const app = createApp();
          
          // 通過renderToString將vue實(shí)例渲染成字符串
          renderToString(app).then((html) => {
            // 將字符串插入到html模板中
            const htmlStr = `
              <!DOCTYPE html>
              <html>
                <head>
                  <title>Vue SSR Example</title>
                </head>
                <body>
                  <div id="app">${html}</div>
                </body>
              </html>
            `;
            console.log(htmlStr);
          });
          


          將上述代碼拷貝進(jìn)任意.js文件,然后執(zhí)行node xxx.js,即可看到控制臺(tái)打印出渲染好的字符串,如下:

          2.2 通過服務(wù)器發(fā)送html字符串

          為了簡便,這里使用比較流行的express作為服務(wù)器。代碼很簡單,直接看注釋就能理解。

          import express from 'express'
          import { renderToString } from 'vue/server-renderer'
          import { createSSRApp } from 'vue'
          
          // 一個(gè)計(jì)數(shù)的vue組件
          function createApp() {
            return createSSRApp({
              data: () => ({ count: 1 }),
              template: `<button @click="count++">{{ count }}</button>`,
            });
          }
          
          // 創(chuàng)建一個(gè)express實(shí)例
          const server = express();
          
          // 通過express.get方法創(chuàng)建一個(gè)路由, 作用是當(dāng)瀏覽器訪問'/'時(shí), 對該請求進(jìn)行處理
          server.get('/', (req, res) => {
          
            // 通過createSSRApp創(chuàng)建一個(gè)vue實(shí)例
            const app = createApp();
            
            // 通過renderToString將vue實(shí)例渲染成字符串
            renderToString(app).then((html) => {
              // 將字符串插入到html模板中
              const htmlStr = `
                <!DOCTYPE html>
                <html>
                  <head>
                    <title>Vue SSR Example</title>
                  </head>
                  <body>
                    <div id="app">${html}</div>
                  </body>
                </html>
              `;
              // 通過res.send將字符串返回給瀏覽器
              res.send(htmlStr);
            });
          })
          
          // 監(jiān)聽3000端口
          server.listen(3000, () => {
            console.log('ready http://localhost:3000')
          })
          


          同樣在控制臺(tái)輸入node xxx.js,即可啟動(dòng)服務(wù)器,然后在瀏覽器訪問http://localhost:3000/ ,就能訪問到頁面了。

          2.3 激活客戶端渲染

          如果你訪問過上面的地址,就會(huì)發(fā)現(xiàn)頁面上的按鈕是點(diǎn)不動(dòng)的。這是因?yàn)橥ㄟ^renderToString渲染出來的頁面是完全靜態(tài)的,這時(shí)候就要進(jìn)行客戶端激活。

          激活的方法其實(shí)就是執(zhí)行一遍客戶端渲染,在vue里面就是執(zhí)行app.mount。我們可以創(chuàng)建一個(gè)js,在里面寫入客戶端激活的代碼,然后通過script標(biāo)簽把這個(gè)文件插入到html模板中,這樣瀏覽器就會(huì)請求這個(gè)js文件了。

          如下所示,首先寫一段客戶端激活的代碼,放到名為client-entry.js的文件里:

          import { createSSRApp } from 'vue'
          
          // 通過createSSRApp創(chuàng)建一個(gè)vue實(shí)例
          function createApp() {
            return createSSRApp({
              data: () => ({ count: 1 }),
              template: `<button @click="count++">{{ count }}</button>`,
            });
          }
          
          createApp().mount('#app');
          


          可以看到,這里的createApp函數(shù)和服務(wù)器端的counter組件是完全相同的(在實(shí)際開發(fā)中,createApp代表的就是你的整個(gè)應(yīng)用),所以客戶端激活實(shí)際上就是把客戶端渲染再執(zhí)行一遍,唯一區(qū)別就是要使用createSSRApp這個(gè)api防止重復(fù)渲染。

          另外,要使用vue激活,我們還需要在客戶端下載vue。因?yàn)槲覀兊拇a沒有經(jīng)過打包器轉(zhuǎn)換,所以沒法在瀏覽器中直接使用import { createSSRApp } from 'vue'這樣的語法。為了方便,這里借用了Import Map功能,這樣就支持import直接使用了。如果想進(jìn)一步了解可以自行搜索Import Map關(guān)鍵字。

          改造后的如下html模板如下:

          const htmlStr = `
            <!DOCTYPE html>
            <html>
              <head>
                <title>Vue SSR Example</title>
                // 使用Import Map
                <script type="importmap">
                {
                  "imports": {
                    "vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
                  }
                }
                </script>
                // 將client-entry.js文件路徑寫入script
                <script type="module" src="/client-entry.js"></script>
              </head>
              <body>
                <div id="app">${html}</div>
              </body>
            </html>
          `;
          


          這樣我們的按鈕就可以點(diǎn)擊了,而且查看控制臺(tái),請求的html資源也是有內(nèi)容的,不再是csr那種空白的html了。

          查看完整的代碼可以點(diǎn)擊這里。

          3. 實(shí)現(xiàn)脫水(Dehydrate)和注水(Hydrate)

          同構(gòu)應(yīng)用還有一個(gè)比較重要的點(diǎn),就是如何實(shí)現(xiàn)服務(wù)器端的數(shù)據(jù)的預(yù)取,并讓其隨著html一起傳遞到瀏覽器端。

          例如我們有一個(gè)列表頁,列表數(shù)據(jù)是從其他服務(wù)器獲取的;為了讓用戶第一時(shí)間就看到頁面內(nèi)容,最好的方法當(dāng)然是在服務(wù)器就拿到數(shù)據(jù),然后隨著html一起傳遞給瀏覽器。瀏覽器拿到html和傳過來的數(shù)據(jù),直接對頁面進(jìn)行初始化,而不需要再在客戶端請求這個(gè)接口(除非服務(wù)器端因?yàn)槟承┰颢@取數(shù)據(jù)失敗)。

          為了實(shí)現(xiàn)這個(gè)功能,整個(gè)過程分為兩部分:

          1. 服務(wù)器端獲取到數(shù)據(jù)后,把數(shù)據(jù)隨著html一起傳給客戶端的過程,一般叫做脫水(Dehydrate)
          2. 客戶端拿到html和數(shù)據(jù),利用這個(gè)數(shù)據(jù)來初始化組件的過程叫做注水(Hydrate)

          注水其實(shí)就是前面提到過的客戶端激活,區(qū)別只是前面的沒有數(shù)據(jù),而這次我們會(huì)試著加上數(shù)據(jù)。國內(nèi)也有翻譯成"水合"的,現(xiàn)在你應(yīng)該知道了,注水、客戶端激活、水合還有Hydrate其實(shí)都是一碼事。

          查看完整的代碼可以點(diǎn)擊這里。

          3.1 實(shí)現(xiàn)服務(wù)器端脫水

          要在服務(wù)器端直接請求一個(gè)接口當(dāng)然很簡單,但是為了保持最基本的前后端分離,我們最好的寫法還是將接口請求寫在組件中。

          為了讓服務(wù)器獲取到我們要請求的接口,我們可以在vue組件中掛載一個(gè)自定義函數(shù),然后在服務(wù)器端調(diào)用這個(gè)函數(shù)即可(需要注意的是,服務(wù)器環(huán)境不能直接使用fetch,應(yīng)該用axios或者node-fetch替代)。如下:

          // 組件中的代碼
          import { createSSRApp } from 'vue'
          function createApp() {
            return createSSRApp({
              data: () => ({ count: 1 }),
              template: `<button @click="count++">{{ count }}</button>`,
              // 自定義一個(gè)名為asyncData的函數(shù)
              asyncData: async () => { 
                  // 在處理遠(yuǎn)程數(shù)據(jù)并return出去
                  const data = await getSomeData()
                  return data; 
              }
            });
          }
          
          // 服務(wù)器端的代碼
          const app = createApp();
          // 保存初始化數(shù)據(jù)
          let initData = null;
          // 判斷是否有我們自定義的asyncData方法,如果有就用該函數(shù)初始化數(shù)據(jù)
          if (app._component.asyncData) {
              initData = await app._component.asyncData();
          }
          


          拿到數(shù)據(jù)后該如何傳遞到瀏覽器呢?其實(shí)有一個(gè)很簡單的方法:我們可以把數(shù)據(jù)格式化成字符串,然后用如下的方式,直接將這個(gè)字符串放到html模板的一個(gè)script標(biāo)簽中:

          const htmlStr = `
            <!DOCTYPE html>
            <html>
              <head>
                ...
                // 將數(shù)據(jù)格式化成json字符串,放到script標(biāo)簽中
                <script>window.__INITIAL_DATA__ = ${JSON.stringify(initData)}</script>
              </head>
              ...
            </html>
          `;
          


          當(dāng)html被傳到瀏覽器端的時(shí)候,這個(gè)script標(biāo)簽就會(huì)被瀏覽器執(zhí)行,于是我們的數(shù)據(jù)就被放到了window.__INITIAL_DATA__里面。此時(shí)客戶端就可以從這個(gè)對象里面拿到數(shù)據(jù)了。

          3.2實(shí)現(xiàn)客戶端注水

          實(shí)現(xiàn)了脫水,注水就很簡單了。我們先判斷window.__INITIAL_DATA__是否有值,如果有的話直接將其賦值給頁面state;否則就讓客戶對自己請求一次接口。代碼如下:

          function createApp() {
            return createSSRApp({
              data: () => ({ count: 1 }),
              template: `<button @click="count++">{{ count }}</button>`,
              // 自定義一個(gè)名為asyncData的函數(shù)
              asyncData: async () => { 
                  // 在處理遠(yuǎn)程數(shù)據(jù)并return出去
                  const data = await getSomeData()
                  return data; 
              },
              async mounted() {
                // 如果已經(jīng)有數(shù)據(jù)了,直接從window中獲取
                if (window.__INITIAL_DATA__) {
                  // 有服務(wù)端數(shù)據(jù)時(shí),使用服務(wù)端渲染時(shí)的數(shù)據(jù)
                  this.count = window.__INITIAL_DATA__;
                  window.__INITIAL_DATA__ = undefined;
                  return;
                } else {
                  // 如果沒有數(shù)據(jù),就請求數(shù)據(jù)
                  this.count = await getSomeData();
                }
              }
            });
          }
          


          這樣我們就實(shí)現(xiàn)了一套完整的注水和脫水流程。

          查看完整的代碼可以點(diǎn)擊這里。

          4. 同構(gòu)渲染要(坑)點(diǎn)

          服務(wù)器端和瀏覽器端環(huán)境不同,所以我們不能像寫csr代碼一樣寫同構(gòu)代碼。根據(jù)我的踩坑經(jīng)歷,寫同構(gòu)應(yīng)用需要尤其注意以下幾點(diǎn):

          4.1 避免狀態(tài)單例

          服務(wù)器端返回給客戶端的每個(gè)請求都應(yīng)該是全新的、獨(dú)立的應(yīng)用程序?qū)嵗虼瞬粦?yīng)當(dāng)有單例對象——也就是避免直接將對象或變量創(chuàng)建在全局作用域,否則它將在所有請求之間共享,在不同請求之間造成狀態(tài)污染。

          在客戶端中,vue/pinia/vue-router都是以單例的形式存在,為此可以用函數(shù)的形式將vue/pinia/vue-router等進(jìn)行初始化。也就是像上面的例子那樣,用一個(gè)函數(shù)進(jìn)行包裹,然后調(diào)用這個(gè)函數(shù)進(jìn)行應(yīng)用的初始化。

          4.2 避免訪問特定平臺(tái)api

          服務(wù)器端是node環(huán)境,而客戶端是瀏覽器環(huán)境,如果你在node端直接使用了像 window 、 document或者fetch(在node端應(yīng)該用axios或node-fetch),這種僅瀏覽器可用的全局變量或api,則會(huì)在 Node.js 中執(zhí)行時(shí)拋出錯(cuò)誤;反之,在瀏覽器使用了node端的api也是如此。

          需要注意的是,在vue組件中,服務(wù)器端渲染時(shí)只會(huì)執(zhí)行beforeCreate和created生命周期,在這兩個(gè)生命周期之外執(zhí)行瀏覽器api是安全的。所以推薦將操作dom或訪問window之類的瀏覽器行為,一并寫在onMounted生命周期中,這樣就能避免在node端訪問到瀏覽器api。

          如果要在這兩個(gè)生命周期中使用瀏覽器端api,可以利用相關(guān)打包工具提供的變量(如vite提供了import.meta.env.SSR),來避免服務(wù)器端調(diào)用相關(guān)代碼。

          尤其需要注意的是,一些組件庫可能也會(huì)因?yàn)榫帉懙臅r(shí)候沒有考慮到服務(wù)器端渲染的情況,導(dǎo)致渲染出錯(cuò)。這時(shí)候可以借助一些第三方組件,如nuxt中的ClientOnly,可以避免這些出錯(cuò)的組件在服務(wù)器端進(jìn)行渲染。

          4.3 避免在服務(wù)器端生命周期內(nèi)執(zhí)行全局副作用代碼

          vue服務(wù)器端渲染會(huì)執(zhí)行beforeCreate和created生命周期,應(yīng)該避免在這兩個(gè)生命周期里產(chǎn)生全局副作用的代碼。

          例如使用setInterval設(shè)置定時(shí)器。在純客戶端的代碼中,我們可以設(shè)置一個(gè)定時(shí)器,然后在 beforeDestroy 或 destroyed 生命周期時(shí)將其銷毀。但是,由于在 SSR 期間并不會(huì)調(diào)用銷毀鉤子函數(shù),所以 timer 將永遠(yuǎn)保留下來,最終造成服務(wù)器內(nèi)存溢出。

          5. 創(chuàng)建實(shí)際生產(chǎn)中的同構(gòu)應(yīng)用

          上面的例子是一個(gè)最基礎(chǔ)的同構(gòu)渲染,但距離一個(gè)能在開發(fā)中實(shí)際使用的框架還差得很遠(yuǎn)。如果把這些內(nèi)容都細(xì)細(xì)講完,我估摸文章要到三萬字了,實(shí)在太累,而且也很難讓新手程序員看得懂。所以這些難點(diǎn)我只講解一下關(guān)鍵點(diǎn),如果有興趣深究的可以下來自己研究。

          按照我踩坑的經(jīng)歷,至少還要解決下面幾個(gè)問題:

          1. 集成前端工具鏈,如vite、eslint、ts等
          2. 集成前端路由,如vue-router
          3. 集成全局狀態(tài)管理庫,如pinia
          4. 處理#app節(jié)點(diǎn)之外的元素。如vue的teleport,react的portal
          5. 處理預(yù)加載資源

          順帶一提,vue社區(qū)有一篇vue ssr指南也值得一看,雖然只有vue2版本的,但是仍然有很多值得學(xué)習(xí)的地方。

          4.1 集成前端工具鏈

          這部分內(nèi)容實(shí)在太多太雜,需要對打包工具有比較好的掌握才能理解。好在vite官方已經(jīng)有了一篇完善的教程,而且提供了完整的代碼示例,想深入了解的可以點(diǎn)進(jìn)去看看。

          4.2 集成前端路由

          前端路由都提供了相關(guān)的api來輔助服務(wù)器端進(jìn)行處理。如vue-router進(jìn)行服務(wù)器端處理的流程如下:

          1. 使用createMemoryHistory創(chuàng)建路由。
          2. 在服務(wù)器端獲取用戶請求的路徑,將路徑傳入router.push函數(shù),這樣router就會(huì)處理該路徑對應(yīng)的頁面。
          3. router在處理頁面的時(shí)候,可能會(huì)碰到一些異步代碼,所以vue-router提供了router.isReady這個(gè)異步函數(shù)。await這個(gè)函數(shù)后,再渲染整個(gè)應(yīng)用,獲取的就是當(dāng)前用戶請求的頁面了。

          4.3 集成全局狀態(tài)管理庫

          官方文檔一般就有詳細(xì)教程,如pinia官網(wǎng)就有教你如何進(jìn)行服務(wù)器端渲染。實(shí)際上全局狀態(tài)管理庫的處理就是脫水和注水,所以這里不做詳細(xì)解釋了。

          4.4 處理#app節(jié)點(diǎn)之外的元素

          頁面內(nèi)容一般會(huì)渲染到id為app的節(jié)點(diǎn)下,但像vue中的teleport和react的portal獨(dú)立于app節(jié)點(diǎn)外,因此需要單獨(dú)處理。

          這里建議把所有的根節(jié)點(diǎn)之外的元素統(tǒng)一設(shè)置到一個(gè)節(jié)點(diǎn)下面,如teleport可以通過設(shè)置to屬性來指定掛載的節(jié)點(diǎn);同時(shí)vue也提供了方法來獲取所有的teleport。拿到teleport的信息后,即可通過字符串拼接的方式,將它們一并放到html模板中的目標(biāo)節(jié)點(diǎn)下面了。

          4.5 處理預(yù)加載資源

          使用打包器可以生成manifest,它的作用是將打包后的模塊 ID 與它們關(guān)聯(lián)的 chunk 和資源文件進(jìn)行映射(簡單理解就是通過它你可以知道js、圖片等頁面資源的位置在哪兒)。依靠這個(gè)manifest獲取資源的路徑,然后創(chuàng)建link標(biāo)簽拼接到html模板中即可。

          詳情可查看這里。

          5. 服務(wù)器端優(yōu)化

          雖然我們寫好了服務(wù)端的代碼,但是這樣的代碼是十分脆弱的,無論性能還是可靠性都沒有保障,是沒法在實(shí)際生產(chǎn)中應(yīng)用的。為此我們需要對服務(wù)端代碼進(jìn)行一系列優(yōu)化。

          點(diǎn)擊這里查看完整代碼。

          5.1 服務(wù)器端測試

          壓力測試

          為了衡量服務(wù)器優(yōu)化的指標(biāo),我們可以借助一系列測試工具,apach bench、jmeter等。我使用的是apach bench,它可以模擬一系列并發(fā)請求,用來對服務(wù)器進(jìn)行壓力測試。

          apach bench可以通過執(zhí)行abs -n <請求總數(shù)> -c <并發(fā)數(shù)> <測試路徑>來進(jìn)行測試。例:abs -n 1000 -c 100 http://localhost:3000/,表示以100并發(fā)的形式發(fā)送1000個(gè)請求到localhost:3000。

          因?yàn)槲覀兊姆?wù)本身比較簡單,所以這里我以1000并發(fā)的形式發(fā)送了10000個(gè)請求,結(jié)果如下:

          可以看到Time taken for tests這一欄,總共花了6.6秒左右。

          Node調(diào)試工具

          除此之外,我們還可以用Chrome瀏覽器的"開發(fā)者工具"作為node服務(wù)器的調(diào)試工具。使用node調(diào)試工具不僅能方便地進(jìn)行調(diào)試,還可以清楚地看到諸如內(nèi)存使用情況等指標(biāo),對代碼進(jìn)行更精確地優(yōu)化。

          關(guān)于node調(diào)試工具的使用可以參考這篇文章。

          5.2 多進(jìn)程優(yōu)化

          node內(nèi)置了cluster模塊,可以快速方便地創(chuàng)建子進(jìn)程。如下:

          通過os模塊判斷當(dāng)前的cpu總數(shù),然后通過cluster.isMaster判斷當(dāng)前是否是主進(jìn)程,最后通過cluster.fork即可創(chuàng)建一個(gè)子進(jìn)程。

          在主進(jìn)程里,我們進(jìn)行一些創(chuàng)建、維護(hù)子進(jìn)程的工作,而在子進(jìn)程里我們則運(yùn)行真正的node服務(wù)。如下圖所示,我們啟動(dòng)多線程再進(jìn)行測試:

          可以看到速度提升到了3.7秒,明顯快了很多。

          5.3 內(nèi)存溢出處理

          通過process.memoryUsage();可以判斷當(dāng)前子進(jìn)程用掉的內(nèi)存,當(dāng)占用內(nèi)存大于某個(gè)數(shù)(如300M)的時(shí)候,我們便將這個(gè)子進(jìn)程關(guān)掉,防止內(nèi)存泄露。

          5.4 處理未捕獲異常

          在子進(jìn)程中,通過process.on('uncaughtException', err => {})可以獲取到該進(jìn)程中的未捕獲異常(如服務(wù)器端渲染時(shí)候發(fā)生的一些錯(cuò)誤)。當(dāng)捕獲到錯(cuò)誤后,我們可以對錯(cuò)誤進(jìn)行上報(bào)或?qū)懭肴罩尽?/p>

          也可以借助一些第三方監(jiān)控平臺(tái)如sentry來處理這類問題。sentry在node端的部署方法可以參考這里。

          5.5 心跳包檢測

          所謂心跳包檢測,就是主進(jìn)程每隔一段時(shí)間向子進(jìn)程發(fā)送一個(gè)信息,子進(jìn)程收到這個(gè)信息后,立即回應(yīng)給主進(jìn)程一個(gè)信息;如果主進(jìn)程在某次信息發(fā)送后,子進(jìn)程沒有回應(yīng),說明子進(jìn)程卡死了。這時(shí)候就需要?dú)⑺肋@個(gè)子進(jìn)程然后重新創(chuàng)建一個(gè)。

          所以心跳包檢測的作用主要是為了防止子進(jìn)程卡死。

          具體步驟如下:

          1. 主進(jìn)程通過woker.send方法可以向子進(jìn)程發(fā)送信息(woker為cluster創(chuàng)建的子進(jìn)程引用)
          2. 子進(jìn)程通過process.on('message', () => {})訂閱主進(jìn)程發(fā)送的信息,并在收到信息后通過process.send方法返回給主進(jìn)程信息
          3. 主進(jìn)程通過woker.on('message', () => {})訂閱子進(jìn)程發(fā)送的信息。如果累計(jì)一定次數(shù)沒有收到子進(jìn)程返回的信息,則關(guān)閉子進(jìn)程。

          主進(jìn)程代碼如下:

          子進(jìn)程代碼如下:

          5.6 子進(jìn)程自動(dòng)重建

          在上面的代碼里,如果子進(jìn)程因?yàn)槟撤N錯(cuò)誤(如內(nèi)存溢出)而被關(guān)閉的時(shí)候,我們需要重新創(chuàng)建一個(gè)子進(jìn)程,這樣就能保證線上服務(wù)能夠長時(shí)間運(yùn)行了。通過如下代碼即可監(jiān)聽子進(jìn)程關(guān)閉并重新創(chuàng)建子進(jìn)程。

          點(diǎn)擊這里查看完整代碼。

          完結(jié)

          文章到這里就結(jié)束了,如果有需要補(bǔ)充或者錯(cuò)誤的地方,歡迎在評(píng)論區(qū)指出。


          作者:monet
          鏈接:https://juejin.cn/post/7289661061984501819

          能大家在看到這個(gè)標(biāo)題的時(shí)候,會(huì)覺得,只不過又是一篇爛大街的 SSR 從零入門的教程而已。別急,往下看,相信你或多或少會(huì)有一些不一樣的收獲呢。

          在落地一種技術(shù)的時(shí)候,我們首先要想一想:

          1. 是否一定需要引入這種技術(shù)呢?他能解決什么問題,或者能帶來什么收益?
          2. 為什么要采用這種技術(shù)選型而不是其他的?
          3. 引入了這種技術(shù)后,會(huì)帶來什么問題嗎(比如額外的開發(fā)成本等)?

          上面三個(gè)問題思考清楚之后,才能真正地去落地。上面三個(gè)問題思考清楚之后,才能真正地去落地。而有贊教育接入服務(wù)端渲染,正是為了優(yōu)化 H5 頁面的首屏內(nèi)容到達(dá)時(shí)間,帶來更好的用戶體驗(yàn)(順便利于 SEO)。

          說了這么多,以下開始正文。

          一、后端模版引擎時(shí)代

          在較早時(shí)期,前后端的配合模式為:后端負(fù)責(zé)服務(wù)層、業(yè)務(wù)邏輯層和模版渲染層(表現(xiàn)層);前端只是實(shí)現(xiàn)頁面的交互邏輯以及發(fā)送 AJAX。比較典型的例子就是 JSP 或 FreeMarker 模板引擎負(fù)責(zé)渲染出 html 字符串模版,字符串模版里的 js 靜態(tài)資源才是真正前端負(fù)責(zé)的東西。

          而這種形式,就是天然的服務(wù)端渲染模式:用戶請求頁面 -> 請求發(fā)送到應(yīng)用服務(wù)器 -> 后端根據(jù)用戶和請求信息獲取底層服務(wù) -> 根據(jù)服務(wù)返回的數(shù)據(jù)進(jìn)行組裝,同時(shí) JSP 或 FreeMarker 模版引擎根據(jù)組裝的數(shù)據(jù)渲染為 html 字符串 -> 應(yīng)用服務(wù)器講 html 字符串返回給瀏覽器 -> 瀏覽器解析 html 字符串渲染 UI 及加載靜態(tài)資源 -> js 靜態(tài)資源加載完畢界面可交互。

          那么既然后端模版引擎時(shí)代帶來的效果就是我們想要的,那為啥還有以后讓前端發(fā)展服務(wù)端渲染呢?因?yàn)楹苊黠@,這種模式從開發(fā)角度來講還有挺多的問題,比如:

          1. 后端需要寫表現(xiàn)層的邏輯,但其實(shí)后端更應(yīng)該注重服務(wù)層(和部分業(yè)務(wù)邏輯層)。當(dāng)然,其實(shí)也可以讓前端寫 JSP 或 FreeMarker,但從體驗(yàn)上來說,肯定不如寫 JS 來的爽;
          2. 本地開發(fā)的時(shí)候,需要啟動(dòng)后端環(huán)境,比如 Tomcat,影響開發(fā)效率,對前端也不友好;
          3. 所賦予前端的能力太少,使得前端需要的一些功能只能由后端提供,比如路由控制;
          4. 前后端耦合。

          二、SPA 時(shí)代

          后來,誕生了 SPA(Single Page Application),解決了上面說的部分問題:

          1. 后端不需要關(guān)心表現(xiàn)層的邏輯,只需要注重服務(wù)層和業(yè)務(wù)邏輯層就可以了,暴露出相應(yīng)的接口供前端調(diào)用。這種模式也同時(shí)實(shí)現(xiàn)了前后端解耦。
          2. 本地開發(fā)的時(shí)候,前端只需要啟動(dòng)一個(gè)本地服務(wù),如:dev-server 就可以開始開發(fā)了。
          3. 賦予了前端更多的能力,比如前端的路由控制和鑒權(quán),比如通過 SPA + 路由懶加載的模式可以帶來更好的用戶體驗(yàn)。

          但同時(shí),也帶來了一些問題:

          1. 頁面的 DOM 完全由 js 來渲染,使得大部分搜索引擎無法爬取渲染后真實(shí)的 DOM,不利于 SEO。
          2. 頁面的首屏內(nèi)容到達(dá)時(shí)間強(qiáng)依賴于 js 靜態(tài)資源的加載(因?yàn)?DOM 的渲染由 js 來執(zhí)行),使得在網(wǎng)絡(luò)越差的情況下,白屏?xí)r間大幅上升。

          三、服務(wù)端渲染

          正因?yàn)?SPA 帶來的一些問題(尤其是首屏白屏的問題),接入服務(wù)端渲染顯得尤為必要。// 終于講到服務(wù)端渲染這個(gè)重點(diǎn)了。

          而正是 Node 的發(fā)展和基于 Virtual DOM 的前端框架的出現(xiàn),使得用 js 實(shí)現(xiàn)服務(wù)端渲染成為可能。因此在 SPA 的優(yōu)勢基礎(chǔ)上,我們順便解決了因?yàn)?SPA 引入的問題:

          1. 服務(wù)端渲染的首屏直出,使得輸出到瀏覽器的就是完備的 html 字符串模板,瀏覽器可以直接解析該字符串模版,因此首屏的內(nèi)容不再依賴 js 的渲染。
          2. 正是因?yàn)榉?wù)端渲染輸出到瀏覽器的是完備的 html 字符串,使得搜索引擎能抓取到真實(shí)的內(nèi)容,利于 SEO。
          3. 同時(shí),通過基于 Node 和前端 MVVM 框架結(jié)合的服務(wù)端渲染,有著比后端模版引擎的服務(wù)端渲染更明顯的優(yōu)勢:可以優(yōu)雅降級(jí)為客戶端渲染(這個(gè)后續(xù)會(huì)講,先占個(gè)坑)。

          3.1 實(shí)現(xiàn)

          既然服務(wù)端渲染能帶來這么多好處,那具體怎么實(shí)現(xiàn)呢?從官網(wǎng)給出的原理圖,我們可以清晰地看出:

          • Source 為我們的源代碼區(qū),即工程代碼;
          • Universal Appliation Code 和我們平時(shí)的客戶端渲染的代碼組織形式完全一致,只是需要注意這些代碼在 Node 端執(zhí)行過程觸發(fā)的生命周期鉤子不要涉及 DOM 和 BOM 對象即可;
          • 比客戶端渲染多出來的 app.js、Server entry 、Client entry 的主要作用為:app.js 分別給 Server entry 、Client entry 暴露出 createApp() 方法,使得每個(gè)請求進(jìn)來會(huì)生成新的 app 實(shí)例。而 Server entry 和 Client entry 分別會(huì)被 webpack 打包成 vue-ssr-server-bundle.json 和 vue-ssr-client-manifest.json(這兩個(gè) json 文件才是有用的,app.js、Server entry 、Client entry 可以抽離,開發(fā)者不感知);
          • Node 端會(huì)根據(jù) webpack 打包好的 vue-ssr-server-bundle.json,通過調(diào)用 createBundleRenderer 生成 renderer 實(shí)例,再通過調(diào)用 renderer.renderToString 生成完備的 html 字符串;
          • Node 端將 render 好的 html 字符串返回給 Browser,同時(shí) Node 端根據(jù) vue-ssr-client-manifest.json 生成的 js 會(huì)和 html 字符串 hydrate,完成客戶端激活 html,使得頁面可交互。

          3.2 優(yōu)化

          按照 Vue SSR 官方文檔建立起一個(gè)服務(wù)端渲染的工程后,是否就可以直接上線了呢?別急,我們先看看是否有什么可以優(yōu)化的地方。

          3.2.1 路由和代碼分割

          一個(gè)大的 SPA,主文件 js 往往很大,通過代碼分割可以將主文件 js 拆分為一個(gè)個(gè)單獨(dú)的路由組件 js 文件,可以很大程度上減小首屏的資源加載體積,其他路由組件可以預(yù)加載。

           復(fù)制代碼

          // router.js
          constIndex =()=>import(/* webpackChunkName: "index" */'./pages/Index.vue');
          constDetail =()=>import(/* webpackChunkName: "detail" */'./pages/Detail.vue');
          constroutes = [
          {
          path:'/',
          component: Index
          },
          {
          path:'/detail',
          component: Detail
          }
          ];
          constrouter =newRouter({
          mode:'history',
          routes
          });
          

          3.2.2 部分模塊(不需要 SSR 的模塊)客戶端渲染

          因?yàn)榉?wù)端渲染是 CPU 密集型操作,非首屏的模塊或者不重要的模塊(比如底部的推薦列表)完全可以采用客戶端渲染,只有首屏的核心模塊采用服務(wù)端渲染。這樣做的好處是明顯的:1. 較大地節(jié)省 CPU 資源;2. 減小了服務(wù)端渲染直出的 html 字符串長度,能夠更快地響應(yīng)給瀏覽器,減小白屏?xí)r間。

           復(fù)制代碼

          // Index.vue
          asyncData({ store }) {
          returnthis.methods.dispatch(store);// 核心模塊數(shù)據(jù)預(yù)取,服務(wù)端渲染
          }
          mounted() {
          this.initOtherModules();// 非核心模塊,客戶端渲染,在 mounted 生命周期鉤子里觸發(fā)
          }
          

          3.2 3 頁面緩存 / 組件緩存

          頁面緩存一般適用于狀態(tài)無關(guān)的靜態(tài)頁面,命中緩存直接返回頁面;組件緩存一般適用于純靜態(tài)組件,也可以一定程度上提升性能。

           復(fù)制代碼

          // page-level caching
          constmicroCache = LRU({
          max:100,
          maxAge:1000// 重要提示:條目在 1 秒后過期。
          })
          server.get('*', (req, res) => {
          consthit = microCache.get(req.url)
          if(hit) {// 命中緩存,直接返回頁面
          returnres.end(hit)
          }
          // 服務(wù)端渲染邏輯
          ...
          })
          

           復(fù)制代碼

          // component-level caching
          // server.js
          constLRU =require('lru-cache')
          constrenderer = createRenderer({
          cache: LRU({
          max:10000,
          maxAge: ...
          })
          });
          // component.js
          exportdefault{
          name:'item',// 必填選項(xiàng)
          props: ['item'],
          serverCacheKey:props=>props.item.id,
          render (h) {
          returnh('div',this.item.id)
          }
          };
          

          3.2.4 頁面靜態(tài)化

          如果工程中大部分頁面都是狀態(tài)相關(guān)的,所以技術(shù)選型采用了服務(wù)端渲染,但有部分頁面是狀態(tài)無關(guān)的,這個(gè)時(shí)候用服務(wù)端渲染就有點(diǎn)浪費(fèi)資源了。像這些狀態(tài)無關(guān)的頁面,完全可以通過 Nginx Proxy Cache 緩存到 Nginx 服務(wù)器,可以避免這些流量打到應(yīng)用服務(wù)器集群,同時(shí)也能減少響應(yīng)的時(shí)間。

          3.3 降級(jí)

          進(jìn)行優(yōu)化之后,是否就可以上線了呢?這時(shí)我們想一想,萬一服務(wù)端渲染出錯(cuò)了怎么辦?萬一服務(wù)器壓力飆升了怎么辦(因?yàn)榉?wù)端渲染是 CPU 密集型操作,很耗 CPU 資源)?為了保證系統(tǒng)的高可用,我們需要設(shè)計(jì)一些降級(jí)方案來避免這些。具體可以采用的降級(jí)方案有:

          • 單個(gè)流量降級(jí) – 偶發(fā)的服務(wù)端渲染失敗降級(jí)為客戶端渲染
          • Disconf / Apollo 配置降級(jí) – 分布式配置平臺(tái)修改配置主動(dòng)降級(jí),比如可預(yù)見性的大流量情況下(雙十一),可提前通過配置平臺(tái)將整個(gè)應(yīng)用集群都降級(jí)為客戶端渲染
          • CPU 閾值降級(jí) – 物理機(jī) / Docker 實(shí)例 CPU 資源占用達(dá)到閾值觸發(fā)降級(jí),避免負(fù)載均衡服務(wù)器在某些情況下給某臺(tái)應(yīng)用服務(wù)器導(dǎo)入過多流量,使得單臺(tái)應(yīng)用服務(wù)器的 CPU 負(fù)載過高
          • 旁路系統(tǒng)降級(jí) – 旁路系統(tǒng)跑定時(shí)任務(wù)監(jiān)控應(yīng)用集群狀態(tài),集群資源占用達(dá)到設(shè)定閾值將整個(gè)集群降級(jí)(或觸發(fā)集群的自動(dòng)擴(kuò)容)
          • 渲染服務(wù)集群降級(jí) – 若渲染服務(wù)和接口服務(wù)是獨(dú)立的服務(wù),當(dāng)渲染服務(wù)集群宕機(jī),html 的獲取邏輯回溯到 Nginx 獲取,此時(shí)觸發(fā)客戶端渲染,通過 ajax 調(diào)用接口服務(wù)獲取數(shù)據(jù)

          3.4 上線前準(zhǔn)備

          3.4.1 壓測

          壓測可以分為多個(gè)階段:本地開發(fā)階段、QA 性能測試階段、線上階段。

          • 本地開發(fā)階段:當(dāng)本地的服務(wù)端渲染開發(fā)完成之后,首先需要用 loadtest 之類的壓測工具壓下性能如何,同時(shí)可以根據(jù)壓測出來的數(shù)據(jù)做一些優(yōu)化,如果有內(nèi)存泄漏之類的 bug 也可以在這個(gè)階段就能被發(fā)現(xiàn)。
          • QA 性能測試階段:當(dāng)通過本地開發(fā)階段的壓測之后,我們的代碼已經(jīng)是經(jīng)過性能優(yōu)化且沒有內(nèi)存泄漏之類嚴(yán)重 bug 的。部署到 QA 性能測試環(huán)境之后,通過壓真實(shí) QA 環(huán)境,和原來的客戶端渲染做對比,看 QPS 會(huì)下降多少(因?yàn)榉?wù)端渲染耗更多的 CPU 資源,所以 QPS 對比客戶端渲染肯定會(huì)有下降)。
          • 線上階段:QA 性能測試階段壓測過后,若性能指標(biāo)達(dá)到原來的預(yù)期,部署到線上環(huán)境,同時(shí)可以開啟一定量的壓測,確保服務(wù)的可用性。

          3.4.2 日志

          作為生產(chǎn)環(huán)境的應(yīng)用,肯定不能“裸奔”,必須接入日志平臺(tái),將一些報(bào)錯(cuò)信息收集起來,以便之后問題的排查。

          3.4.3 灰度

          如果上線服務(wù)端渲染的工程是提供核心服務(wù)的應(yīng)用,應(yīng)該采用灰度發(fā)布的方式,避免全量上線。一般灰度方案可以采用:百分比灰度、白名單灰度、自定義標(biāo)簽灰度。具體采用哪種灰度方式看場景自由選擇,每隔一段時(shí)間觀察灰度集群沒有問題,所以漸漸增大灰度比例 / 覆蓋范圍,直到全量發(fā)布。

          3.5 落地

          在有贊電商的服務(wù)端渲染的落地場景中,我們抽離了單獨(dú)的依賴包,提供各個(gè)能力。

          3.6 效果

          從最終的上線效果來看,相同功能的頁面,服務(wù)端渲染的首屏內(nèi)容時(shí)間比客戶端渲染提升了 300%+。

          3.7 Q & A

          Q1:為什么服務(wù)端渲染就比客戶端渲染快呢?

          A:首先我們明確一點(diǎn),服務(wù)端渲染比客戶端渲染快的是首屏的內(nèi)容到達(dá)時(shí)間(而非首屏可交互時(shí)間)。至于為什么會(huì)更快,我們可以從兩者的 DOM 渲染過程來對比:

          客戶端渲染:瀏覽器發(fā)送請求 -> CDN / 應(yīng)用服務(wù)器返回空 html 文件 -> 瀏覽器接收到空 html 文件,加載的 css 和 js 資源 -> 瀏覽器發(fā)送 css 和 js 資源請求 -> CDN / 應(yīng)用服務(wù)器返回 css 和 js 文件 -> 瀏覽器解析 css 和 js -> js 中發(fā)送 ajax 請求到 Node 應(yīng)用服務(wù)器 -> Node 服務(wù)器調(diào)用底層服務(wù)后返回結(jié)果 -> 前端拿到結(jié)果 setData 觸發(fā) vue 組件渲染 -> 組件渲染完成

          服務(wù)端渲染:瀏覽器發(fā)送請求 -> Node 應(yīng)用服務(wù)器匹配路由 -> 數(shù)據(jù)預(yù)取:Node 服務(wù)器調(diào)用底層服務(wù)拿到 asyncData 存入 store -> Node 端根據(jù) store 生成 html 字符串返回給瀏覽器 -> 瀏覽器接收到 html 字符串將其激活:

          我們可以很明顯地看出,客戶端渲染的組件渲染強(qiáng)依賴 js 靜態(tài)資源的加載以及 ajax 接口的返回時(shí)間,而通常一個(gè) page.js 可能會(huì)達(dá)到幾十 KB 甚至更多,很大程度上制約了 DOM 生成的時(shí)間。而服務(wù)端渲染從用戶發(fā)出一次頁面 url 請求之后,應(yīng)用服務(wù)器返回的 html 字符串就是完備的計(jì)算好的,可以交給瀏覽器直接渲染,使得 DOM 的渲染不再受靜態(tài)資源和 ajax 的限制。

          Q2:服務(wù)端渲染有哪些限制?

          A:比較常見的限制比如:

          1. 因?yàn)殇秩具^程是在 Node 端,所以沒有 DOM 和 BOM 對象,因此不要在常見的 Vue 的 beforeCreate 和 created 生命周期鉤子里做涉及 DOM 和 BOM 的操作
          2. 對第三方庫的要求比較高,如果想直接在 Node 渲染過程中調(diào)用第三方庫,那這個(gè)庫必須支持服務(wù)端渲染

          Q3:如果我的需求只是生成文案類的靜態(tài)頁面,需要用到服務(wù)端渲染嗎?

          A:像這些和用戶狀態(tài)無關(guān)的靜態(tài)頁面,完全可以采用預(yù)渲染的方式(具體見 Vue SSR 官方指南),服務(wù)端渲染適用的更多場景會(huì)是狀態(tài)相關(guān)的(比如用戶信息相關(guān)),需要經(jīng)過 CPU 計(jì)算才能輸出完備的 html 字符串,因此服務(wù)端渲染是一個(gè) CPU 密集型的操作。而靜態(tài)頁面完全不需要涉及任何復(fù)雜計(jì)算,通過預(yù)渲染更快且更節(jié)省 CPU 資源。


          主站蜘蛛池模板: 人妻少妇精品一区二区三区| AV无码精品一区二区三区| 亚洲AV成人一区二区三区观看 | 国产免费一区二区三区| 久久久国产精品一区二区18禁| 亚洲视频一区二区| 一区二区三区观看| 亚洲香蕉久久一区二区三区四区| 色窝窝无码一区二区三区成人网站| 亚洲片一区二区三区| 精品aⅴ一区二区三区| 国产一区二区三区91| 亚洲一区影音先锋色资源| 精品国产鲁一鲁一区二区| 无码播放一区二区三区| 日本韩国一区二区三区| 国产成人精品无人区一区| 国产精品熟女一区二区| 日韩中文字幕一区| 在线精品国产一区二区三区 | 中文字幕久久久久一区| 亚洲天堂一区二区三区| 国产日韩精品一区二区在线观看 | 久久久久人妻一区精品果冻| 精品国产日产一区二区三区| 无码国产精品一区二区免费式影视 | 国产激情精品一区二区三区| 国产成人精品视频一区| 国产高清在线精品一区小说| 中文国产成人精品久久一区| 曰韩精品无码一区二区三区| 国产高清不卡一区二区| 亚洲精品国产suv一区88| 免费无码一区二区三区蜜桃大| 一区二区国产在线观看| 性色av一区二区三区夜夜嗨| av无码免费一区二区三区| 精品视频一区二区观看| 亲子乱av一区二区三区| 蜜桃AV抽搐高潮一区二区| 国产成人精品无人区一区|