面向运行时的 Environment API
面向运行时的 Environment API
:::info 发布候选状态
Environment API 总体上处于发布候选阶段。我们将在主要版本之间保持 API 的稳定性,以便生态系统能够进行实验并在此基础上构建。但请注意,一些特定 API 仍被视为实验性。
我们计划在下游项目有足够时间体验新特性并验证它们之后,在未来的主要版本中稳定这些新 API(可能包含破坏性变更)。
资源:
- 反馈讨论:我们正在此收集关于新 API 的反馈。
- Environment API PR:新 API 在此实现并进行了评审。
欢迎分享您的反馈。
本页面面向运行时提供者,即那些将 JavaScript 运行时与 Vite 集成的作者。这里的运行时是转换后代码执行的 JavaScript 引擎,例如 Node.js、浏览器、Cloudflare 的 workerd 或 Worker 线程。运行时提供者会为其中一种运行时打包集成方案,这样框架作者和最终用户(构建应用的开发者)就无需自行配置。
环境工厂
环境工厂旨在由运行时提供者实现,而非最终用户。环境工厂为目标运行时在开发和生产环境中最常见的用例返回一个
EnvironmentOptions。也可以设置默认的环境选项,这样用户无需自行配置。
tsfunction 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, ) }然后配置文件可以这样写:
jsimport { createWorkerdEnvironment } from 'vite-environment-workerd' export default { environments: { ssr: createWorkerdEnvironment({ build: { outDir: '/dist/ssr', }, }), rsc: createWorkerdEnvironment({ build: { outDir: '/dist/rsc', }, }), }, }框架可以使用带有 workerd 运行时的环境来进行 SSR:
jsconst ssrEnvironment = server.environments.ssr创建新的环境工厂
Vite 开发服务器默认暴露两个环境:
client环境和ssr环境。默认情况下,client 环境是浏览器环境,其模块运行器通过向客户端应用导入虚拟模块/@vite/client来实现。SSR 环境默认与 Vite 服务器运行在同一个 Node.js 运行时中,并允许在开发期间使用应用服务器渲染请求,并支持完整的 HMR。转换后的源代码称为模块,每个环境中处理的模块之间的关系保存在模块图中。这些模块的转换后代码被发送到与每个环境关联的运行时中执行。当模块在运行时中被求值时,其导入的模块将被请求,从而触发模块图的一部分处理。
Vite 模块运行器允许先通过 Vite 插件处理代码,然后运行任何代码。它与
server.ssrLoadModule不同,因为运行器的实现与服务器解耦。这使得库和框架作者可以实现 Vite 服务器与运行器之间的通信层。浏览器通过服务器的 WebSocket 和 HTTP 请求与其对应的环境通信。Node 模块运行器可以直接进行函数调用来处理模块,因为它运行在同一进程中。其他环境可以连接到 JavaScript 运行时(如 workerd)或像 Vitest 那样使用 Worker 线程来运行模块。
dotdigraph module_runner { rankdir=LR node [shape=box style="rounded,filled" fontname="Arial" fontsize=11 margin="0.2,0.1" fontcolor="${#3c3c43|#ffffff}" color="${#c2c2c4|#3c3f44}"] edge [color="${#67676c|#98989f}" fontname="Arial" fontsize=10 fontcolor="${#67676c|#98989f}"] bgcolor="transparent" compound=true subgraph cluster_server { label="Vite Dev Server (Node.js)" labeljust=l fontname="Arial" fontsize=12 style="rounded,filled" fillcolor="${#f6f6f7|#1a1a1f}" color="${#c2c2c4|#3c3f44}" fontcolor="${#3c3c43|#ffffff}" subgraph cluster_env { label="DevEnvironment" labeljust=l fontname="Arial" fontsize=11 style="rounded,filled" fillcolor="${#f2ecfc|#2c273e}" color="${#c2c2c4|#3c3f44}" fontcolor="${#3c3c43|#ffffff}" plugins [label="Plugin\nPipeline" fillcolor="${#e9eaff|#222541}"] mg [label="Module\nGraph" fillcolor="${#e9eaff|#222541}"] hot [label="HotChannel" fillcolor="${#fcf4dc|#38301a}"] plugins -> mg [dir=both] mg -> hot [style=invis] } } subgraph cluster_runtime { label="Target Runtime" labeljust=l fontname="Arial" fontsize=12 style="rounded,filled" fillcolor="${#f0fdf4|#131b15}" color="${#c2c2c4|#3c3f44}" fontcolor="${#3c3c43|#ffffff}" subgraph cluster_runner { label="ModuleRunner" labeljust=l fontname="Arial" fontsize=11 style="rounded,filled" fillcolor="${#def5ed|#15312d}" color="${#c2c2c4|#3c3f44}" fontcolor="${#3c3c43|#ffffff}" evaluator [label="Module\nEvaluator" fillcolor="${#def5ed|#15312d}"] transport [label="Transport" fillcolor="${#fcf4dc|#38301a}"] } } hot -> transport [label="HMR / Module\nfetch & invoke" dir=both style=bold color="${#6f42c1|#c8abfa}"] }此功能的目标之一是提供可自定义的 API 来处理和运行代码。用户可以使用暴露的原始构件创建新的环境工厂。
tsimport { 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 }默认情况下,
HotChannel传输会应用server.fs限制,这意味着只有允许目录内的文件才能被提供。如果您的传输不通过网络暴露(例如,它通过 Worker 线程或进程内调用进行通信),您可以在HotChannel上设置skipFsCheck: true来绕过这些限制。
DevEnvironment有多个通信级别。为了使框架更容易编写与运行时无关的代码,我们建议实现最灵活的通信级别。
ModuleRunner模块运行器在目标运行时中实例化。除非另有说明,下一节中的所有 API 都是从
vite/module-runner导入的。这个导出入口点尽可能保持轻量,只导出创建模块运行器所需的最小内容。类型签名:
tsexport class ModuleRunner { constructor( public options: ModuleRunnerOptions, public evaluator: ModuleEvaluator = new ESModulesEvaluator(), private debug?: ModuleRunnerDebugger, ) {} /** * 要执行的 URL。 * 接受文件路径、服务器路径或相对于根目录的 id。 */ public async import<T = any>(url: string): Promise<T> /** * 清除所有缓存,包括 HMR 监听器。 */ public clearCache(): void /** * 清除所有缓存,移除所有 HMR 监听器,重置 sourcemap 支持。 * 此方法不会停止 HMR 连接。 */ public async close(): Promise<void> /** * 如果已通过调用 `close()` 关闭运行器,则返回 `true`。 */ public isClosed(): boolean }
ModuleRunner中的模块求值器负责执行代码。Vite 默认导出ESModulesEvaluator,它使用new AsyncFunction来求值代码。如果你的 JavaScript 运行时不支持不安全求值,你可以提供自己的实现。模块运行器暴露
import方法。当 Vite 服务器触发full-reloadHMR 事件时,所有受影响的模块将被重新执行。请注意,模块运行器在这种情况下不会更新exports对象(而是覆盖它),如果你依赖最新的exports对象,你需要再次运行import或从evaluatedModules获取模块。示例用法:
jsimport { ModuleRunner, ESModulesEvaluator, createNodeImportMeta, } from 'vite/module-runner' import { transport } from './rpc-implementation.js' const moduleRunner = new ModuleRunner( { transport, createImportMeta: createNodeImportMeta, // 如果模块运行器在 Node.js 中运行 }, new ESModulesEvaluator(), ) await moduleRunner.import('/src/entry-point.js')
ModuleRunnerOptions
ts twoslashimport type { InterceptorOptions as InterceptorOptionsRaw, ModuleRunnerHmr as ModuleRunnerHmrRaw, EvaluatedModules, } from 'vite/module-runner' import type { Debug } from '@type-challenges/utils' type InterceptorOptions = Debug<InterceptorOptionsRaw> type ModuleRunnerHmr = Debug<ModuleRunnerHmrRaw> /** 见下文 */ type ModuleRunnerTransport = unknown // ---cut--- interface ModuleRunnerOptions { /** * 与服务器通信的一组方法。 */ transport: ModuleRunnerTransport /** * 配置如何解析 source map。 * 如果 `process.setSourceMapsEnabled` 可用,则优先使用 `node`。 * 否则,默认使用 `prepareStackTrace`,这会覆盖 * `Error.prepareStackTrace` 方法。 * 你可以提供对象来配置对于未由 Vite 处理的文件如何解析文件内容和 source map。 */ sourcemapInterceptor?: false | 'node' | 'prepareStackTrace' | InterceptorOptions /** * 禁用 HMR 或配置 HMR 选项。 * * @default true */ hmr?: boolean | ModuleRunnerHmr /** * 自定义模块缓存。如果未提供,则每个模块运行器实例会创建单独的模块缓存。 */ evaluatedModules?: EvaluatedModules }
ModuleEvaluator类型签名:
ts twoslashimport type { ModuleRunnerContext as ModuleRunnerContextRaw } from 'vite/module-runner' import type { Debug } from '@type-challenges/utils' type ModuleRunnerContext = Debug<ModuleRunnerContextRaw> // ---cut--- export interface ModuleEvaluator { /** * 转换后代码中前置的行数。 */ startOffset?: number /** * 求值由 Vite 转换的代码。 * @param context 函数上下文 * @param code 转换后的代码 * @param id 用于获取模块的 ID */ runInlinedModule( context: ModuleRunnerContext, code: string, id: string, ): Promise<any> /** * 求值外部化模块。 * @param file 外部模块的文件 URL */ runExternalModule(file: string): Promise<any> }Vite 默认导出实现此接口的
ESModulesEvaluator。它使用new AsyncFunction来求值代码,因此如果代码包含内联 source map,它应该包含 2 行的偏移量,以容纳添加的新行。ESModulesEvaluator会自动完成此操作。自定义求值器不会添加额外的行。
ModuleRunnerTransport类型签名:
ts twoslashimport type { ModuleRunnerTransportHandlers } from 'vite/module-runner' /** 一个对象 */ type HotPayload = unknown // ---cut--- 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 线程中创建: code-group
js [worker.js]
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(),
)
js [server.js]
import { BroadcastChannel } from 'node:worker_threads'
import { createServer, 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 = {
// Worker 线程的 postMessage 不通过网络暴露,跳过 server.fs 检查
skipFsCheck: true,
send: (data) => worker.postMessage(data),
on: (event, handler) => {
// client 已连接
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 请求在运行器与服务器之间通信:
ts
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, // 禁用 HMR,因为 HMR 需要 transport.connect
},
new ESModulesEvaluator(),
)
await runner.import('/entry.js')
在这种情况下,可以使用 NormalizedHotChannel 中的 handleInvoke 方法:
ts
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 服务器运行在同一个 Node.js 进程中的 SSR 环境,Vite 导出了现成的 HotChannel:createServerHotChannel。
js
import { createServerHotChannel, DevEnvironment } from 'vite'
new DevEnvironment(name, config, {
hot: true,
transport: createServerHotChannel(),
})
帮助我们改进文档
发现翻译问题或内容错误?请告诉我们。
