框架的环境 API
实验性
环境 API 是实验性的。我们仍然会在主要版本之间的 API 中保持稳定性,以允许生态系统进行实验并在此基础上构建。我们计划在未来的主要版本中稳定这些新的 API(可能会有重大更改),以便下游项目有时间尝试新功能并验证它们。
资源
请与我们分享您的反馈。
环境和框架
隐式 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
仅在首次访问时才会被延迟评估。请注意,Vite 通过调用 process.setSourceMapsEnabled
或在 Error.prepareStackTrace
不可用时覆盖它来启用源映射支持。
通过 Fetch API 与其运行时通信的框架可以利用 FetchableDevEnvironment
,该环境提供了一种通过 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('/request-to-handle'),
)
}
警告
Vite 验证 dispatchFetch
方法的输入和输出:请求必须是全局 Request
类的实例,响应必须是全局 Response
类的实例。如果不是这种情况,Vite 将抛出 TypeError
。
请注意,尽管 FetchableDevEnvironment
是作为一个类实现的,但 Vite 团队认为它是一个实现细节,并且可能随时更改。
默认 RunnableDevEnvironment
给定一个以中间件模式配置的 Vite 服务器,如 SSR 设置指南 所述,让我们使用环境 API 实现 SSR 中间件。请记住,它不必被称为 ssr
,所以在这个例子中我们将它命名为 server
。错误处理被省略。
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { createServer } from 'vite'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
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(__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)
})
运行时无关的 SSR
由于 RunnableDevEnvironment
只能用于在与 Vite 服务器相同的运行时中运行代码,因此它需要一个可以运行 Vite 服务器的运行时(与 Node.js 兼容的运行时)。这意味着您需要使用原始 DevEnvironment
使其与运行时无关。
FetchableDevEnvironment
提案
最初的提案在 DevEnvironment
类上有一个 run
方法,该方法允许使用者通过使用 transport
选项来调用运行器端的导入。在我们的测试中,我们发现 API 的通用性不足以开始推荐它。目前,我们正在寻找有关 FetchableDevEnvironment
提案 的反馈。
RunnableDevEnvironment
有一个 runner.import
函数,它返回模块的值。但是这个函数在原始 DevEnvironment
中不可用,并且需要使用 Vite 的 API 的代码和用户模块解耦。
例如,以下示例使用来自使用 Vite 的 API 的代码的用户模块的值
// code using the Vite's APIs
import { createServer } from 'vite'
const server = createServer()
const ssrEnvironment = server.environment.ssr
const input = {}
const { createHandler } = await ssrEnvironment.runner.import('./entrypoint.js')
const handler = createHandler(input)
const response = handler(new Request('/'))
// -------------------------------------
// ./entrypoint.js
export function createHandler(input) {
return function handler(req) {
return new Response('hello')
}
}
如果您的代码可以在与用户模块相同的运行时中运行(即,它不依赖于 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 RunnableDevEnvironment) {
ssrEnvironment.runner.import('virtual:entrypoint')
} else 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('/'))
// -------------------------------------
// ./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('/')
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('/'))
// -------------------------------------
// ./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
记录的顺序。框架或用户可以进一步配置如何使用构建环境
export default {
builder: {
buildApp: async (builder) => {
const environments = Object.values(builder.environments)
return Promise.all(
environments.map((environment) => builder.build(environment)),
)
},
},
}
插件还可以定义一个 buildApp
钩子。Order 'pre'
和 null
在配置的 builder.buildApp
之前执行,而 order 'post'
钩子在它之后执行。environment.isBuilt
可用于检查是否已经构建了环境。
环境无关的代码
大多数时候,当前 environment
实例将作为正在运行的代码的上下文的一部分提供,因此通过 server.environments
访问它们的需要应该很少。例如,在插件钩子中,环境作为 PluginContext
的一部分公开,因此可以使用 this.environment
访问它。请参阅 插件的环境 API 以了解如何构建环境感知插件。