一個典型的 SSR 應(yīng)用應(yīng)該有如下的源文件結(jié)構(gòu):
- index.html
- src/
- main.js # 導(dǎo)出環(huán)境無關(guān)的(通用的)應(yīng)用代碼
- entry-client.js # 將應(yīng)用掛載到一個 DOM 元素上
- entry-server.js # 使用某框架的 SSR API 渲染該應(yīng)用
index.html
將需要引用 entry-client.js
并包含一個占位標(biāo)記供給服務(wù)端渲染時注入:
<div id="app"><!--ssr-outlet--></div>
<script type="module" src="/src/entry-client.js"></script>
你可以使用任何你喜歡的占位標(biāo)記來替代 <!--ssr-outlet-->
,只要它能夠被正確替換。
如果需要執(zhí)行 SSR 和客戶端間情景邏輯,可以使用:
if (import.meta.env.SSR) {
// ... 僅在服務(wù)端執(zhí)行的邏輯
}
這是在構(gòu)建過程中被靜態(tài)替換的,因此它將允許對未使用的條件分支進行搖樹優(yōu)化。
在構(gòu)建 SSR 應(yīng)用程序時,你可能希望完全控制主服務(wù)器,并將 Vite 與生產(chǎn)環(huán)境脫鉤。因此,建議以中間件模式使用 Vite。下面是一個關(guān)于 express 的例子:
server.js
const fs = require('fs')
const path = require('path')
const express = require('express')
const { createServer: createViteServer } = require('vite')
async function createServer() {
const app = express()
// 以中間件模式創(chuàng)建 Vite 應(yīng)用,這將禁用 Vite 自身的 HTML 服務(wù)邏輯
// 并讓上級服務(wù)器接管控制
//
// 如果你想使用 Vite 自己的 HTML 服務(wù)邏輯(將 Vite 作為
// 一個開發(fā)中間件來使用),那么這里請用 'html'
const vite = await createViteServer({
server: { middlewareMode: 'ssr' }
})
// 使用 vite 的 Connect 實例作為中間件
app.use(vite.middlewares)
app.use('*', async (req, res) => {
// 服務(wù) index.html - 下面我們來處理這個問題
})
app.listen(3000)
}
createServer()
這里 vite 是 ViteDevServer 的一個實例。?vite.middlewares
? 是一個 Connect 實例,它可以在任何一個兼容 connect 的 Node.js 框架中被用作一個中間件。
下一步是實現(xiàn) ?*
? 處理程序供給服務(wù)端渲染的 HTML:
app.use('*', async (req, res) => {
const url = req.originalUrl
try {
// 1. 讀取 index.html
let template = fs.readFileSync(
path.resolve(__dirname, 'index.html'),
'utf-8'
)
// 2. 應(yīng)用 Vite HTML 轉(zhuǎn)換。這將會注入 Vite HMR 客戶端,
// 同時也會從 Vite 插件應(yīng)用 HTML 轉(zhuǎn)換。
// 例如:@vitejs/plugin-react-refresh 中的 global preambles
template = await vite.transformIndexHtml(url, template)
// 3. 加載服務(wù)器入口。vite.ssrLoadModule 將自動轉(zhuǎn)換
// 你的 ESM 源碼使之可以在 Node.js 中運行!無需打包
// 并提供類似 HMR 的根據(jù)情況隨時失效。
const { render } = await vite.ssrLoadModule('/src/entry-server.js')
// 4. 渲染應(yīng)用的 HTML。這假設(shè) entry-server.js 導(dǎo)出的 `render`
// 函數(shù)調(diào)用了適當(dāng)?shù)?SSR 框架 API。
// 例如 ReactDOMServer.renderToString()
const appHtml = await render(url)
// 5. 注入渲染后的應(yīng)用程序 HTML 到模板中。
const html = template.replace(`<!--ssr-outlet-->`, appHtml)
// 6. 返回渲染后的 HTML。
res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
} catch (e) {
// 如果捕獲到了一個錯誤,讓 Vite 來修復(fù)該堆棧,這樣它就可以映射回
// 你的實際源碼中。
vite.ssrFixStacktrace(e)
console.error(e)
res.status(500).end(e.message)
}
})
package.json
中的 dev
腳本也應(yīng)該相應(yīng)地改變,使用服務(wù)器腳本:
"scripts": {
- "dev": "vite"
+ "dev": "node server"
}
為了將 SSR 項目交付生產(chǎn),我們需要:
require()
? 直接加載,這樣便無需再使用 Vite 的 ?ssrLoadModule
?;?package.json
? 中的腳本應(yīng)該看起來像這樣:
{
"scripts": {
"dev": "node server",
"build:client": "vite build --outDir dist/client",
"build:server": "vite build --outDir dist/server --ssr src/entry-server.js "
}
}
注意使用 ?--ssr
? 標(biāo)志表明這將會是一個 SSR 構(gòu)建。同時需要指定 SSR 的入口。
接著,在 ?server.js
? 中,通過 ?process.env.NODE_ENV
? 條件分支,需要添加一些用于生產(chǎn)環(huán)境的特定邏輯:
dist/client/index.html
? 作為模板,而不是根目錄的 ?index.html
?,因為前者包含了到客戶端構(gòu)建的正確資源鏈接。require('./dist/server/entry-server.js')
? ,而不是 ?await vite.ssrLoadModule('/src/entry-server.js')
?(前者是 SSR 構(gòu)建后的最終結(jié)果)。vite
?開發(fā)服務(wù)器的創(chuàng)建和所有使用都移到 dev-only 條件分支后面,然后添加靜態(tài)文件服務(wù)中間件來服務(wù) ?dist/client
? 中的文件。可以在此參考 Vue 和 React 的設(shè)置范例。
vite build
支持使用 --ssrManifest
標(biāo)志,這將會在構(gòu)建輸出目錄中生成一份 ssr-manifest.json
:
- "build:client": "vite build --outDir dist/client",
+ "build:client": "vite build --outDir dist/client --ssrManifest",
上面的腳本將會為客戶端構(gòu)建生成 ?dist/client/ssr-manifest.json
?(是的,該 SSR 清單是從客戶端構(gòu)建生成而來,因為我們想要將模塊 ID 映射到客戶端文件上)。清單包含模塊 ID 到它們關(guān)聯(lián)的 chunk 和資源文件的映射。
為了利用該清單,框架需要提供一種方法來收集在服務(wù)器渲染調(diào)用期間使用到的組件模塊 ID。
?@vitejs/plugin-vue
? 支持該功能,開箱即用,并會自動注冊使用的組件模塊 ID 到相關(guān)的 Vue SSR 上下文:
// src/entry-server.js
const ctx = {}
const html = await vueServerRenderer.renderToString(app, ctx)
// ctx.modules 現(xiàn)在是一個渲染期間使用的模塊 ID 的 Set
我們現(xiàn)在需要在 server.js
的生產(chǎn)環(huán)境分支下讀取該清單,并將其傳遞到 src/entry-server.js
導(dǎo)出的 render
函數(shù)中。這將為我們提供足夠的信息,來為異步路由相應(yīng)的文件渲染預(yù)加載指令!
如果預(yù)先知道某些路由所需的路由和數(shù)據(jù),我們可以使用與生產(chǎn)環(huán)境 SSR 相同的邏輯將這些路由預(yù)先渲染到靜態(tài) HTML 中。這也被視為一種靜態(tài)站點生成(SSG)的形式。
許多依賴都同時提供 ESM 和 CommonJS 文件。當(dāng)運行 SSR 時,提供 CommonJS 構(gòu)建的依賴關(guān)系可以從 Vite 的 SSR 轉(zhuǎn)換/模塊系統(tǒng)進行 “外部化”,從而加速開發(fā)和構(gòu)建。例如,并非去拉取 React 的預(yù)構(gòu)建的 ESM 版本然后將其轉(zhuǎn)換回 Node.js 兼容版本,用 ?require('react')
? 代替會更有效。它還大大提高了 SSR 包構(gòu)建的速度。
Vite 基于以下策略執(zhí)行自動化的 SSR 外部化:
vue
? 將被自動外部化,因為它同時提供 ESM 和 CommonJS 構(gòu)建。react-dom
? 將被自動外部化,因為它只指定了唯一的一個 CommonJS 格式的入口。如果這個策略導(dǎo)致了錯誤,你可以通過 ?ssr.external
? 和 ?ssr.noExternal
? 配置項手動調(diào)整。
在未來,這個策略將可能得到改進,將去探測該項目是否有啟用 ?type: "module"
?,這樣 Vite 便可以在 SSR 期間通過動態(tài) ?import()
? 導(dǎo)入兼容 Node 的 ESM 構(gòu)建依賴來實現(xiàn)外部化依賴項。
如果你為某個包配置了一個別名,為了能使 SSR 外部化依賴功能正常工作,你可能想要使用的別名應(yīng)該指的是實際的 ?node_modules
? 中的包。Yarn 和 pnpm 都支持通過 ?npm
?: 前綴來設(shè)置別名。
一些框架,如 Vue 或 Svelte,會根據(jù)客戶端渲染和服務(wù)端渲染的區(qū)別,將組件編譯成不同的格式??梢韵蛞韵碌牟寮^子中,給 Vite 傳遞額外的 ?options
對象,對象中包含 ?ssr
?屬性來支持根據(jù)情景轉(zhuǎn)換:
resolveId
?load
?transform
?示例:
export function mySSRPlugin() {
return {
name: 'my-ssr',
transform(code, id, options) {
if (options?.ssr) {
// 執(zhí)行 ssr 專有轉(zhuǎn)換...
}
}
}
}
?options
中的 ?load
和 ?transform
為可選項,rollup 目前并未使用該對象,但將來可能會用額外的元數(shù)據(jù)來擴展這些鉤子函數(shù)。
注意 Vite 2.7 之前的版本,會提示你 ?ssr
參數(shù)的位置不應(yīng)該是 ?options
對象。目前所有主框架和插件都已對應(yīng)更新,但你可能還會發(fā)現(xiàn)使用過時 API 的舊文章。
SSR 構(gòu)建的默認目標(biāo)為 node 環(huán)境,但你也可以讓服務(wù)運行在 Web Worker 上。每個平臺的打包條目解析是不同的。你可以將ssr.target
設(shè)置為 webworker
,以將目標(biāo)配置為 Web Worker。
在某些如 ?webworker
運行時等特殊情況中,你可能想要將你的 SSR 打包成單個 JavaScript 文件。你可以通過設(shè)置 ?ssr.noExternal
? 為 ?true
來啟用這個行為。這將會做兩件事:
noExternal
?(非外部化)
更多建議: