一個(gè)典型的 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)用掛載到一個(gè) DOM 元素上
- entry-server.js # 使用某框架的 SSR API 渲染該應(yīng)用
index.html
將需要引用 entry-client.js
并包含一個(gè)占位標(biāo)記供給服務(wù)端渲染時(shí)注入:
<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)替換的,因此它將允許對未使用的條件分支進(jìn)行搖樹優(yōu)化。
在構(gòu)建 SSR 應(yīng)用程序時(shí),你可能希望完全控制主服務(wù)器,并將 Vite 與生產(chǎn)環(huán)境脫鉤。因此,建議以中間件模式使用 Vite。下面是一個(gè)關(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 作為
// 一個(gè)開發(fā)中間件來使用),那么這里請用 'html'
const vite = await createViteServer({
server: { middlewareMode: 'ssr' }
})
// 使用 vite 的 Connect 實(shí)例作為中間件
app.use(vite.middlewares)
app.use('*', async (req, res) => {
// 服務(wù) index.html - 下面我們來處理這個(gè)問題
})
app.listen(3000)
}
createServer()
這里 vite 是 ViteDevServer 的一個(gè)實(shí)例。?vite.middlewares
? 是一個(gè) Connect 實(shí)例,它可以在任何一個(gè)兼容 connect 的 Node.js 框架中被用作一個(gè)中間件。
下一步是實(shí)現(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 客戶端,
// 同時(shí)也會從 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 中運(yùn)行!無需打包
// 并提供類似 HMR 的根據(jù)情況隨時(shí)失效。
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) {
// 如果捕獲到了一個(gè)錯(cuò)誤,讓 Vite 來修復(fù)該堆棧,這樣它就可以映射回
// 你的實(shí)際源碼中。
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 項(xiàng)目交付生產(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)志表明這將會是一個(gè) SSR 構(gòu)建。同時(shí)需要指定 SSR 的入口。
接著,在 ?server.js
? 中,通過 ?process.env.NODE_ENV
? 條件分支,需要添加一些用于生產(chǎn)環(huán)境的特定邏輯:
dist/client/index.html
? 作為模板,而不是根目錄的 ?index.html
?,因?yàn)榍罢甙说娇蛻舳藰?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)建生成而來,因?yàn)槲覀兿胍獙⒛K 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)在是一個(gè)渲染期間使用的模塊 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)站點(diǎn)生成(SSG)的形式。
許多依賴都同時(shí)提供 ESM 和 CommonJS 文件。當(dāng)運(yùn)行 SSR 時(shí),提供 CommonJS 構(gòu)建的依賴關(guān)系可以從 Vite 的 SSR 轉(zhuǎn)換/模塊系統(tǒng)進(jìn)行 “外部化”,從而加速開發(fā)和構(gòu)建。例如,并非去拉取 React 的預(yù)構(gòu)建的 ESM 版本然后將其轉(zhuǎn)換回 Node.js 兼容版本,用 ?require('react')
? 代替會更有效。它還大大提高了 SSR 包構(gòu)建的速度。
Vite 基于以下策略執(zhí)行自動化的 SSR 外部化:
vue
? 將被自動外部化,因?yàn)樗瑫r(shí)提供 ESM 和 CommonJS 構(gòu)建。react-dom
? 將被自動外部化,因?yàn)樗恢付宋ㄒ坏囊粋€(gè) CommonJS 格式的入口。如果這個(gè)策略導(dǎo)致了錯(cuò)誤,你可以通過 ?ssr.external
? 和 ?ssr.noExternal
? 配置項(xiàng)手動調(diào)整。
在未來,這個(gè)策略將可能得到改進(jìn),將去探測該項(xiàng)目是否有啟用 ?type: "module"
?,這樣 Vite 便可以在 SSR 期間通過動態(tài) ?import()
? 導(dǎo)入兼容 Node 的 ESM 構(gòu)建依賴來實(shí)現(xiàn)外部化依賴項(xiàng)。
如果你為某個(gè)包配置了一個(gè)別名,為了能使 SSR 外部化依賴功能正常工作,你可能想要使用的別名應(yīng)該指的是實(shí)際的 ?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
為可選項(xiàng),rollup 目前并未使用該對象,但將來可能會用額外的元數(shù)據(jù)來擴(kuò)展這些鉤子函數(shù)。
注意 Vite 2.7 之前的版本,會提示你 ?ssr
參數(shù)的位置不應(yīng)該是 ?options
對象。目前所有主框架和插件都已對應(yīng)更新,但你可能還會發(fā)現(xiàn)使用過時(shí) API 的舊文章。
SSR 構(gòu)建的默認(rèn)目標(biāo)為 node 環(huán)境,但你也可以讓服務(wù)運(yùn)行在 Web Worker 上。每個(gè)平臺的打包條目解析是不同的。你可以將ssr.target
設(shè)置為 webworker
,以將目標(biāo)配置為 Web Worker。
在某些如 ?webworker
運(yùn)行時(shí)等特殊情況中,你可能想要將你的 SSR 打包成單個(gè) JavaScript 文件。你可以通過設(shè)置 ?ssr.noExternal
? 為 ?true
來啟用這個(gè)行為。這將會做兩件事:
noExternal
?(非外部化)
更多建議: