跳转到内容

运行时环境 API

候选发布版本 (Release Candidate)

环境 API 目前处于候选发布阶段。我们将在主要版本之间保持 API 的稳定性,以便生态系统能够进行实验和构建。但请注意,某些特定 API 仍被视为实验性的。

一旦下游项目有时间试验这些新功能并完成验证,我们计划在未来的主要版本中稳定这些新 API(可能会有破坏性变更)。

资源

请与我们分享您的反馈。

环境工厂

环境工厂旨在由 Cloudflare 等环境提供商实现,而非最终用户。对于开发和构建环境都使用目标运行时的最常见情况,环境工厂会返回一个 EnvironmentOptions。同时也可以设置默认环境选项,以便用户无需手动配置。

ts
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,
  )
}

随后,配置文件可以写成:

js
import { createWorkerdEnvironment } from 'vite-environment-workerd'

export default {
  environments: {
    ssr: createWorkerdEnvironment({
      build: {
        outDir: '/dist/ssr',
      },
    }),
    rsc: createWorkerdEnvironment({
      build: {
        outDir: '/dist/rsc',
      },
    }),
  },
}

框架可以使用 workerd 运行时环境,通过以下方式进行 SSR:

js
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 线程来运行模块。

module_runner 构建工具cluster_server 构建工具Vite Dev Server (Node.js)cluster_env 构建工具DevEnvironmentcluster_runtime 构建工具Target Runtimecluster_runner 构建工具ModuleRunnerplugins 构建工具PluginPipelinemg 构建工具ModuleGraphplugins->mg 构建工具hot 构建工具HotChanneltransport 构建工具Transporthot->transport 构建工具HMR / Modulefetch & invokeevaluator 构建工具ModuleEvaluator
module_runner 构建工具cluster_server 构建工具Vite Dev Server (Node.js)cluster_env 构建工具DevEnvironmentcluster_runtime 构建工具Target Runtimecluster_runner 构建工具ModuleRunnerplugins 构建工具PluginPipelinemg 构建工具ModuleGraphplugins->mg 构建工具hot 构建工具HotChanneltransport 构建工具Transporthot->transport 构建工具HMR / Modulefetch & invokeevaluator 构建工具ModuleEvaluator

此功能的目标之一是提供一个可定制的 API 来处理和运行代码。用户可以使用暴露的原语创建新的环境工厂。

ts
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 导入。此导出入口点尽可能保持轻量级,仅导出创建模块运行器所需的最小集合。

类型签名

ts
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 中重新获取该模块。

用法示例

js
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

ts
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

类型签名

ts
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

类型签名

ts
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 方法时,必须实现 sendconnect 方法。Vite 将在内部构造 invoke

你需要将其与服务器上的 HotChannel 实例耦合,就像在这个在 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
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 请求在运行器和服务器之间通信的示例:

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, // disable HMR as HMR requires 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,必须实现 sendconnect 方法。send 方法通常在触发自定义事件时被调用(例如 import.meta.hot.send("my-event"))。

Vite 从主入口点导出 createServerHotChannel,以支持 Vite SSR 期间的 HMR。