知海

面向框架的 Environment API

vite-mainAPI 参考

面向框架的 Environment API

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

我们计划在未来的一个主要版本中稳定这些新 API(可能会引入破坏性变更),以便下游项目有时间体验新功能并对其进行验证。

资源:

请与我们分享您的反馈。

开发环境通信级别

由于环境可能在不同的运行时中运行,因此与环境的通信可能会受到运行时的限制。为了让框架能够轻松编写与运行时无关的代码,Environment API 提供了三种通信级别。

RunnableDevEnvironment

RunnableDevEnvironment 是一种可以与您的应用程序代码通信任意 JavaScript 值的环境。导入一个模块将返回其真实的、实时的导出(函数、类实例以及任何其他值),因此框架可以直接运行其服务端入口。在开发期间,隐式的 ssr 环境和其他非客户端环境默认使用 RunnableDevEnvironment。您可以使用 isRunnableDevEnvironment 函数来保护对 runner 的访问。

它的 runner 是一个 ModuleRunner。您可以通过 runner.import(url) 从它导入模块,它会从 Vite 模块图中获取、转换并执行一个模块(url 接受相对于 root 的文件路径、服务器路径或 id),并返回带有完整 HMR 支持的实例化模块。它是 server.ssrLoadModule 的现代替代品,因此框架可以迁移到它,以便为其 SSR 开发体验启用 HMR。info 为什么它可以通信任意值
RunnableDevEnvironment 在与 Vite 服务器相同的运行时中执行模块,因此值在进程内跨边界传递,而不是被序列化。这就是它与 FetchableDevEnvironment 的区别,后者只能通过 Fetch API 上的序列化 Request / Response 对象进行通信。因此,使用 RunnableDevEnvironment 要求 runner 的运行时与 Vite 服务器运行的运行时相同。

ts 复制代码
export class RunnableDevEnvironment extends DevEnvironment {
  public readonly runner: ModuleRunner
}

class ModuleRunner {
  /**
   * 要执行的 URL。
   * 接受文件路径、服务器路径或相对于 root 的 id。
   * 返回一个实例化的模块(与 ssrLoadModule 相同)
   */
  public async import(url: string): Promise<Record<string, any>>
  /**
   * 其他 ModuleRunner 方法...
   */
}

if (isRunnableDevEnvironment(server.environments.ssr)) {
  await server.environments.ssr.runner.import('/entry-point.js')
}
```warning

runner 仅在首次访问时才会被惰性评估。请注意,当 runner 创建时,Vite 会通过调用 process.setSourceMapsEnabled 或(如果不可用)覆盖 Error.prepareStackTrace 来启用 source map 支持。

根据 SSR 搭建指南 中描述的以中间件模式配置的 Vite 服务器,让我们使用 Environment API 来实现 SSR 中间件。请记住,它不一定非要命名为 ssr,因此在此示例中我们将其命名为 server。错误处理已省略。

js 复制代码
import fs from 'node:fs'
import path from 'node:path'
import { createServer } from 'vite'

const viteServer = await createServer({
  server: { middlewareMode: true },
  appType: 'custom',
  environments: {
    server: {
      // 默认情况下,模块在与 vite 服务器相同的进程中运行
    },
  },
})

// 在 TypeScript 中,您可能需要将其转换为 RunnableDevEnvironment
// 或使用 isRunnableDevEnvironment 来保护对 runner 的访问
const serverEnvironment = viteServer.environments.server

app.use('*', async (req, res, next) => {
  const url = req.originalUrl

  // 1. 读取 index.html
  const indexHtmlPath = path.resolve(import.meta.dirname, 'index.html')
  let template = fs.readFileSync(indexHtmlPath, 'utf-8')

  // 2. 应用 Vite HTML 转换。这会注入 Vite HMR 客户端,
  //    并应用来自 Vite 插件的 HTML 转换,例如来自
  //    @vitejs/plugin-react 的全局 preamble
  template = await viteServer.transformIndexHtml(url, template)

  // 3. 加载服务端入口。import(url) 会自动将
  //    ESM 源代码转换为可在 Node.js 中使用!无需打包,
  //    并提供完整的 HMR 支持。
  const { render } = await serverEnvironment.runner.import(
    '/src/entry-server.js',
  )

  // 4. 渲染应用 HTML。这假设 entry-server.js 导出的
  //    `render` 函数调用适当的框架 SSR API,
  //    例如 ReactDOMServer.renderToString()
  const appHtml = await render(url)

  // 5. 将应用渲染的 HTML 注入到模板中。
  const html = template.replace(`<!--ssr-outlet-->`, appHtml)

  // 6. 将渲染后的 HTML 发送回客户端。
  res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
})

当使用支持 HMR 的环境(例如 RunnableDevEnvironment)时,您应该在服务端入口文件中添加 import.meta.hot.accept() 以获得最佳行为。否则,服务端文件更改将使整个服务端模块图失效:

js 复制代码
// src/entry-server.js
export function render(...) { ... }

if (import.meta.hot) {
  import.meta.hot.accept()
}

FetchableDevEnvironmentinfo

我们正在寻求关于 the FetchableDevEnvironment proposal 的反馈。

FetchableDevEnvironment 是一种可以通过 Fetch API 接口与其运行时通信的环境。由于 RunnableDevEnvironment 只能在一组有限的运行时中实现,我们建议使用 FetchableDevEnvironment 而不是 RunnableDevEnvironment

使用它的一个常见原因是,框架希望支持无法直接运行 Vite 的运行时(例如 Cloudflare Workers)。RunnableDevEnvironment 不能用于那里,因为它要求 runner 共享 Vite 服务器的运行时,以便值可以在进程内跨边界传递。标准化 Fetch API 可以让框架在其所有目标运行时中保持单一的请求处理路径:其开发中间件将每个传入的浏览器请求作为 Request 转发,并将返回的 Response 发送回浏览器,这镜像了应用在生产环境中处理请求的方式。

该环境通过 handleRequest 方法提供了一种标准化的请求处理方式:

ts 复制代码
import {
  createServer,
  createFetchableDevEnvironment,
  isFetchableDevEnvironment,
} from 'vite'

const server = await createServer({
  server: { middlewareMode: true },
  appType: 'custom',
  environments: {
    custom: {
      dev: {
        createEnvironment(name, config) {
          return createFetchableDevEnvironment(name, config, {
            handleRequest(request: Request): Promise<Response> | Response {
              // 处理 Request 并返回一个 Response
            },
          })
        },
      },
    },
  },
})

// 任何 Environment API 的使用者现在都可以调用 `dispatchFetch`
if (isFetchableDevEnvironment(server.environments.custom)) {
  const response: Response = await server.environments.custom.dispatchFetch(
    new Request('http://example.com/request-to-handle'),
  )
}
```warning

Vite 会验证 dispatchFetch 方法的输入和输出:请求必须是全局 Request 类的实例,响应必须是全局 Response 类的实例。如果不是这种情况,Vite 将抛出 TypeError

请注意,尽管 FetchableDevEnvironment 是作为一个类实现的,但 Vite 团队将其视为实现细节,可能随时更改。
:::

原始的 DevEnvironment

如果环境未实现 RunnableDevEnvironmentFetchableDevEnvironment 接口,您需要手动设置通信。

如果您的代码可以在与用户模块相同的运行时中运行(即,它不依赖于 Node.js 特定的 API),则可以使用虚拟模块。这种方法无需通过 Vite 的 API 访问代码中的值。

ts 复制代码
// 使用 Vite API 的代码
import { createServer } from 'vite'

const server = createServer({
  plugins: [
    // 一个处理 `virtual:entrypoint` 的插件
    {
      name: 'virtual-module',
      /* 插件实现 */
    },
  ],
})
const ssrEnvironment = server.environment.ssr
const input = {}

// 使用每个环境工厂运行代码所暴露的函数
// 检查每个环境工厂提供了什么
if (ssrEnvironment instanceof CustomDevEnvironment) {
  ssrEnvironment.runEntrypoint('virtual:entrypoint')
} else {
  throw new Error(`Unsupported runtime for ${ssrEnvironment.name}`)
}

// -------------------------------------
// virtual:entrypoint
const { createHandler } = await import('./entrypoint.js')
const handler = createHandler(input)
const response = handler(new Request('http://example.com/'))

// -------------------------------------
// ./entrypoint.js
export function createHandler(input) {
  return function handler(req) {
    return new Response('hello')
  }
}

例如,要在用户模块上调用 transformIndexHtml,可以使用以下插件:

ts {13-21} 复制代码
function vitePluginVirtualIndexHtml(): Plugin {
  let server: ViteDevServer | undefined
  return {
    name: vitePluginVirtualIndexHtml.name,
    configureServer(server_) {
      server = server_
    },
    resolveId(source) {
      return source === 'virtual:index-html' ? '\0' + source : undefined
    },
    async load(id) {
      if (id === '\0' + 'virtual:index-html') {
        let html: string
        if (server) {
          this.addWatchFile('index.html')
          html = fs.readFileSync('index.html', 'utf-8')
          html = await server.transformIndexHtml('/', html)
        } else {
          html = fs.readFileSync('dist/client/index.html', 'utf-8')
        }
        return `export default ${JSON.stringify(html)}`
      }
      return
    },
  }
}

如果您的代码需要 Node.js API,您可以使用 hot.send 与用户模块中使用 Vite API 的代码进行通信。但请注意,这种方法在构建过程之后可能无法以相同的方式工作。

ts 复制代码
// 使用 Vite API 的代码
import { createServer } from 'vite'

const server = createServer({
  plugins: [
    // 一个处理 `virtual:entrypoint` 的插件
    {
      name: 'virtual-module',
      /* 插件实现 */
    },
  ],
})
const ssrEnvironment = server.environment.ssr
const input = {}

// 使用每个环境工厂运行代码所暴露的函数
// 检查每个环境工厂提供了什么
if (ssrEnvironment instanceof RunnableDevEnvironment) {
  ssrEnvironment.runner.import('virtual:entrypoint')
} else if (ssrEnvironment instanceof CustomDevEnvironment) {
  ssrEnvironment.runEntrypoint('virtual:entrypoint')
} else {
  throw new Error(`Unsupported runtime for ${ssrEnvironment.name}`)
}

const req = new Request('http://example.com/')

const uniqueId = 'a-unique-id'
ssrEnvironment.send('request', serialize({ req, uniqueId }))
const response = await new Promise((resolve) => {
  ssrEnvironment.on('response', (data) => {
    data = deserialize(data)
    if (data.uniqueId === uniqueId) {
      resolve(data.res)
    }
  })
})

// -------------------------------------
// virtual:entrypoint
const { createHandler } = await import('./entrypoint.js')
const handler = createHandler(input)

import.meta.hot.on('request', (data) => {
  const { req, uniqueId } = deserialize(data)
  const res = handler(req)
  import.meta.hot.send('response', serialize({ res: res, uniqueId }))
})

const response = handler(new Request('http://example.com/'))

// -------------------------------------
// ./entrypoint.js
export function createHandler(input) {
  return function handler(req) {
    return new Response('hello')
  }
}

构建期间的环境

在 CLI 中,调用 vite buildvite build --ssr 仍将仅为向后兼容性构建仅客户端和仅 SSR 的环境。

当设置了 builder 选项(即使设置为空对象 {},这也是 vite build --app 所做的)时,vite build 将选择构建整个应用。这将在未来的一个主要版本中成为默认行为。在此模式下,Vite 会创建一个 ViteBuilder 实例(ViteDevServer 的构建时等效物),并使用它为生产环境构建所有配置的环境。默认情况下,环境按照 environments 记录的顺序串行构建。

使用 builder.buildApp 配置应用构建

框架或用户可以通过 builder.buildApp 选项控制环境的构建方式。它接收 ViteBuilder 实例(在下面的示例中命名为 builder),并负责构建每个环境;例如,并行构建其中一些:

js [vite.config.js] 复制代码
import { defineConfig } from 'vite'

export default defineConfig({
  builder: {
    buildApp: async (builder) => {
      const environments = Object.values(builder.environments)
      await Promise.all(
        environments.map((environment) => builder.build(environment)),
      )
    },
  },
})

buildApp 插件钩子

  • 类型: (this: MinimalPluginContextWithoutEnvironment, builder: ViteBuilder) => Promise<void>
  • 类型: asyncsequential
  • 作用域: 全局

除了 builder.buildApp 配置选项外,插件还可以定义 buildApp 钩子来参与应用构建。配置选项和插件钩子按定义顺序运行:具有 'pre'null 顺序的钩子首先运行,然后是配置的 builder.buildApp,最后是具有 'post' 顺序的钩子。在钩子内部,environment.isBuilt 告知某个环境是否已经构建,这可以让插件避免重复构建它。

使用 createBuilder 以编程方式构建

要从您自己的代码触发应用构建,请使用 createBuilder 而不是独立的 build 函数。createBuildercreateServer 的构建时等效物:它解析配置并返回一个 ViteBuilder,其 buildApp 方法构建每个配置的环境。您也可以使用 builder.build(environment) 构建单个环境。

js [build.js] 复制代码
import { createBuilder } from 'vite'

const builder = await createBuilder()
await builder.buildApp()

对于环境感知的构建,createBuilder 取代了独立的 build 函数。build 仍然作为上述传统仅客户端和仅 SSR 构建的简单入口点,但它无法构建任意环境。运行 builder.buildApp()vite build --app 的编程等效方式。

环境无关代码

大多数情况下,当前的 environment 实例将作为正在运行的代码上下文的一部分可用,因此很少需要通过 server.environments 访问它们。例如,在插件钩子内部,环境作为 PluginContext 的一部分暴露,因此可以使用 this.environment 访问。请参阅 面向插件的 Environment API 以了解如何构建环境感知插件。

帮助我们改进文档

发现翻译问题或内容错误?请告诉我们。