<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          前端頁面秒開的關鍵 - 小白也能看懂的同構(gòu)渲染原理和實現(xiàn)

          共 18782字,需瀏覽 38分鐘

           ·

          2023-10-30 10:36


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

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

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

          點擊這里查看完整代碼和PPT(https://github.com/fuxiang123/Isomorphic-test/tree/master)

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

          1.1 什么是渲染?

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

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

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

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


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

          當瀏覽器請求頁面的時候,服務器先會返回一個空的html和打包好的js代碼;等到js代碼下載完畢,瀏覽器再執(zhí)行js代碼,頁面就被渲染出來了。因為頁面的渲染是在瀏覽器中而非服務器端進行的,所以被稱為客戶端渲染。

          CSR的優(yōu)劣

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

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

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

          「總而言之,客戶端渲染就是通過犧牲首屏加載速度和SEO,來獲取用戶體驗的一種技術?!?/strong>

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

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

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

          同時我們還可以寫很多個JSP,根據(jù)用戶的http請求路徑返回相應的文件,這樣就完成了一個網(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技術,優(yōu)劣勢和客戶端渲染正好相反:因為html在服務器端就已經(jīng)渲染好了,所以不存在客戶端的白屏和seo問題;相對應地,每次跳轉(zhuǎn)頁面都要向服務器重新請求,意味著用戶每次切換頁面都要等待一小段時間,所以用戶體驗方面則不如客戶端。

          還有一點顯而易見的問題,就是SSR相比CSR會占用較多的服務器端資源。

          「總而言之,服務器端渲染擁有良好的首屏性能和SEO,但用戶體驗方面較差。且會占用較多的服務器端資源?!?/strong>

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

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

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

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

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

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

          以下摘自《vue.js設計與實現(xiàn)》


          CSR SSR 同構(gòu)
          SEO 不友好 友好 友好
          白屏問題
          占用服務器資源
          用戶體驗

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

          查看完整的代碼可以點擊這里(https://github.com/fuxiang123/Isomorphic-test/tree/master/%E7%AE%80%E5%8D%95%E5%90%8C%E6%9E%84)。

          2.1 服務器端渲染html字符串

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

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

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

          代碼如下:

          import { renderToString } from 'vue/server-renderer'
          import { createSSRApp } from 'vue'

          // 一個計數(shù)的vue組件
          function createApp({
            // 通過createSSRApp創(chuàng)建一個vue實例
            return createSSRApp({
              data() => ({ count1 }),
              template`<button @click="count++">{{ count }}</button>`,
            });
          }

          const app = createApp();

          // 通過renderToString將vue實例渲染成字符串
          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);
          });

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


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

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

          import express from 'express'
          import { renderToString } from 'vue/server-renderer'
          import { createSSRApp } from 'vue'

          // 一個計數(shù)的vue組件
          function createApp({
            return createSSRApp({
              data() => ({ count1 }),
              template`<button @click="count++">{{ count }}</button>`,
            });
          }

          // 創(chuàng)建一個express實例
          const server = express();

          // 通過express.get方法創(chuàng)建一個路由, 作用是當瀏覽器訪問'/'時, 對該請求進行處理
          server.get('/', (req, res) => {

            // 通過createSSRApp創(chuàng)建一個vue實例
            const app = createApp();
            
            // 通過renderToString將vue實例渲染成字符串
            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')
          })

          同樣在控制臺輸入node xxx.js,即可啟動服務器,然后在瀏覽器訪問http://localhost:3000/ ,就能訪問到頁面了。


          2.3 激活客戶端渲染

          如果你訪問過上面的地址,就會發(fā)現(xiàn)頁面上的按鈕是點不動的。這是因為通過renderToString渲染出來的頁面是完全靜態(tài)的,這時候就要進行客戶端激活。

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

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

          import { createSSRApp } from 'vue'

          // 通過createSSRApp創(chuàng)建一個vue實例
          function createApp({
            return createSSRApp({
              data() => ({ count1 }),
              template`<button @click="count++">{{ count }}</button>`,
            });
          }

          createApp().mount('#app');

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

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

          改造后的如下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>
          `;

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


          「查看完整的代碼可以」點擊這里(https://github.com/fuxiang123/Isomorphic-test/tree/master/%E7%AE%80%E5%8D%95%E5%90%8C%E6%9E%84)。

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

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

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

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

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

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

          「查看完整的代碼可以」點擊這里(https://github.com/fuxiang123/Isomorphic-test/tree/master/%E5%AE%9E%E7%8E%B0%E8%84%B1%E6%B0%B4%E5%92%8C%E6%B3%A8%E6%B0%B4)。

          3.1 實現(xiàn)服務器端脫水

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

          為了讓服務器獲取到我們要請求的接口,我們可以在vue組件中掛載一個自定義函數(shù),然后在服務器端調(diào)用這個函數(shù)即可。如下:

          // 組件中的代碼
          import { createSSRApp } from 'vue'
          function createApp({
            return createSSRApp({
              data() => ({ count1 }),
              template`<button @click="count++">{{ count }}</button>`,
              // 自定義一個名為asyncData的函數(shù)
              asyncDataasync () => { 
                  // 在處理遠程數(shù)據(jù)并return出去
                  const data = await getSomeData()
                  return data; 
              }
            });
          }

          // 服務器端的代碼
          const app = createApp();
          // 保存初始化數(shù)據(jù)
          let initData = null;
          // 判斷是否有我們自定義的asyncData方法,如果有就用該函數(shù)初始化數(shù)據(jù)
          if (app._component.asyncData) {
              initData = await app._component.asyncData();
          }

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

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

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

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

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

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

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

          「查看完整的代碼可以」點擊這里(https://github.com/fuxiang123/Isomorphic-test/tree/master/%E5%AE%9E%E7%8E%B0%E8%84%B1%E6%B0%B4%E5%92%8C%E6%B3%A8%E6%B0%B4)。

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

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

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

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

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

          image.png

          4.2 避免訪問特定平臺api

          服務器端是node環(huán)境,而客戶端是瀏覽器環(huán)境,如果你在node端直接使用了像 window 或 document,這種僅瀏覽器可用的全局變量,則會在 Node.js 中執(zhí)行時拋出錯誤;反之,在瀏覽器使用了node端的api也是如此。

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

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

          image.png

          尤其需要注意的是,一些組件庫可能也會因為編寫的時候沒有考慮到服務器端渲染的情況,導致渲染出錯。這時候可以借助一些第三方組件,如nuxt中的ClientOnly,可以避免這些出錯的組件在服務器端進行渲染。

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

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

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

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

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

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

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

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

          4.1 集成前端工具鏈

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

          4.2 集成前端路由

          前端路由都提供了相關的api來輔助服務器端進行處理。如vue-router進行服務器端處理的流程如下:

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

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

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

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

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

          這里建議把所有的根節(jié)點之外的元素統(tǒng)一設置到一個節(jié)點下面,如teleport可以通過設置to屬性來指定掛載的節(jié)點;同時vue也提供了方法來獲取所有的teleport(https://cn.vuejs.org/guide/scaling-up/ssr.html#teleports)。拿到teleport的信息后,即可通過字符串拼接的方式,將它們一并放到html模板中的目標節(jié)點下面了。

          4.5 處理預加載資源

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

          詳情可查看這里(https://cn.vitejs.dev/guide/ssr.html#generating-preload-directives)。

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

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

          點擊這里查看完整代碼(https://github.com/fuxiang123/Isomorphic-test/blob/master/%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%AB%AF%E4%BC%98%E5%8C%96/processDaemon.js)。

          5.1 服務器端測試

          壓力測試

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

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

          因為我們的服務本身比較簡單,所以這里我以1000并發(fā)的形式發(fā)送了10000個請求,結(jié)果如下:


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

          Node調(diào)試工具

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

          關于node調(diào)試工具的使用可以參考這篇文章(https://www.ruanyifeng.com/blog/2018/03/node-debugger.html)。

          5.2 多進程優(yōu)化

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

          image.png

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

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

          image.png

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

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

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


          5.4 處理未捕獲異常

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

          也可以借助一些第三方監(jiān)控平臺如sentry來處理這類問題。sentry在node端的部署方法可以參考這里(https://docs.sentry.io/platforms/node/)。


          5.5 心跳包檢測

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

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

          具體步驟如下:

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

          主進程代碼如下:

          子進程代碼如下:


          5.6 子進程自動重建

          在上面的代碼里,如果子進程因為某種錯誤(如內(nèi)存溢出)而被關閉的時候,我們需要重新創(chuàng)建一個子進程,這樣就能保證線上服務能夠長時間運行了。通過如下代碼即可監(jiān)聽子進程關閉并重新創(chuàng)建子進程。


          點擊這里查看完整代碼(https://github.com/fuxiang123/Isomorphic-test/blob/master/%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%AB%AF%E4%BC%98%E5%8C%96/processDaemon.js)

          瀏覽 110
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                    <th id="afajh"><progress id="afajh"></progress></th>
                    豆花视频综合 | 黄色欧美在线 | 日韩性爱视频 | 99视频在线精品免费看 | 看色婷婷免费视频 |