框架环境 API
候选发布版本 (Release Candidate)
环境 API 目前处于候选发布阶段。我们将在主要版本之间保持 API 的稳定性,以便生态系统能够进行实验和构建。但请注意,某些特定 API 仍被视为实验性的。
一旦下游项目有时间试验这些新功能并完成验证,我们计划在未来的主要版本中稳定这些新 API(可能会有破坏性变更)。
资源
请与我们分享您的反馈。
DevEnvironment 通信级别
由于环境可能运行在不同的运行时中,因此针对环境的通信可能会受到运行时限制。为了允许框架轻松编写与运行时无关的代码,环境 API 提供了三种通信级别。
RunnableDevEnvironment
RunnableDevEnvironment 是一个可以传输任意值的环境。隐式的 ssr 环境和其他非客户端环境在开发过程中默认使用 RunnableDevEnvironment。虽然这要求运行时必须与 Vite 服务器运行的运行时相同,但其工作方式与 ssrLoadModule 类似,并允许框架迁移并为其 SSR 开发场景启用 HMR。您可以使用 isRunnableDevEnvironment 函数来守卫任何可运行的环境。
export class RunnableDevEnvironment extends DevEnvironment {
public readonly runner: ModuleRunner
}
class ModuleRunner {
/**
* URL to execute.
* Accepts file path, server path, or id relative to the root.
* Returns an instantiated module (same as in ssrLoadModule)
*/
public async import(url: string): Promise<Record<string, any>>
/**
* Other ModuleRunner methods...
*/
}
if (isRunnableDevEnvironment(server.environments.ssr)) {
await server.environments.ssr.runner.import('/entry-point.js')
}警告
runner 仅在首次访问时才会惰性求值。请注意,当通过调用 process.setSourceMapsEnabled 或在不可用时覆盖 Error.prepareStackTrace 来创建 runner 时,Vite 会启用源映射(source map)支持。
在使用 SSR 设置指南中描述的中间件模式配置 Vite 服务器的情况下,让我们使用环境 API 实现 SSR 中间件。请记住,它不必命名为 ssr,因此在本示例中我们将其命名为 server。此处省略了错误处理。
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: {
// by default, modules are run in the same process as the vite server
},
},
})
// You might need to cast this to RunnableDevEnvironment in TypeScript or
// use isRunnableDevEnvironment to guard the access to the runner
const serverEnvironment = viteServer.environments.server
app.use('*', async (req, res, next) => {
const url = req.originalUrl
// 1. Read index.html
const indexHtmlPath = path.resolve(import.meta.dirname, 'index.html')
let template = fs.readFileSync(indexHtmlPath, 'utf-8')
// 2. Apply Vite HTML transforms. This injects the Vite HMR client,
// and also applies HTML transforms from Vite plugins, e.g. global
// preambles from @vitejs/plugin-react
template = await viteServer.transformIndexHtml(url, template)
// 3. Load the server entry. import(url) automatically transforms
// ESM source code to be usable in Node.js! There is no bundling
// required, and provides full HMR support.
const { render } = await serverEnvironment.runner.import(
'/src/entry-server.js',
)
// 4. render the app HTML. This assumes entry-server.js's exported
// `render` function calls appropriate framework SSR APIs,
// e.g. ReactDOMServer.renderToString()
const appHtml = await render(url)
// 5. Inject the app-rendered HTML into the template.
const html = template.replace(`<!--ssr-outlet-->`, appHtml)
// 6. Send the rendered HTML back.
res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
})当使用支持 HMR 的环境(例如 RunnableDevEnvironment)时,您应该在服务器入口文件中添加 import.meta.hot.accept() 以获得最佳行为。否则,服务器文件的变更将使整个服务器模块图失效。
// src/entry-server.js
export function render(...) { ... }
if (import.meta.hot) {
import.meta.hot.accept()
}FetchableDevEnvironment
信息
我们正在征求关于 FetchableDevEnvironment 提案的反馈。
FetchableDevEnvironment 是一个可以通过 Fetch API 接口与其运行时进行通信的环境。由于 RunnableDevEnvironment 只能在有限的运行时集合中实现,我们建议使用 FetchableDevEnvironment 代替 RunnableDevEnvironment。
该环境提供了一种通过 handleRequest 方法处理请求的标准化方式。
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 {
// handle Request and return a Response
},
})
},
},
},
},
})
// Any consumer of the environment API can now call `dispatchFetch`
if (isFetchableDevEnvironment(server.environments.custom)) {
const response: Response = await server.environments.custom.dispatchFetch(
new Request('http://example.com/request-to-handle'),
)
}警告
Vite 会验证 dispatchFetch 方法的输入和输出:请求必须是全局 Request 类的实例,响应必须是全局 Response 类的实例。如果不是这种情况,Vite 将抛出 TypeError。
请注意,尽管 FetchableDevEnvironment 是作为类实现的,但它被 Vite 团队视为实现细节,随时可能发生变化。
原始 DevEnvironment
如果环境没有实现 RunnableDevEnvironment 或 FetchableDevEnvironment 接口,则需要手动设置通信。
如果您的代码可以在与用户模块相同的运行时中运行(即它不依赖于 Node.js 特定 API),则可以使用虚拟模块。这种方法消除了使用 Vite API 从代码访问值的需要。
// code using the Vite's APIs
import { createServer } from 'vite'
const server = createServer({
plugins: [
// a plugin that handles `virtual:entrypoint`
{
name: 'virtual-module',
/* plugin implementation */
},
],
})
const ssrEnvironment = server.environment.ssr
const input = {}
// use exposed functions by each environment factories that runs the code
// check for each environment factories what they provide
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,可以使用以下插件:
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 的用户模块代码进行通信。但请注意,这种方法在构建过程之后可能无法以相同方式工作。
// code using the Vite's APIs
import { createServer } from 'vite'
const server = createServer({
plugins: [
// a plugin that handles `virtual:entrypoint`
{
name: 'virtual-module',
/* plugin implementation */
},
],
})
const ssrEnvironment = server.environment.ssr
const input = {}
// use exposed functions by each environment factories that runs the code
// check for each environment factories what they provide
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 选项不为 undefined 时(或调用 vite build --app 时),vite build 将选择构建整个应用程序。这在未来的大版本更新中将成为默认行为。届时将创建一个 ViteBuilder 实例(构建时等同于 ViteDevServer)来为生产环境构建所有已配置的环境。默认情况下,环境构建按 environments 记录的顺序串行运行。框架或用户可以使用 builder.buildApp 选项进一步配置环境的构建方式。
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 钩子。顺序为 'pre' 和 null 的钩子会在配置的 builder.buildApp 之前执行,顺序为 'post' 的钩子会在其之后执行。可以使用 environment.isBuilt 来检查环境是否已经完成构建。
与环境无关的代码
大多数情况下,当前 environment 实例将作为所运行代码上下文的一部分可用,因此通过 server.environments 访问它们的需求很少见。例如,在插件钩子内部,环境被暴露为 PluginContext 的一部分,因此可以使用 this.environment 进行访问。请参阅 插件环境 API 以了解如何构建环境感知插件。
