Server-Side Rendering (SSR) 這個名稱真的是很常看到!
今天終於來到ep50,但怎麼還是對Vue還是覺得很陌生www
繼續學習吧SSR 這張卡片吧~ www
Vue.js 是一個用於構建客戶端應用的框架。默認情況下,Vue 組件會在瀏覽器中生成並操作 DOM 作為輸出。然而,也可以在伺服器上將相同的組件渲染為 HTML 字串,然後直接將其發送到瀏覽器,最終將靜態標記「水合」成為在客戶端的完全互動應用。
伺服器渲染的 Vue.js 應用也可以被視為「同構」isomorphic或「通用」universal,因為您應用的大部分代碼在伺服器和客戶端上運行。
為什麼使用 SSR? - Why SSR?
與客戶端的單頁應用(SPA)相比,SSR 的主要優勢在於:
提示: 目前,Google 和 Bing 可以很好地索引同步 JavaScript 應用。這裡的關鍵詞是「同步」。如果您的應用以加載旋轉器開始,然後通過 Ajax 獲取內容,爬蟲不會等您完成。這意味著如果您在 SEO 重要的頁面上有異步獲取的內容,那麼使用 SSR 可能是必要的。
使用 SSR 時還需要考慮一些權衡:
在使用 SSR 開發應用之前,您首先要問的問題是您是否真的需要它。這主要取決於內容顯示時間對您應用的重要性。例如,如果您正在構建一個內部儀表板,初次加載多幾百毫秒並不太重要,那麼使用 SSR 將會是多餘的。然而,在內容顯示時間至關重要的情況下,SSR 可以幫助您實現最佳的初始加載性能。
SSR vs. SSG
靜態網站生成(SSG),也稱為預渲染,是另一種構建快速網站的流行技術。如果渲染一個頁面所需的數據對每個用戶都是相同的,那麼我們可以在每次請求到達時只渲染一次,而是在構建過程中提前渲染。預渲染的頁面生成並作為靜態 HTML 文件提供。
SSG 保留了 SSR 應用的相同性能特徵:提供出色的內容顯示時間性能。同時,由於輸出是靜態 HTML 和資源,它的部署成本較低且更容易。這裡的關鍵詞是靜態:SSG 只能應用於提供靜態數據的頁面,即在構建時已知且在請求之間不會改變的數據。每當數據更改時,都需要進行新的部署。
如果您僅在調查 SSR 以改善少數營銷頁面的 SEO(例如 /、/about、/contact 等),那麼您可能希望選擇 SSG 而不是 SSR。SSG 對於基於內容的網站(如文檔網站或博客)也非常適合。事實上,您現在正在閱讀的這個網站就是使用 VitePress(一個基於 Vue 的靜態網站生成器)靜態生成的。
讓我們看看 Vue SSR 的最基本範例。
npm init -y
。package.json
中添加 "type": "module"
,以便 Node.js 以 ES 模組模式運行。npm install vue
。example.js
文件:// 這在伺服器上的 Node.js 中運行。
import { createSSRApp } from 'vue'
// Vue 的伺服器渲染 API 在 `vue/server-renderer` 下暴露。
import { renderToString } from 'vue/server-renderer'
const app = createSSRApp({
data: () => ({ count: 1 }),
template: `<button @click="count++">{{ count }}</button>`
})
renderToString(app).then((html) => {
console.log(html)
})
然後執行:
> node example.js
這應該會在命令行上打印以下內容:
<button>1</button>
renderToString()
接收一個 Vue 應用實例,並返回一個 Promise,該 Promise 解析為應用的渲染 HTML。還可以使用 Node.js Stream API 或 Web Streams API 進行流式渲染。請查看 SSR API 參考以獲取完整詳細資訊。
然後我們可以將 Vue SSR 代碼移動到伺服器請求處理程序中,將應用程序標記包裝在完整的頁面 HTML 中。我們將在接下來的步驟中使用 express:
npm install express
。server.js
文件:import express from 'express'
import { createSSRApp } from 'vue'
import { renderToString } from 'vue/server-renderer'
const server = express()
server.get('/', (req, res) => {
const app = createSSRApp({
data: () => ({ count: 1 }),
template: `<button @click="count++">{{ count }}</button>`
})
renderToString(app).then((html) => {
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>Vue SSR Example</title>
</head>
<body>
<div id="app">${html}</div>
</body>
</html>
`)
})
})
server.listen(3000, () => {
console.log('ready')
})
最後,執行 node server.js
,並訪問 http://localhost:3000。您應該會看到頁面正常工作並顯示按鈕。
如果您單擊按鈕,您會注意到數字不會改變。由於我們沒有在瀏覽器中加載 Vue,因此 HTML 在客戶端完全靜態。
要使客戶端應用具備交互性,Vue 需要執行水合步驟。在水合期間,它創建與在伺服器上運行的相同 Vue 應用,將每個組件與它應該控制的 DOM 節點匹配,並附加 DOM 事件監聽器。
要在水合模式下掛載應用,我們需要使用 createSSRApp()
而不是 createApp()
:
// 這在瀏覽器中運行。
import { createSSRApp } from 'vue'
const app = createSSRApp({
// ...與伺服器上的相同應用
})
// 在客戶端掛載 SSR 應用假設
// HTML 是預渲染的,將執行
// 水合而不是掛載新的 DOM 節點。
app.mount('#app')
注意到我們需要重用與伺服器上相同的應用實現。這是我們需要開始考慮 SSR 應用中的代碼結構的地方——如何在伺服器和客戶端之間共享相同的應用代碼?
這裡我們將演示最基本的設置。首先,讓我們將應用創建邏輯分割到一個專用文件 app.js
中:
// app.js(在伺服器和客戶端之間共享)
import { createSSRApp } from 'vue'
export function createApp() {
return createSSRApp({
data: () => ({ count: 1 }),
template: `<button @click="count++">{{ count }}</button>`
})
}
該文件及其依賴項在伺服器和客戶端之間共享——我們稱之為通用代碼。在編寫通用代碼時,您需要注意許多事項,稍後我們將討論。
我們的客戶端入口導入通用代碼,創建應用並執行掛載:
// client.js
import { createApp } from './app.js'
createApp().mount('#app')
而伺服器則在請求處理程序中使用相同的應用創建邏輯:
// server.js(不相關代碼省略)
import { createApp } from './app.js'
server.get('/', (req, res) => {
const app = createApp()
renderToString(app).then(html => {
// ...
})
})
此外,為了在瀏覽器中加載客戶端文件,我們還需要:
server.js
中添加 server.use(express.static('.'))
來服務客戶端文件。<script type="module" src="/client.js"></script>
來加載客戶端入口。import * from 'vue'
的語法。在 StackBlitz 上試試完成的範例。按鈕現在可以交互了!
從範例轉向生產就緒的 SSR 應用涉及更多工作。我們需要:
提示: Vue 組件在用於 SSR 時的編譯方式不同——模板被編譯為字符串連接,而不是虛擬 DOM 渲染函數,以提高渲染性能。
完整的實現將相當複雜,並且取決於您選擇的構建工具鏈。因此,我們強烈建議使用更高級、具有主見的解決方案,以便為您抽象掉複雜性。以下將介紹一些在 Vue 生態系統中推薦的 SSR 解決方案。
Nuxt
Nuxt 是一個基於 Vue 生態系統的高級框架,為編寫通用 Vue 應用提供了一個流暢的開發體驗。更好的是,您還可以將其用作靜態網站生成器!我們強烈推薦您試試。
Quasar
Quasar 是一個完整的基於 Vue 的解決方案,允許您針對 SPA、SSR、PWA、移動應用、桌面應用和瀏覽器擴展,所有這些都使用同一代碼庫。它不僅處理構建設置,還提供一整套符合 Material Design 的 UI 組件。
Vite SSR
Vite 提供對 Vue 伺服器端渲染的內建支持,但它的設計是故意較低階的。如果您希望直接使用 Vite,請查看 vite-plugin-ssr
,這是一個社區插件,為您抽象掉許多具有挑戰性的細節。
您還可以在這裡找到使用手動設置的 Vue + Vite SSR 項目的範例,這可以作為構建的基礎。請注意,這僅建議給有 SSR / 構建工具經驗並且真的想要對高級架構有完全控制權的人。
無論您的構建設置或高級框架選擇為何,以下原則適用於所有 Vue SSR 應用。
在 SSR 過程中,每個請求 URL 都映射到我們應用程序的期望狀態。因為沒有用戶互動和 DOM 更新,所以在伺服器上不需要響應性。為了提高性能,默認情況下在 SSR 期間禁用響應性。
由於沒有動態更新,生命週期鉤子如 onMounted
或 onUpdated
在 SSR 期間將不會被調用,只會在客戶端執行。
您應避免在 setup()
或 <script setup>
的根範圍內編寫需要清理的副作用代碼。例如,設置計時器的 setInterval
就是這樣的副作用。在僅用於客戶端的代碼中,我們可能會設置計時器,然後在 onBeforeUnmount
或 onUnmounted
中將其拆除。然而,由於卸載鉤子在 SSR 期間不會被調用,計時器將永久存在。為避免此問題,請將您的副作用代碼移到 onMounted
中。
通用代碼不能假定訪問平台特定的 API,因此如果您的代碼直接使用僅在瀏覽器中的全局變量如 window
或 document
,在 Node.js 中執行時會引發錯誤,反之亦然。
對於在伺服器和客戶端之間共享但具有不同平台 API 的任務,建議將平台特定的實現包裝在通用 API 內,或使用為您執行此操作的庫。例如,您可以使用 node-fetch
來在伺服器和客戶端上使用相同的 fetch
API。
對於僅在瀏覽器中的 API,通常的做法是在僅用於客戶端的生命週期鉤子(如 onMounted
)中延遲訪問它們。
請注意,如果第三方庫不是以通用使用為考量而編寫的,將其集成到伺服器渲染的應用中可能會很棘手。您可能能夠通過模擬某些全局變量使其運行,但這會顯得不穩定,並可能干擾其他庫的環境檢測代碼。
在狀態管理章節中,我們介紹了一種使用響應性 API 的簡單狀態管理模式。在 SSR 的上下文中,這一模式需要一些額外的調整。
這一模式在 JavaScript 模塊的根範圍內聲明共享狀態。這使它們成為單例,即在我們的應用程序整個生命週期中只有一個響應式對象的實例。在純客戶端 Vue 應用中,這樣的工作是預期的,因為我們應用中的模塊在每次瀏覽器頁面訪問時都是新初始化的。
然而,在 SSR 的上下文中,應用程序模塊通常僅在伺服器啟動時初始化一次。相同的模塊實例將在多個伺服器請求之間重用,我們的單例狀態對象也將如此。如果我們用特定於一個用戶的數據來變更共享的單例狀態,則可能意外地洩露到另一個用戶的請求中。我們稱這種情況為跨請求狀態污染。
技術上,我們可以在每次請求時重新初始化所有 JavaScript 模塊,就像在瀏覽器中一樣。然而,初始化 JavaScript 模塊可能是成本高昂的,因此這將顯著影響伺服器性能。
建議的解決方案是在每次請求時創建整個應用的新實例——包括路由器和全局商店。然後,而不是直接在我們的組件中導入它,我們使用應用級的 provide
提供共享狀態,並在需要它的組件中注入:
// app.js(伺服器和客戶端共享)
import { createSSRApp } from 'vue'
import { createStore } from './store.js'
// 每次請求時調用
export function createApp() {
const app = createSSRApp(/* ... */)
// 每次請求創建新的商店實例
const store = createStore(/* ... */)
// 在應用級別提供商店
app.provide('store', store)
// 也為水合目的暴露商店
return { app, store }
}
像 Pinia 這樣的狀態管理庫是為此設計的。更多細節請參考 Pinia 的 SSR 指南。
如果預渲染的 HTML 的 DOM 結構與客戶端應用的預期輸出不匹配,將會出現水合不匹配錯誤。水合不匹配最常見的原因有以下幾個:
<div>
不能放在 <p>
裡:<p><div>hi</div></p>
如果我們在伺服器渲染的 HTML 中生成這個,當瀏覽器遇到 <div>
時,將會終止第一個 <p>
並解析為以下 DOM 結構:
<p></p>
<div>hi</div>
<p></p>
v-if
+ onMounted
僅在客戶端渲染依賴隨機值的部分。您的框架可能還具有內建功能使這更容易,例如 VitePress 中的 <ClientOnly>
組件。當 Vue 遇到水合不匹配時,將嘗試自動恢復並調整預渲染的 DOM 以匹配客戶端狀態。這將導致一些渲染性能損失,因為不正確的節點將被丟棄,新的節點將被掛載,但在大多數情況下,應用應該繼續正常運行。儘管如此,在開發過程中消除水合不匹配仍然是最佳做法。
在 Vue 3.5+ 中,可以使用 data-allow-mismatch
屬性選擇性地抑制不可避免的水合不匹配。
由於大多數自定義指令涉及直接的 DOM 操作,因此在 SSR 期間會被忽略。然而,如果您想指定自定義指令的渲染方式(即它應該向渲染的元素添加什麼屬性),可以使用 getSSRProps
指令鉤子:
const myDirective = {
mounted(el, binding) {
// 客戶端實現:
// 直接更新 DOM
el.id = binding.value
},
getSSRProps(binding) {
// 伺服器端實現:
// 返回要渲染的屬性。
// `getSSRProps` 只接收指令綁定。
return {
id: binding.value
}
}
}
傳送內容 - Teleports
在 SSR 期間,傳送內容需要特別處理。如果渲染的應用包含傳送內容,則傳送的內容不會成為渲染字符串的一部分。一個簡單的解決方案是在掛載時有條件地渲染傳送內容。
如果您需要水合傳送的內容,它們會在 SSR 上下文對象的 teleports
屬性下暴露:
const ctx = {}
const html = await renderToString(app, ctx)
console.log(ctx.teleports) // { '#teleported': 'teleported content' }
您需要將傳送的標記注入到最終頁面 HTML 的正確位置,這與您需要注入主要應用標記的方式類似。
提示:在同時使用傳送內容和 SSR 時,避免針對<body>
進行操作——通常<body>
會包含其他伺服器渲染的內容,這使得傳送內容無法確定水合的正確起始位置。
相反,建議使用專用容器,例如<div id="teleported"></div>
,該容器僅包含傳送的內容。
這一篇也是內容好多~再來看看AI的總結吧
在使用 Vue.js 的應用中,選擇使用 SSR(伺服器端渲染)還是其他渲染方式(如 CSR,客戶端渲染)通常取決於以下幾個因素:
選擇 SSR 的時機主要取決於您的應用需求、性能考量及 SEO 需求。如果您需要良好的搜尋引擎可見性和更快的加載速度,那麼 SSR 是一個不錯的選擇。如果您的應用更注重用戶互動,則 CSR 可能會更合適。