Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
eb 服務存在兩種 HTML 渲染方法。
最早的HTML(web 1.0時代),都是服務器端渲染的,瀏覽器發送請求,服務器端將整個html作為一個完整文檔發送給瀏覽器。最早響應瀏覽器請求的被稱為CGI .
CGI
Java語言進入web 開發領域后,首先出現的技術是 servlet,這個技術模仿的是CGI.也是在服務器端渲染好整個HTML文檔,然后反饋給瀏覽器。
Servlet能夠很好地組織業務邏輯代碼,但是在Java源文件中通過字符串拼接的方式生成動態HTML內容會導致代碼維護困難、可讀性差。于是產生了JSP技術,JSP在靜態HTML內容中嵌入Java代碼,Java代碼被動態執行后生成HTML內容,類似的還有ASP,PHP等技術,這些技術本質上都是服務端渲染好整個HTML文檔,都屬于服務器端渲染。
web2.0時代 最大的思想革命本質不是前后端分離,而是把網頁當作獨立的應用程序(app)。前后端分離只是實現這一新架構的必然結果。web 2.0 時代最重要的就是ajax技術。
使用ajax技術后,HTTP GET拿到的不是渲染后的網頁,而是一個由html和Javascript組成的應用, 這個應用以瀏覽器為虛擬機。裝載和顯示數據是app啟動之后的運行邏輯。傳統上應用叫什么?叫Client,也就是前端。于是前后端就這么分離了,瀏覽器變成了應用的運行環境,后端蛻化成了單純的業務邏輯和數據接口。最典型的ajax 應用就是gmail,gmail實質上就是把過去桌面端的email 應用搬到了瀏覽器中。ajax這種技術也就是客戶端渲染。
概在昨年下半年,我利用同構渲染技術,把公司中一個需要7、8秒才能打開的vue3項目成功優化至秒開(當然除了同構之外也配合了一些其他手段),由于那段時間vue3推出不久,很多框架這部分功能還沒有跟上,我便試著用vue和vite本身提供的api來完成同構,最終取得了令人滿意的效果,自己在這個過程中也獲益匪淺。
如今各大框架的功能已經完善,如果你現在想做同構渲染,我推薦直接使用next.js(react)或nuxt.js(vue)來進行開發,而不是像我一樣手動進行實現。本文主要是對于同構原理的描述,不涉及框架的使用。
為了讓小白也能看懂,文章會包含很多特別基礎的理論描述,如果覺得沒必要了解,你可以通過標題跳轉到自己感興趣的部分。文章中的代碼主要以vue為例,但是原理不局限于任何框架。
點擊這里查看完整代碼和PPT
以現在前端流行的react和vue框架為例。react中的jsx和vue里面的模板,都是是無法直接在瀏覽器運行的。將它們轉換成可在瀏覽器中運行的html,這個過程被稱為渲染。
CSR是現在前端開發者最熟悉的渲染方式。利用vue-cli或create-react-app創建一個應用,不作任何額外配置直接打包的出來代碼就是CSR。
你可以用如下的方法辨別一個web頁面是否是CSR:打開chrome控制臺 - 網絡面板,查看第一條請求,就能看到當前頁面向服務器請求的html資源;如果是CSR(如下圖所示),這個html的body中是沒有實際內容的。
那么頁面內容是如何渲染出來的呢?仔細看上面的html,會發現存在一個script標簽,打包器正是把整個應用都打包進了這個js文件里面。
當瀏覽器請求頁面的時候,服務器先會返回一個空的html和打包好的js代碼;等到js代碼下載完畢,瀏覽器再執行js代碼,頁面就被渲染出來了。因為頁面的渲染是在瀏覽器中而非服務器端進行的,所以被稱為客戶端渲染。
CSR會把整個網站打包進js里,當js下載完畢后,相當于網站的頁面資源都被下載好了。這樣在跳轉新頁面的時候,不需要向服務器再次請求資源(js會直接操作dom進行頁面渲染),從而讓整個網站的使用體驗上更加流暢。
但是這種做法也帶來了一些問題:在請求第一個頁面的時候需要下載js,而下載js直至頁面渲染出來這段時間,頁面會因為沒有任何內容而出現白屏。在js體積較大或者渲染過程較為復雜的情況下,白屏問題會非常明顯。
另外,由于使用了CSR的網站,會先下載一個空的html,然后才通過js進行渲染;這個空的html會導致某些搜索引擎無法通過爬蟲正確獲取網站信息,從而影響網站的搜索引擎排名(一般稱之為搜索引擎優化Search Engine Optimization,簡稱SEO)。
總而言之,客戶端渲染就是通過犧牲首屏加載速度和SEO,來獲取用戶體驗的一種技術。
理解了CSR,SSR也很好理解了,其實就是把渲染過程放在了在服務器端。以早年比較流行的java服務器端渲染技術jsp為例,會先寫一個html模板,并用特殊的語法<%...%>標記動態內容,里面可以寫一些java程序。
渲染的時候,jsp會通過字符串替換的方式,把<%...%>替換為程序執行的結果。最后服務器將替換完畢的html以字符串的形式發送給用戶即可。
同時我們還可以寫很多個JSP,根據用戶的http請求路徑返回相應的文件,這樣就完成了一個網站的開發。
// jsp示例
<body>
<hr>
<hr>
<h2>java腳本1</h2>
<%
Object obj = new Object();
System.out.println(obj);
out.write(obj.toString()); // 這一行表示把結果輸出到最終的html中
%>
<hr>
<hr>
<%
out.write(obj.toString());
%>
</body>
像jsp這類SSR技術,優劣勢和客戶端渲染正好相反:因為html在服務器端就已經渲染好了,所以不存在客戶端的白屏和seo問題;相對應地,每次跳轉頁面都要向服務器重新請求,意味著用戶每次切換頁面都要等待一小段時間,所以用戶體驗方面則不如客戶端。
還有一點顯而易見的問題,就是SSR相比CSR會占用較多的服務器端資源。
總而言之,服務器端渲染擁有良好的首屏性能和SEO,但用戶體驗方面較差。且會占用較多的服務器端資源。
可以看到,CSR和SSR的優劣勢是互補的,所以只要把它們二者結合起來,就能實現理想的渲染方法,也就是同構渲染。
同構的理念十分簡單,最開始的步驟和SSR相同,將生成的html字符串返回給用戶即可;但同時我們可以將CSR生成的JS也一并發送給用戶;這樣用戶在接收到SSR生成的html后,頁面還會再執行一次CSR的流程。
這導致用戶只有請求的第一個頁面是在服務器端渲染的,其他頁面則都是在客戶端進行的。這樣我們就擁有了一個同時兼顧首屏、SEO和用戶體驗的網站。
當然這只是最簡單的概念描述,實際操作起來仍然有不少難點。我將在后面的內容一一指出。
以下摘自《vue.js設計與實現》
CSR | SSR | 同構 | |
SEO | 不友好 | 友好 | 友好 |
白屏問題 | 有 | 無 | 無 |
占用服務器資源 | 少 | 多 | 中 |
用戶體驗 | 好 | 差 | 好 |
查看完整的代碼可以點擊這里。
前面說過,同構渲染可以看作把SSR和CSR進行結合。單獨完成SSR和CSR都很簡單:CSR就不用說了;SSR的話,vue和react都提供了renderToString函數,只要將組件傳入這個函數,可以直接將組件渲染成html字符串。
還有一點需要注意的是,在客戶端渲染里我們會使用createApp來創建一個vue應用實例,但在同構渲染中則需要替換成createSSRApp。如果仍然使用原本的createApp,會導致首屏頁面先在服務器端渲染一次,瀏覽器端又重復渲染一次。
而使用了createSSRApp,vue就會在瀏覽器端渲染前先進行一次檢查,如果結果和服務器端渲染的結果一致,就會停止首屏的客戶端渲染過程,從而避免了重復渲染的問題。
代碼如下:
import { renderToString } from 'vue/server-renderer'
import { createSSRApp } from 'vue'
// 一個計數的vue組件
function createApp() {
// 通過createSSRApp創建一個vue實例
return createSSRApp({
data: () => ({ count: 1 }),
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文件,然后執行node xxx.js,即可看到控制臺打印出渲染好的字符串,如下:
為了簡便,這里使用比較流行的express作為服務器。代碼很簡單,直接看注釋就能理解。
import express from 'express'
import { renderToString } from 'vue/server-renderer'
import { createSSRApp } from 'vue'
// 一個計數的vue組件
function createApp() {
return createSSRApp({
data: () => ({ count: 1 }),
template: `<button @click="count++">{{ count }}</button>`,
});
}
// 創建一個express實例
const server = express();
// 通過express.get方法創建一個路由, 作用是當瀏覽器訪問'/'時, 對該請求進行處理
server.get('/', (req, res) => {
// 通過createSSRApp創建一個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);
});
})
// 監聽3000端口
server.listen(3000, () => {
console.log('ready http://localhost:3000')
})
同樣在控制臺輸入node xxx.js,即可啟動服務器,然后在瀏覽器訪問http://localhost:3000/ ,就能訪問到頁面了。
如果你訪問過上面的地址,就會發現頁面上的按鈕是點不動的。這是因為通過renderToString渲染出來的頁面是完全靜態的,這時候就要進行客戶端激活。
激活的方法其實就是執行一遍客戶端渲染,在vue里面就是執行app.mount。我們可以創建一個js,在里面寫入客戶端激活的代碼,然后通過script標簽把這個文件插入到html模板中,這樣瀏覽器就會請求這個js文件了。
如下所示,首先寫一段客戶端激活的代碼,放到名為client-entry.js的文件里:
import { createSSRApp } from 'vue'
// 通過createSSRApp創建一個vue實例
function createApp() {
return createSSRApp({
data: () => ({ count: 1 }),
template: `<button @click="count++">{{ count }}</button>`,
});
}
createApp().mount('#app');
可以看到,這里的createApp函數和服務器端的counter組件是完全相同的(在實際開發中,createApp代表的就是你的整個應用),所以客戶端激活實際上就是把客戶端渲染再執行一遍,唯一區別就是要使用createSSRApp這個api防止重復渲染。
另外,要使用vue激活,我們還需要在客戶端下載vue。因為我們的代碼沒有經過打包器轉換,所以沒法在瀏覽器中直接使用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資源也是有內容的,不再是csr那種空白的html了。
查看完整的代碼可以點擊這里。
同構應用還有一個比較重要的點,就是如何實現服務器端的數據的預取,并讓其隨著html一起傳遞到瀏覽器端。
例如我們有一個列表頁,列表數據是從其他服務器獲取的;為了讓用戶第一時間就看到頁面內容,最好的方法當然是在服務器就拿到數據,然后隨著html一起傳遞給瀏覽器。瀏覽器拿到html和傳過來的數據,直接對頁面進行初始化,而不需要再在客戶端請求這個接口(除非服務器端因為某些原因獲取數據失敗)。
為了實現這個功能,整個過程分為兩部分:
注水其實就是前面提到過的客戶端激活,區別只是前面的沒有數據,而這次我們會試著加上數據。國內也有翻譯成"水合"的,現在你應該知道了,注水、客戶端激活、水合還有Hydrate其實都是一碼事。
查看完整的代碼可以點擊這里。
要在服務器端直接請求一個接口當然很簡單,但是為了保持最基本的前后端分離,我們最好的寫法還是將接口請求寫在組件中。
為了讓服務器獲取到我們要請求的接口,我們可以在vue組件中掛載一個自定義函數,然后在服務器端調用這個函數即可(需要注意的是,服務器環境不能直接使用fetch,應該用axios或者node-fetch替代)。如下:
// 組件中的代碼
import { createSSRApp } from 'vue'
function createApp() {
return createSSRApp({
data: () => ({ count: 1 }),
template: `<button @click="count++">{{ count }}</button>`,
// 自定義一個名為asyncData的函數
asyncData: async () => {
// 在處理遠程數據并return出去
const data = await getSomeData()
return data;
}
});
}
// 服務器端的代碼
const app = createApp();
// 保存初始化數據
let initData = null;
// 判斷是否有我們自定義的asyncData方法,如果有就用該函數初始化數據
if (app._component.asyncData) {
initData = await app._component.asyncData();
}
拿到數據后該如何傳遞到瀏覽器呢?其實有一個很簡單的方法:我們可以把數據格式化成字符串,然后用如下的方式,直接將這個字符串放到html模板的一個script標簽中:
const htmlStr = `
<!DOCTYPE html>
<html>
<head>
...
// 將數據格式化成json字符串,放到script標簽中
<script>window.__INITIAL_DATA__ = ${JSON.stringify(initData)}</script>
</head>
...
</html>
`;
當html被傳到瀏覽器端的時候,這個script標簽就會被瀏覽器執行,于是我們的數據就被放到了window.__INITIAL_DATA__里面。此時客戶端就可以從這個對象里面拿到數據了。
實現了脫水,注水就很簡單了。我們先判斷window.__INITIAL_DATA__是否有值,如果有的話直接將其賦值給頁面state;否則就讓客戶對自己請求一次接口。代碼如下:
function createApp() {
return createSSRApp({
data: () => ({ count: 1 }),
template: `<button @click="count++">{{ count }}</button>`,
// 自定義一個名為asyncData的函數
asyncData: async () => {
// 在處理遠程數據并return出去
const data = await getSomeData()
return data;
},
async mounted() {
// 如果已經有數據了,直接從window中獲取
if (window.__INITIAL_DATA__) {
// 有服務端數據時,使用服務端渲染時的數據
this.count = window.__INITIAL_DATA__;
window.__INITIAL_DATA__ = undefined;
return;
} else {
// 如果沒有數據,就請求數據
this.count = await getSomeData();
}
}
});
}
這樣我們就實現了一套完整的注水和脫水流程。
查看完整的代碼可以點擊這里。
服務器端和瀏覽器端環境不同,所以我們不能像寫csr代碼一樣寫同構代碼。根據我的踩坑經歷,寫同構應用需要尤其注意以下幾點:
服務器端返回給客戶端的每個請求都應該是全新的、獨立的應用程序實例,因此不應當有單例對象——也就是避免直接將對象或變量創建在全局作用域,否則它將在所有請求之間共享,在不同請求之間造成狀態污染。
在客戶端中,vue/pinia/vue-router都是以單例的形式存在,為此可以用函數的形式將vue/pinia/vue-router等進行初始化。也就是像上面的例子那樣,用一個函數進行包裹,然后調用這個函數進行應用的初始化。
服務器端是node環境,而客戶端是瀏覽器環境,如果你在node端直接使用了像 window 、 document或者fetch(在node端應該用axios或node-fetch),這種僅瀏覽器可用的全局變量或api,則會在 Node.js 中執行時拋出錯誤;反之,在瀏覽器使用了node端的api也是如此。
需要注意的是,在vue組件中,服務器端渲染時只會執行beforeCreate和created生命周期,在這兩個生命周期之外執行瀏覽器api是安全的。所以推薦將操作dom或訪問window之類的瀏覽器行為,一并寫在onMounted生命周期中,這樣就能避免在node端訪問到瀏覽器api。
如果要在這兩個生命周期中使用瀏覽器端api,可以利用相關打包工具提供的變量(如vite提供了import.meta.env.SSR),來避免服務器端調用相關代碼。
尤其需要注意的是,一些組件庫可能也會因為編寫的時候沒有考慮到服務器端渲染的情況,導致渲染出錯。這時候可以借助一些第三方組件,如nuxt中的ClientOnly,可以避免這些出錯的組件在服務器端進行渲染。
vue服務器端渲染會執行beforeCreate和created生命周期,應該避免在這兩個生命周期里產生全局副作用的代碼。
例如使用setInterval設置定時器。在純客戶端的代碼中,我們可以設置一個定時器,然后在 beforeDestroy 或 destroyed 生命周期時將其銷毀。但是,由于在 SSR 期間并不會調用銷毀鉤子函數,所以 timer 將永遠保留下來,最終造成服務器內存溢出。
上面的例子是一個最基礎的同構渲染,但距離一個能在開發中實際使用的框架還差得很遠。如果把這些內容都細細講完,我估摸文章要到三萬字了,實在太累,而且也很難讓新手程序員看得懂。所以這些難點我只講解一下關鍵點,如果有興趣深究的可以下來自己研究。
按照我踩坑的經歷,至少還要解決下面幾個問題:
順帶一提,vue社區有一篇vue ssr指南也值得一看,雖然只有vue2版本的,但是仍然有很多值得學習的地方。
這部分內容實在太多太雜,需要對打包工具有比較好的掌握才能理解。好在vite官方已經有了一篇完善的教程,而且提供了完整的代碼示例,想深入了解的可以點進去看看。
前端路由都提供了相關的api來輔助服務器端進行處理。如vue-router進行服務器端處理的流程如下:
官方文檔一般就有詳細教程,如pinia官網就有教你如何進行服務器端渲染。實際上全局狀態管理庫的處理就是脫水和注水,所以這里不做詳細解釋了。
頁面內容一般會渲染到id為app的節點下,但像vue中的teleport和react的portal獨立于app節點外,因此需要單獨處理。
這里建議把所有的根節點之外的元素統一設置到一個節點下面,如teleport可以通過設置to屬性來指定掛載的節點;同時vue也提供了方法來獲取所有的teleport。拿到teleport的信息后,即可通過字符串拼接的方式,將它們一并放到html模板中的目標節點下面了。
使用打包器可以生成manifest,它的作用是將打包后的模塊 ID 與它們關聯的 chunk 和資源文件進行映射(簡單理解就是通過它你可以知道js、圖片等頁面資源的位置在哪兒)。依靠這個manifest獲取資源的路徑,然后創建link標簽拼接到html模板中即可。
詳情可查看這里。
雖然我們寫好了服務端的代碼,但是這樣的代碼是十分脆弱的,無論性能還是可靠性都沒有保障,是沒法在實際生產中應用的。為此我們需要對服務端代碼進行一系列優化。
點擊這里查看完整代碼。
為了衡量服務器優化的指標,我們可以借助一系列測試工具,apach bench、jmeter等。我使用的是apach bench,它可以模擬一系列并發請求,用來對服務器進行壓力測試。
apach bench可以通過執行abs -n <請求總數> -c <并發數> <測試路徑>來進行測試。例:abs -n 1000 -c 100 http://localhost:3000/,表示以100并發的形式發送1000個請求到localhost:3000。
因為我們的服務本身比較簡單,所以這里我以1000并發的形式發送了10000個請求,結果如下:
可以看到Time taken for tests這一欄,總共花了6.6秒左右。
除此之外,我們還可以用Chrome瀏覽器的"開發者工具"作為node服務器的調試工具。使用node調試工具不僅能方便地進行調試,還可以清楚地看到諸如內存使用情況等指標,對代碼進行更精確地優化。
關于node調試工具的使用可以參考這篇文章。
node內置了cluster模塊,可以快速方便地創建子進程。如下:
通過os模塊判斷當前的cpu總數,然后通過cluster.isMaster判斷當前是否是主進程,最后通過cluster.fork即可創建一個子進程。
在主進程里,我們進行一些創建、維護子進程的工作,而在子進程里我們則運行真正的node服務。如下圖所示,我們啟動多線程再進行測試:
可以看到速度提升到了3.7秒,明顯快了很多。
通過process.memoryUsage();可以判斷當前子進程用掉的內存,當占用內存大于某個數(如300M)的時候,我們便將這個子進程關掉,防止內存泄露。
在子進程中,通過process.on('uncaughtException', err => {})可以獲取到該進程中的未捕獲異常(如服務器端渲染時候發生的一些錯誤)。當捕獲到錯誤后,我們可以對錯誤進行上報或寫入日志。
也可以借助一些第三方監控平臺如sentry來處理這類問題。sentry在node端的部署方法可以參考這里。
所謂心跳包檢測,就是主進程每隔一段時間向子進程發送一個信息,子進程收到這個信息后,立即回應給主進程一個信息;如果主進程在某次信息發送后,子進程沒有回應,說明子進程卡死了。這時候就需要殺死這個子進程然后重新創建一個。
所以心跳包檢測的作用主要是為了防止子進程卡死。
具體步驟如下:
主進程代碼如下:
子進程代碼如下:
在上面的代碼里,如果子進程因為某種錯誤(如內存溢出)而被關閉的時候,我們需要重新創建一個子進程,這樣就能保證線上服務能夠長時間運行了。通過如下代碼即可監聽子進程關閉并重新創建子進程。
點擊這里查看完整代碼。
文章到這里就結束了,如果有需要補充或者錯誤的地方,歡迎在評論區指出。
作者:monet
鏈接:https://juejin.cn/post/7289661061984501819
能大家在看到這個標題的時候,會覺得,只不過又是一篇爛大街的 SSR 從零入門的教程而已。別急,往下看,相信你或多或少會有一些不一樣的收獲呢。
在落地一種技術的時候,我們首先要想一想:
上面三個問題思考清楚之后,才能真正地去落地。上面三個問題思考清楚之后,才能真正地去落地。而有贊教育接入服務端渲染,正是為了優化 H5 頁面的首屏內容到達時間,帶來更好的用戶體驗(順便利于 SEO)。
說了這么多,以下開始正文。
一、后端模版引擎時代
在較早時期,前后端的配合模式為:后端負責服務層、業務邏輯層和模版渲染層(表現層);前端只是實現頁面的交互邏輯以及發送 AJAX。比較典型的例子就是 JSP 或 FreeMarker 模板引擎負責渲染出 html 字符串模版,字符串模版里的 js 靜態資源才是真正前端負責的東西。
而這種形式,就是天然的服務端渲染模式:用戶請求頁面 -> 請求發送到應用服務器 -> 后端根據用戶和請求信息獲取底層服務 -> 根據服務返回的數據進行組裝,同時 JSP 或 FreeMarker 模版引擎根據組裝的數據渲染為 html 字符串 -> 應用服務器講 html 字符串返回給瀏覽器 -> 瀏覽器解析 html 字符串渲染 UI 及加載靜態資源 -> js 靜態資源加載完畢界面可交互。
那么既然后端模版引擎時代帶來的效果就是我們想要的,那為啥還有以后讓前端發展服務端渲染呢?因為很明顯,這種模式從開發角度來講還有挺多的問題,比如:
二、SPA 時代
后來,誕生了 SPA(Single Page Application),解決了上面說的部分問題:
但同時,也帶來了一些問題:
三、服務端渲染
正因為 SPA 帶來的一些問題(尤其是首屏白屏的問題),接入服務端渲染顯得尤為必要。// 終于講到服務端渲染這個重點了。
而正是 Node 的發展和基于 Virtual DOM 的前端框架的出現,使得用 js 實現服務端渲染成為可能。因此在 SPA 的優勢基礎上,我們順便解決了因為 SPA 引入的問題:
3.1 實現
既然服務端渲染能帶來這么多好處,那具體怎么實現呢?從官網給出的原理圖,我們可以清晰地看出:
3.2 優化
按照 Vue SSR 官方文檔建立起一個服務端渲染的工程后,是否就可以直接上線了呢?別急,我們先看看是否有什么可以優化的地方。
3.2.1 路由和代碼分割
一個大的 SPA,主文件 js 往往很大,通過代碼分割可以將主文件 js 拆分為一個個單獨的路由組件 js 文件,可以很大程度上減小首屏的資源加載體積,其他路由組件可以預加載。
復制代碼
// 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 的模塊)客戶端渲染
因為服務端渲染是 CPU 密集型操作,非首屏的模塊或者不重要的模塊(比如底部的推薦列表)完全可以采用客戶端渲染,只有首屏的核心模塊采用服務端渲染。這樣做的好處是明顯的:1. 較大地節省 CPU 資源;2. 減小了服務端渲染直出的 html 字符串長度,能夠更快地響應給瀏覽器,減小白屏時間。
復制代碼
// Index.vue asyncData({ store }) { returnthis.methods.dispatch(store);// 核心模塊數據預取,服務端渲染 } mounted() { this.initOtherModules();// 非核心模塊,客戶端渲染,在 mounted 生命周期鉤子里觸發 }
3.2 3 頁面緩存 / 組件緩存
頁面緩存一般適用于狀態無關的靜態頁面,命中緩存直接返回頁面;組件緩存一般適用于純靜態組件,也可以一定程度上提升性能。
復制代碼
// page-level caching constmicroCache = LRU({ max:100, maxAge:1000// 重要提示:條目在 1 秒后過期。 }) server.get('*', (req, res) => { consthit = microCache.get(req.url) if(hit) {// 命中緩存,直接返回頁面 returnres.end(hit) } // 服務端渲染邏輯 ... })
復制代碼
// component-level caching // server.js constLRU =require('lru-cache') constrenderer = createRenderer({ cache: LRU({ max:10000, maxAge: ... }) }); // component.js exportdefault{ name:'item',// 必填選項 props: ['item'], serverCacheKey:props=>props.item.id, render (h) { returnh('div',this.item.id) } };
3.2.4 頁面靜態化
如果工程中大部分頁面都是狀態相關的,所以技術選型采用了服務端渲染,但有部分頁面是狀態無關的,這個時候用服務端渲染就有點浪費資源了。像這些狀態無關的頁面,完全可以通過 Nginx Proxy Cache 緩存到 Nginx 服務器,可以避免這些流量打到應用服務器集群,同時也能減少響應的時間。
3.3 降級
進行優化之后,是否就可以上線了呢?這時我們想一想,萬一服務端渲染出錯了怎么辦?萬一服務器壓力飆升了怎么辦(因為服務端渲染是 CPU 密集型操作,很耗 CPU 資源)?為了保證系統的高可用,我們需要設計一些降級方案來避免這些。具體可以采用的降級方案有:
3.4 上線前準備
3.4.1 壓測
壓測可以分為多個階段:本地開發階段、QA 性能測試階段、線上階段。
3.4.2 日志
作為生產環境的應用,肯定不能“裸奔”,必須接入日志平臺,將一些報錯信息收集起來,以便之后問題的排查。
3.4.3 灰度
如果上線服務端渲染的工程是提供核心服務的應用,應該采用灰度發布的方式,避免全量上線。一般灰度方案可以采用:百分比灰度、白名單灰度、自定義標簽灰度。具體采用哪種灰度方式看場景自由選擇,每隔一段時間觀察灰度集群沒有問題,所以漸漸增大灰度比例 / 覆蓋范圍,直到全量發布。
3.5 落地
在有贊電商的服務端渲染的落地場景中,我們抽離了單獨的依賴包,提供各個能力。
3.6 效果
從最終的上線效果來看,相同功能的頁面,服務端渲染的首屏內容時間比客戶端渲染提升了 300%+。
3.7 Q & A
Q1:為什么服務端渲染就比客戶端渲染快呢?
A:首先我們明確一點,服務端渲染比客戶端渲染快的是首屏的內容到達時間(而非首屏可交互時間)。至于為什么會更快,我們可以從兩者的 DOM 渲染過程來對比:
客戶端渲染:瀏覽器發送請求 -> CDN / 應用服務器返回空 html 文件 -> 瀏覽器接收到空 html 文件,加載的 css 和 js 資源 -> 瀏覽器發送 css 和 js 資源請求 -> CDN / 應用服務器返回 css 和 js 文件 -> 瀏覽器解析 css 和 js -> js 中發送 ajax 請求到 Node 應用服務器 -> Node 服務器調用底層服務后返回結果 -> 前端拿到結果 setData 觸發 vue 組件渲染 -> 組件渲染完成
服務端渲染:瀏覽器發送請求 -> Node 應用服務器匹配路由 -> 數據預取:Node 服務器調用底層服務拿到 asyncData 存入 store -> Node 端根據 store 生成 html 字符串返回給瀏覽器 -> 瀏覽器接收到 html 字符串將其激活:
我們可以很明顯地看出,客戶端渲染的組件渲染強依賴 js 靜態資源的加載以及 ajax 接口的返回時間,而通常一個 page.js 可能會達到幾十 KB 甚至更多,很大程度上制約了 DOM 生成的時間。而服務端渲染從用戶發出一次頁面 url 請求之后,應用服務器返回的 html 字符串就是完備的計算好的,可以交給瀏覽器直接渲染,使得 DOM 的渲染不再受靜態資源和 ajax 的限制。
Q2:服務端渲染有哪些限制?
A:比較常見的限制比如:
Q3:如果我的需求只是生成文案類的靜態頁面,需要用到服務端渲染嗎?
A:像這些和用戶狀態無關的靜態頁面,完全可以采用預渲染的方式(具體見 Vue SSR 官方指南),服務端渲染適用的更多場景會是狀態相關的(比如用戶信息相關),需要經過 CPU 計算才能輸出完備的 html 字符串,因此服務端渲染是一個 CPU 密集型的操作。而靜態頁面完全不需要涉及任何復雜計算,通過預渲染更快且更節省 CPU 資源。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。