运行时环境 API
实验性
环境 API 处于实验阶段。在 Vite 6 中,我们将保持 API 稳定,以便生态系统进行实验并在其基础上构建。我们计划在 Vite 7 中稳定这些新的 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 模块运行器允许通过首先使用 Vite 插件处理任何代码来运行它。它与 server.ssrLoadModule
不同,因为运行器实现与服务器分离。这允许库和框架作者实现它们在 Vite 服务器和运行器之间的通信层。浏览器使用服务器 Web Socket 和 HTTP 请求与其对应的环境通信。Node 模块运行器可以直接进行函数调用以处理模块,因为它在同一进程中运行。其他环境可以通过连接到 JS 运行时(如 workerd)或 Worker 线程(如 Vitest 所做的那样)来运行模块。
此功能的目标之一是提供一个可自定义的 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
}
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 } from 'vite/module-runner'
import { root, transport } from './rpc-implementation.js'
const moduleRunner = new ModuleRunner(
{
root,
transport,
},
new ESModulesEvaluator(),
)
await moduleRunner.import('/src/entry-point.js')
ModuleRunnerOptions
interface ModuleRunnerOptions {
/**
* Root of the project
*/
root: string
/**
* 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
来评估代码,因此如果代码包含内联源映射,则它应该包含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
实例耦合,例如在此示例中,模块运行器是在工作线程中创建的
import { parentPort } from 'node:worker_threads'
import { fileURLToPath } from 'node:url'
import { ESModulesEvaluator, ModuleRunner } 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(
{
root: fileURLToPath(new URL('./', import.meta.url)),
transport,
},
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 workerHotChannel = {
send: (data) => w.postMessage(data),
on: (event, handler) => {
if (event === 'connection') return
const listener = (value) => {
if (value.type === 'custom' && value.event === event) {
const client = {
send(payload) {
w.postMessage(payload)
},
}
handler(value.data, client)
}
}
handlerToWorkerListener.set(handler, listener)
w.on('message', listener)
},
off: (event, handler) => {
if (event === 'connection') return
const listener = handlerToWorkerListener.get(handler)
if (listener) {
w.off('message', listener)
handlerToWorkerListener.delete(handler)
}
},
}
return new DevEnvironment(name, config, {
transport: workerHotChannel,
})
}
await createServer({
environments: {
worker: {
dev: {
createEnvironment: createWorkerEnvironment,
},
},
},
})
另一个使用 HTTP 请求在运行器和服务器之间通信的示例
import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner'
export const runner = new ModuleRunner(
{
root: fileURLToPath(new URL('./', import.meta.url)),
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。