跳到內容

執行環境的環境 API

實驗性

環境 API 是實驗性的。我們會在 Vite 6 期間保持 API 的穩定性,以便讓生態系統進行實驗並在其基礎上進行建構。我們計劃在 Vite 7 中穩定這些新的 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 執行環境的環境來使用

js
const ssrEnvironment = server.environments.ssr

建立新的環境工廠

Vite 開發伺服器預設會公開兩個環境:一個 client 環境和一個 ssr 環境。預設情況下,client 環境是瀏覽器環境,而模組執行器是透過將虛擬模組 /@vite/client 匯入到 client 應用程式中來實作的。預設情況下,SSR 環境在與 Vite 伺服器相同的 Node 執行環境中執行,並允許應用程式伺服器在開發期間使用完整的 HMR 支援來渲染請求。

轉換後的原始碼稱為模組,每個環境中處理的模組之間的關係會保留在模組圖中。這些模組的轉換程式碼會傳送到與每個環境相關聯的執行環境以執行。當在執行環境中評估模組時,其匯入的模組將會被請求,觸發模組圖一部分的處理。

Vite 模組執行器允許透過先使用 Vite 外掛處理程式碼來執行任何程式碼。它與 server.ssrLoadModule 不同,因為執行器實作與伺服器解耦。這允許程式庫和框架作者實作其在 Vite 伺服器和執行器之間的溝通層。瀏覽器使用伺服器 Web Socket 和透過 HTTP 請求與其對應的環境進行通訊。Node 模組執行器可以直接進行函式呼叫來處理模組,因為它是在同一程序中執行的。其他環境可以連接到像 workerd 這樣的 JS 執行環境或像 Vitest 這樣的 Worker Thread 來執行模組。

此功能的目的之一是提供可自訂的 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
}

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 } 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

ts
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

類型簽章

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 來評估程式碼,因此如果程式碼具有內嵌的來源對應,則應包含 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 方法時,則必須實作 send 方法和 connect 方法。Vite 會在內部建構 invoke

您需要將其與伺服器上的 HotChannel 實例耦合,例如在 worker 執行緒中建立模組執行器的這個範例

js
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(),
)
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 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 請求在執行器和伺服器之間進行通訊

ts
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 方法

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。

以 MIT 授權發佈。(ccee3d7c)