面向框架的 Environment API
面向框架的 Environment API
:::info 发布候选版本
Environment API 目前总体上处于发布候选阶段。我们将在主要版本之间保持 API 的稳定性,以便生态系统能够基于其进行实验和构建。但请注意,某些特定 API 仍被视为实验性。
我们计划在未来的一个主要版本中稳定这些新 API(可能会引入破坏性变更),以便下游项目有时间体验新功能并对其进行验证。
资源:
- 反馈讨论 我们正在这里收集关于新 API 的反馈。
- Environment API PR 新 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 服务器运行的运行时相同。
tsexport 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。错误处理已省略。
jsimport 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方法提供了一种标准化的请求处理方式:
tsimport { 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
如果环境未实现 RunnableDevEnvironment 或 FetchableDevEnvironment 接口,您需要手动设置通信。
如果您的代码可以在与用户模块相同的运行时中运行(即,它不依赖于 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 build 和 vite 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> - 类型:
async,sequential - 作用域: 全局
除了 builder.buildApp 配置选项外,插件还可以定义 buildApp 钩子来参与应用构建。配置选项和插件钩子按定义顺序运行:具有 'pre' 或 null 顺序的钩子首先运行,然后是配置的 builder.buildApp,最后是具有 'post' 顺序的钩子。在钩子内部,environment.isBuilt 告知某个环境是否已经构建,这可以让插件避免重复构建它。
使用 createBuilder 以编程方式构建
要从您自己的代码触发应用构建,请使用 createBuilder 而不是独立的 build 函数。createBuilder 是 createServer 的构建时等效物:它解析配置并返回一个 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 以了解如何构建环境感知插件。
帮助我们改进文档
发现翻译问题或内容错误?请告诉我们。
