框架环境 API
实验性功能
环境 API 处于实验阶段。在 Vite 6 中,我们将保持 API 的稳定性,以便生态系统进行实验并在其基础上构建。我们计划在 Vite 7 中稳定这些新的 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
时会急切地对其进行评估。请注意,如果 runner
不可用,则 Vite 会在通过调用 process.setSourceMapsEnabled
或覆盖 Error.prepareStackTrace
创建 runner
时启用源映射支持。
默认 RunnableDevEnvironment
假设 Vite 服务器以中间件模式配置,如SSR 设置指南中所述,让我们使用环境 API 实现 SSR 中间件。错误处理被省略。
import { createServer } from 'vite'
const server = 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 environment = server.environments.node
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 server.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 environment.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('./entry.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)),
)
},
},
}
与环境无关的代码
大多数情况下,当前的 environment
实例将作为正在运行的代码上下文的一部分提供,因此很少需要通过 server.environments
访问它们。例如,在插件钩子内部,环境作为 PluginContext
的一部分公开,因此可以使用 this.environment
访问它。请参阅插件的环境 API,了解如何构建环境感知插件。