執行時環境 API
候選釋出版本 (Release Candidate)
環境 API 目前處於候選釋出階段。我們將在主要版本之間保持 API 的穩定性,以便生態系統能夠進行實驗和構建。但請注意,某些特定 API 仍被視為實驗性的。
一旦下游專案有時間試驗這些新功能並完成驗證,我們計劃在未來的主要版本中穩定這些新 API(可能會有破壞性變更)。
資源
請與我們分享您的反饋。
環境工廠
環境工廠旨在由 Cloudflare 等環境提供商實現,而非終端使用者。對於開發和構建環境都使用目標執行時的最常見情況,環境工廠會返回一個 EnvironmentOptions。同時也可以設定預設環境選項,以便使用者無需手動配置。
function createWorkerdEnvironment(
userConfig: EnvironmentOptions,
): EnvironmentOptions {
return mergeConfig(
{
resolve: {
conditions: [
/*...*/
],
},
dev: {
createEnvironment(name, config) {
return createWorkerdDevEnvironment(name, config, {
hot: true,
transport: customHotChannel(),
})
},
},
build: {
createEnvironment(name, config) {
return createWorkerdBuildEnvironment(name, config)
},
},
},
userConfig,
)
}隨後,配置檔案可以寫成:
import { createWorkerdEnvironment } from 'vite-environment-workerd'
export default {
environments: {
ssr: createWorkerdEnvironment({
build: {
outDir: '/dist/ssr',
},
}),
rsc: createWorkerdEnvironment({
build: {
outDir: '/dist/rsc',
},
}),
},
}框架可以使用 workerd 執行時環境,透過以下方式進行 SSR:
const ssrEnvironment = server.environments.ssr建立新的環境工廠
Vite 開發伺服器預設提供兩種環境:client 環境和 ssr 環境。客戶端環境預設為瀏覽器環境,模組執行器透過為客戶端應用匯入虛擬模組 /@vite/client 來實現。SSR 環境預設執行在與 Vite 伺服器相同的 Node 執行時中,並允許在開發過程中使用應用伺服器來渲染請求,同時完全支援 HMR。
轉換後的原始碼被稱為模組,每個環境中處理的模組之間的關係儲存在模組圖中。這些模組的轉換程式碼會被髮送到與每個環境關聯的執行時中執行。當模組在執行時中被評估時,其匯入的模組將被請求,從而觸發模組圖中相應部分的後續處理。
Vite 模組執行器(Module Runner)允許透過先使用 Vite 外掛處理程式碼來執行任何程式碼。它與 server.ssrLoadModule 不同,因為執行器的實現與伺服器是解耦的。這使得庫和框架作者能夠實現自己的 Vite 伺服器與執行器之間的通訊層。瀏覽器使用伺服器 WebSocket 和 HTTP 請求與其對應的環境進行通訊。Node 模組執行器可以因為執行在同一程序中而直接呼叫函式來處理模組。其他環境則可能透過連線到 JS 執行時(如 workerd)或像 Vitest 那樣的 Worker 執行緒來執行模組。
此功能的目標之一是提供一個可定製的 API 來處理和執行程式碼。使用者可以使用暴露的原語建立新的環境工廠。
import { DevEnvironment, HotChannel } from 'vite'
function createWorkerdDevEnvironment(
name: string,
config: ResolvedConfig,
context: DevEnvironmentContext
) {
const connection = /* ... */
const transport: HotChannel = {
on: (listener) => { connection.on('message', listener) },
send: (data) => connection.send(data),
}
const workerdDevEnvironment = new DevEnvironment(name, config, {
options: {
resolve: { conditions: ['custom'] },
...context.options,
},
hot: true,
transport,
})
return workerdDevEnvironment
}DevEnvironment 有多種通訊級別。為了方便框架編寫與執行時無關的程式碼,我們建議實現儘可能靈活的通訊級別。
ModuleRunner
模組執行器在目標執行時中例項化。除非另有說明,本節中的所有 API 均從 vite/module-runner 匯入。此匯出入口點儘可能保持輕量級,僅匯出建立模組執行器所需的最小集合。
型別簽名
export class ModuleRunner {
constructor(
public options: ModuleRunnerOptions,
public evaluator: ModuleEvaluator = new ESModulesEvaluator(),
private debug?: ModuleRunnerDebugger,
) {}
/**
* URL to execute.
* Accepts file path, server path, or id relative to the root.
*/
public async import<T = any>(url: string): Promise<T>
/**
* Clear all caches including HMR listeners.
*/
public clearCache(): void
/**
* Clear all caches, remove all HMR listeners, reset sourcemap support.
* This method doesn't stop the HMR connection.
*/
public async close(): Promise<void>
/**
* Returns `true` if the runner has been closed by calling `close()`.
*/
public isClosed(): boolean
}ModuleRunner 中的模組評估器負責執行程式碼。Vite 預設匯出 ESModulesEvaluator,它使用 new AsyncFunction 來評估程式碼。如果你的 JavaScript 執行時不支援不安全的評估,你可以提供自己的實現。
模組執行器暴露了 import 方法。當 Vite 伺服器觸發 full-reload HMR 事件時,所有受影響的模組都將被重新執行。請注意,當這種情況發生時,模組執行器不會更新 exports 物件(它會覆蓋它);如果你依賴獲取最新的 exports 物件,則需要再次執行 import 或從 evaluatedModules 中重新獲取該模組。
用法示例
import {
ModuleRunner,
ESModulesEvaluator,
createNodeImportMeta,
} from 'vite/module-runner'
import { transport } from './rpc-implementation.js'
const moduleRunner = new ModuleRunner(
{
transport,
createImportMeta: createNodeImportMeta, // if the module runner runs in Node.js
},
new ESModulesEvaluator(),
)
await moduleRunner.import('/src/entry-point.js')ModuleRunnerOptions
interface ModuleRunnerOptions {
/**
* A set of methods to communicate with the server.
*/
transport: ModuleRunnerTransport
/**
* Configure how source maps are resolved.
* Prefers `node` if `process.setSourceMapsEnabled` is available.
* Otherwise it will use `prepareStackTrace` by default which overrides
* `Error.prepareStackTrace` method.
* You can provide an object to configure how file contents and
* source maps are resolved for files that were not processed by Vite.
*/
sourcemapInterceptor?:
| false
| 'node'
| 'prepareStackTrace'
| InterceptorOptions
/**
* Disable HMR or configure HMR options.
*
* @default true
*/
hmr?: boolean | ModuleRunnerHmr
/**
* Custom module cache. If not provided, it creates a separate module
* cache for each module runner instance.
*/
evaluatedModules?: EvaluatedModules
}ModuleEvaluator
型別簽名
export interface ModuleEvaluator {
/**
* Number of prefixed lines in the transformed code.
*/
startOffset?: number
/**
* Evaluate code that was transformed by Vite.
* @param context Function context
* @param code Transformed code
* @param id ID that was used to fetch the module
*/
runInlinedModule(
context: ModuleRunnerContext,
code: string,
id: string,
): Promise<any>
/**
* evaluate externalized module.
* @param file File URL to the external module
*/
runExternalModule(file: string): Promise<any>
}Vite 預設匯出了實現此介面的 ESModulesEvaluator。它使用 new AsyncFunction 來評估程式碼,因此如果程式碼具有內聯源對映(Source Map),它應該包含 2 行的偏移量,以容納所新增的新行。這是由 ESModulesEvaluator 自動處理的。自定義評估器不會新增額外行。
ModuleRunnerTransport
型別簽名
interface ModuleRunnerTransport {
connect?(handlers: ModuleRunnerTransportHandlers): Promise<void> | void
disconnect?(): Promise<void> | void
send?(data: HotPayload): Promise<void> | void
invoke?(data: HotPayload): Promise<{ result: any } | { error: any }>
timeout?: number
}用於透過 RPC 或直接函式呼叫與環境進行通訊的傳輸物件。當未實現 invoke 方法時,必須實現 send 和 connect 方法。Vite 將在內部構造 invoke。
你需要將其與伺服器上的 HotChannel 例項耦合,就像在這個在 Worker 執行緒中建立模組執行器的示例中一樣:
import { parentPort } from 'node:worker_threads'
import { fileURLToPath } from 'node:url'
import {
ESModulesEvaluator,
ModuleRunner,
createNodeImportMeta,
} from 'vite/module-runner'
/** @type {import('vite/module-runner').ModuleRunnerTransport} */
const transport = {
connect({ onMessage, onDisconnection }) {
parentPort.on('message', onMessage)
parentPort.on('close', onDisconnection)
},
send(data) {
parentPort.postMessage(data)
},
}
const runner = new ModuleRunner(
{
transport,
createImportMeta: createNodeImportMeta,
},
new ESModulesEvaluator(),
)import { BroadcastChannel } from 'node:worker_threads'
import { createServer, RemoteEnvironmentTransport, DevEnvironment } from 'vite'
function createWorkerEnvironment(name, config, context) {
const worker = new Worker('./worker.js')
const handlerToWorkerListener = new WeakMap()
const client = {
send(payload: HotPayload) {
worker.postMessage(payload)
},
}
const workerHotChannel = {
send: (data) => worker.postMessage(data),
on: (event, handler) => {
// client is already connected
if (event === 'vite:client:connect') return
if (event === 'vite:client:disconnect') {
const listener = () => {
handler(undefined, client)
}
handlerToWorkerListener.set(handler, listener)
worker.on('exit', listener)
return
}
const listener = (value) => {
if (value.type === 'custom' && value.event === event) {
handler(value.data, client)
}
}
handlerToWorkerListener.set(handler, listener)
worker.on('message', listener)
},
off: (event, handler) => {
if (event === 'vite:client:connect') return
if (event === 'vite:client:disconnect') {
const listener = handlerToWorkerListener.get(handler)
if (listener) {
worker.off('exit', listener)
handlerToWorkerListener.delete(handler)
}
return
}
const listener = handlerToWorkerListener.get(handler)
if (listener) {
worker.off('message', listener)
handlerToWorkerListener.delete(handler)
}
},
}
return new DevEnvironment(name, config, {
transport: workerHotChannel,
})
}
await createServer({
environments: {
worker: {
dev: {
createEnvironment: createWorkerEnvironment,
},
},
},
})請確保在存在 on / off 方法時實現 vite:client:connect / vite:client:disconnect 事件。當連線建立時應觸發 vite:client:connect 事件,當連線關閉時應觸發 vite:client:disconnect 事件。傳遞給事件處理程式的 HotChannelClient 物件對於同一個連線必須具有相同的引用。
另一個使用 HTTP 請求在執行器和伺服器之間通訊的示例:
import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner'
export const runner = new ModuleRunner(
{
transport: {
async invoke(data) {
const response = await fetch(`http://my-vite-server/invoke`, {
method: 'POST',
body: JSON.stringify(data),
})
return response.json()
},
},
hmr: false, // disable HMR as HMR requires transport.connect
},
new ESModulesEvaluator(),
)
await runner.import('/entry.js')在這種情況下,可以使用 NormalizedHotChannel 中的 handleInvoke 方法。
const customEnvironment = new DevEnvironment(name, config, context)
server.onRequest((request: Request) => {
const url = new URL(request.url)
if (url.pathname === '/invoke') {
const payload = (await request.json()) as HotPayload
const result = customEnvironment.hot.handleInvoke(payload)
return new Response(JSON.stringify(result))
}
return Response.error()
})但請注意,為了支援 HMR,必須實現 send 和 connect 方法。send 方法通常在觸發自定義事件時被呼叫(例如 import.meta.hot.send("my-event"))。
Vite 從主入口點匯出 createServerHotChannel,以支援 Vite SSR 期間的 HMR。
