框架環境 API
候選釋出版本 (Release Candidate)
環境 API 目前處於候選釋出階段。我們將在主要版本之間保持 API 的穩定性,以便生態系統能夠進行實驗和構建。但請注意,某些特定 API 仍被視為實驗性的。
一旦下游專案有時間試驗這些新功能並完成驗證,我們計劃在未來的主要版本中穩定這些新 API(可能會有破壞性變更)。
資源
請與我們分享您的反饋。
DevEnvironment 通訊級別
由於環境可能執行在不同的執行時中,因此針對環境的通訊可能會受到執行時限制。為了允許框架輕鬆編寫與執行時無關的程式碼,環境 API 提供了三種通訊級別。
RunnableDevEnvironment
RunnableDevEnvironment 是一個可以傳輸任意值的環境。隱式的 ssr 環境和其他非客戶端環境在開發過程中預設使用 RunnableDevEnvironment。雖然這要求執行時必須與 Vite 伺服器執行的執行時相同,但其工作方式與 ssrLoadModule 類似,並允許框架遷移併為其 SSR 開發場景啟用 HMR。您可以使用 isRunnableDevEnvironment 函式來守衛任何可執行的環境。
export class RunnableDevEnvironment extends DevEnvironment {
public readonly runner: ModuleRunner
}
class ModuleRunner {
/**
* URL to execute.
* Accepts file path, server path, or id relative to the root.
* Returns an instantiated module (same as in ssrLoadModule)
*/
public async import(url: string): Promise<Record<string, any>>
/**
* Other ModuleRunner methods...
*/
}
if (isRunnableDevEnvironment(server.environments.ssr)) {
await server.environments.ssr.runner.import('/entry-point.js')
}警告
runner 僅在首次訪問時才會惰性求值。請注意,當透過呼叫 process.setSourceMapsEnabled 或在不可用時覆蓋 Error.prepareStackTrace 來建立 runner 時,Vite 會啟用源對映(source map)支援。
在使用 SSR 設定指南中描述的中介軟體模式配置 Vite 伺服器的情況下,讓我們使用環境 API 實現 SSR 中介軟體。請記住,它不必命名為 ssr,因此在本示例中我們將其命名為 server。此處省略了錯誤處理。
import fs from 'node:fs'
import path from 'node:path'
import { createServer } from 'vite'
const viteServer = await createServer({
server: { middlewareMode: true },
appType: 'custom',
environments: {
server: {
// by default, modules are run in the same process as the vite server
},
},
})
// You might need to cast this to RunnableDevEnvironment in TypeScript or
// use isRunnableDevEnvironment to guard the access to the runner
const serverEnvironment = viteServer.environments.server
app.use('*', async (req, res, next) => {
const url = req.originalUrl
// 1. Read index.html
const indexHtmlPath = path.resolve(import.meta.dirname, 'index.html')
let template = fs.readFileSync(indexHtmlPath, 'utf-8')
// 2. Apply Vite HTML transforms. This injects the Vite HMR client,
// and also applies HTML transforms from Vite plugins, e.g. global
// preambles from @vitejs/plugin-react
template = await viteServer.transformIndexHtml(url, template)
// 3. Load the server entry. import(url) automatically transforms
// ESM source code to be usable in Node.js! There is no bundling
// required, and provides full HMR support.
const { render } = await serverEnvironment.runner.import(
'/src/entry-server.js',
)
// 4. render the app HTML. This assumes entry-server.js's exported
// `render` function calls appropriate framework SSR APIs,
// e.g. ReactDOMServer.renderToString()
const appHtml = await render(url)
// 5. Inject the app-rendered HTML into the template.
const html = template.replace(`<!--ssr-outlet-->`, appHtml)
// 6. Send the rendered HTML back.
res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
})當使用支援 HMR 的環境(例如 RunnableDevEnvironment)時,您應該在伺服器入口檔案中新增 import.meta.hot.accept() 以獲得最佳行為。否則,伺服器檔案的變更將使整個伺服器模組圖失效。
// src/entry-server.js
export function render(...) { ... }
if (import.meta.hot) {
import.meta.hot.accept()
}FetchableDevEnvironment
資訊
我們正在徵求關於 FetchableDevEnvironment 提案的反饋。
FetchableDevEnvironment 是一個可以透過 Fetch API 介面與其執行時進行通訊的環境。由於 RunnableDevEnvironment 只能在有限的執行時集合中實現,我們建議使用 FetchableDevEnvironment 代替 RunnableDevEnvironment。
該環境提供了一種透過 handleRequest 方法處理請求的標準化方式。
import {
createServer,
createFetchableDevEnvironment,
isFetchableDevEnvironment,
} from 'vite'
const server = await createServer({
server: { middlewareMode: true },
appType: 'custom',
environments: {
custom: {
dev: {
createEnvironment(name, config) {
return createFetchableDevEnvironment(name, config, {
handleRequest(request: Request): Promise<Response> | Response {
// handle Request and return a Response
},
})
},
},
},
},
})
// Any consumer of the environment API can now call `dispatchFetch`
if (isFetchableDevEnvironment(server.environments.custom)) {
const response: Response = await server.environments.custom.dispatchFetch(
new Request('http://example.com/request-to-handle'),
)
}警告
Vite 會驗證 dispatchFetch 方法的輸入和輸出:請求必須是全域性 Request 類的例項,響應必須是全域性 Response 類的例項。如果不是這種情況,Vite 將丟擲 TypeError。
請注意,儘管 FetchableDevEnvironment 是作為類實現的,但它被 Vite 團隊視為實現細節,隨時可能發生變化。
原始 DevEnvironment
如果環境沒有實現 RunnableDevEnvironment 或 FetchableDevEnvironment 介面,則需要手動設定通訊。
如果您的程式碼可以在與使用者模組相同的執行時中執行(即它不依賴於 Node.js 特定 API),則可以使用虛擬模組。這種方法消除了使用 Vite API 從程式碼訪問值的需要。
// code using the Vite's APIs
import { createServer } from 'vite'
const server = createServer({
plugins: [
// a plugin that handles `virtual:entrypoint`
{
name: 'virtual-module',
/* plugin implementation */
},
],
})
const ssrEnvironment = server.environment.ssr
const input = {}
// use exposed functions by each environment factories that runs the code
// check for each environment factories what they provide
if (ssrEnvironment instanceof CustomDevEnvironment) {
ssrEnvironment.runEntrypoint('virtual:entrypoint')
} else {
throw new Error(`Unsupported runtime for ${ssrEnvironment.name}`)
}
// -------------------------------------
// virtual:entrypoint
const { createHandler } = await import('./entrypoint.js')
const handler = createHandler(input)
const response = handler(new Request('http://example.com/'))
// -------------------------------------
// ./entrypoint.js
export function createHandler(input) {
return function handler(req) {
return new Response('hello')
}
}例如,若要在使用者模組上呼叫 transformIndexHtml,可以使用以下外掛:
function vitePluginVirtualIndexHtml(): Plugin {
let server: ViteDevServer | undefined
return {
name: vitePluginVirtualIndexHtml.name,
configureServer(server_) {
server = server_
},
resolveId(source) {
return source === 'virtual:index-html' ? '\0' + source : undefined
},
async load(id) {
if (id === '\0' + 'virtual:index-html') {
let html: string
if (server) {
this.addWatchFile('index.html')
html = fs.readFileSync('index.html', 'utf-8')
html = await server.transformIndexHtml('/', html)
} else {
html = fs.readFileSync('dist/client/index.html', 'utf-8')
}
return `export default ${JSON.stringify(html)}`
}
return
},
}
}如果您的程式碼需要 Node.js API,您可以使用 hot.send 與使用 Vite API 的使用者模組程式碼進行通訊。但請注意,這種方法在構建過程之後可能無法以相同方式工作。
// code using the Vite's APIs
import { createServer } from 'vite'
const server = createServer({
plugins: [
// a plugin that handles `virtual:entrypoint`
{
name: 'virtual-module',
/* plugin implementation */
},
],
})
const ssrEnvironment = server.environment.ssr
const input = {}
// use exposed functions by each environment factories that runs the code
// check for each environment factories what they provide
if (ssrEnvironment instanceof RunnableDevEnvironment) {
ssrEnvironment.runner.import('virtual:entrypoint')
} else if (ssrEnvironment instanceof CustomDevEnvironment) {
ssrEnvironment.runEntrypoint('virtual:entrypoint')
} else {
throw new Error(`Unsupported runtime for ${ssrEnvironment.name}`)
}
const req = new Request('http://example.com/')
const uniqueId = 'a-unique-id'
ssrEnvironment.send('request', serialize({ req, uniqueId }))
const response = await new Promise((resolve) => {
ssrEnvironment.on('response', (data) => {
data = deserialize(data)
if (data.uniqueId === uniqueId) {
resolve(data.res)
}
})
})
// -------------------------------------
// virtual:entrypoint
const { createHandler } = await import('./entrypoint.js')
const handler = createHandler(input)
import.meta.hot.on('request', (data) => {
const { req, uniqueId } = deserialize(data)
const res = handler(req)
import.meta.hot.send('response', serialize({ res: res, uniqueId }))
})
const response = handler(new Request('http://example.com/'))
// -------------------------------------
// ./entrypoint.js
export function createHandler(input) {
return function handler(req) {
return new Response('hello')
}
}構建過程中的環境
在 CLI 中,呼叫 vite build 和 vite build --ssr 為了向後相容,仍將僅構建客戶端環境和 SSR 環境。
當 builder 選項不為 undefined 時(或呼叫 vite build --app 時),vite build 將選擇構建整個應用程式。這在未來的大版本更新中將成為預設行為。屆時將建立一個 ViteBuilder 例項(構建時等同於 ViteDevServer)來為生產環境構建所有已配置的環境。預設情況下,環境構建按 environments 記錄的順序序列執行。框架或使用者可以使用 builder.buildApp 選項進一步配置環境的構建方式。
import { defineConfig } from 'vite'
export default defineConfig({
builder: {
buildApp: async (builder) => {
const environments = Object.values(builder.environments)
await Promise.all(
environments.map((environment) => builder.build(environment)),
)
},
},
})外掛也可以定義 buildApp 鉤子。順序為 'pre' 和 null 的鉤子會在配置的 builder.buildApp 之前執行,順序為 'post' 的鉤子會在其之後執行。可以使用 environment.isBuilt 來檢查環境是否已經完成構建。
與環境無關的程式碼
大多數情況下,當前 environment 例項將作為所執行程式碼上下文的一部分可用,因此透過 server.environments 訪問它們的需求很少見。例如,在外掛鉤子內部,環境被暴露為 PluginContext 的一部分,因此可以使用 this.environment 進行訪問。請參閱 外掛環境 API 以瞭解如何構建環境感知外掛。
