Vite 服務(wù)端渲染(SSR)

2022-03-07 14:12 更新

源碼結(jié)構(gòu)

一個(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)化。

設(shè)置開發(fā)服務(wù)器

在構(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"
  }

生產(chǎn)環(huán)境構(gòu)建

為了將 SSR 項(xiàng)目交付生產(chǎn),我們需要:

  1. 正常生成一個(gè)客戶端構(gòu)建;
  2. 再生成一個(gè) SSR 構(gòu)建,使其通過 ?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è)置范例。

生成預(yù)加載指令

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ù)渲染/SSG

如果預(yù)先知道某些路由所需的路由和數(shù)據(jù),我們可以使用與生產(chǎn)環(huán)境 SSR 相同的邏輯將這些路由預(yù)先渲染到靜態(tài) HTML 中。這也被視為一種靜態(tài)站點(diǎn)生成(SSG)的形式。

SSR外部化

許多依賴都同時(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 外部化:

  • 如果一個(gè)依賴的解析 ESM 入口點(diǎn)和它的默認(rèn) Node 入口點(diǎn)不同,它的默認(rèn) Node 入口可能是一個(gè)可以外部化的 CommonJS 構(gòu)建。例如,?vue? 將被自動外部化,因?yàn)樗瑫r(shí)提供 ESM 和 CommonJS 構(gòu)建。
  • 否則,Vite 將檢查包的入口點(diǎn)是否包含有效的 ESM 語法 - 如果不包含,這個(gè)包可能是 CommonJS,將被外部化。例如,?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è)置別名。

SSR專有插件邏輯

一些框架,如 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 Target

SSR 構(gòu)建的默認(rèn)目標(biāo)為 node 環(huán)境,但你也可以讓服務(wù)運(yùn)行在 Web Worker 上。每個(gè)平臺的打包條目解析是不同的。你可以將ssr.target 設(shè)置為 webworker,以將目標(biāo)配置為 Web Worker。

SSR Bundle

在某些如 ?webworker運(yùn)行時(shí)等特殊情況中,你可能想要將你的 SSR 打包成單個(gè) JavaScript 文件。你可以通過設(shè)置 ?ssr.noExternal? 為 ?true來啟用這個(gè)行為。這將會做兩件事:

  • 將所有依賴視為 ?noExternal?(非外部化)
  • 若任何 Node.js 內(nèi)置內(nèi)容被引入,將拋出一個(gè)錯(cuò)誤


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號