使用Vite进行服务端渲染时如何避免跨请求状态污染
开发环境下跨请求状态污染的核心原因是,默认调用vite.ssrLoadModule时,所有请求会复用同一个Vite Dev Server的SSR模块缓存,模块顶层声明的可变变量会在多请求间共享,和生产环境基于node:vm做请求级沙箱隔离的逻辑不一致。Vite没有直接提供封装好的沙箱能力,但提供了对应API可以实现和生产环境对齐的隔离效果,具体方案如下:
方案1:Vite 5+ 版本使用
createViteRuntime实现请求级隔离
Vite 5新增的createViteRuntimeAPI支持为每个请求创建独立的模块运行时实例,每个实例持有独立的模块缓存、独立的执行上下文,可以直接对接node:vm沙箱,和生产环境隔离逻辑完全对齐。核心实现代码如下:import { createServer } from 'vite' import vm from 'node:vm' // 初始化Vite开发服务,仅启动一次 const viteServer = await createServer({ server: { middlewareMode: true } }) // 每个SSR请求单独执行隔离逻辑 app.use('*', async (req, res) => { // 为当前请求创建独立vm沙箱上下文,注入需要的全局变量 const vmContext = vm.createContext({ console, request: req, __SSR__: true }) // 创建当前请求专属的Vite运行时,绑定独立vm上下文 const runtime = await viteServer.createViteRuntime({ runInContext: (code, filename) => { return vm.runInContext(code, vmContext, { filename }) } }) // 加载SSR入口模块,该模块及所有依赖都在当前请求的独立沙箱内运行 const { render } = await runtime.executeEntrypoint('/src/entry-server.js') const html = await render() // 请求结束销毁runtime,释放模块缓存 runtime.destroy() res.status(200).setHeader('Content-Type', 'text/html').end(html) })该方案性能优于手动清理缓存的方案,隔离效果和生产环境完全一致,是官方推荐的实现方式。
方案2:Vite 4及以下版本兼容方案
如果暂时无法升级Vite版本,可以通过手动清理SSR模块缓存的方式实现隔离:每次请求处理前,清空入口模块及所有递归依赖的缓存,强制ssrLoadModule重新执行模块代码。参考实现:app.use('*', async (req, res) => { const moduleGraph = viteServer.moduleGraph const entryModules = moduleGraph.getModulesByFile('/src/entry-server.js') || [] const seen = new Set() // 递归清空入口及所有依赖的模块缓存 const invalidateModule = (mod) => { if (seen.has(mod.id)) return seen.add(mod.id) moduleGraph.invalidateModule(mod) mod.importedModules.forEach(invalidateModule) } entryModules.forEach(invalidateModule) // 缓存清空后重新加载入口模块 const { render } = await viteServer.ssrLoadModule('/src/entry-server.js') const html = await render() res.status(200).setHeader('Content-Type', 'text/html').end(html) })该方案存在性能损耗,每次请求都需要重新编译所有依赖模块,且无法完全隔离模块手动挂载到全局对象的副作用,仅作为低版本兼容方案使用。
通用编码约束
无论使用上述哪种方案,都需要遵循SSR基础编码规范,从根源上避免状态污染:不要在模块顶层声明可变的跨请求共享实例(比如全局状态store、路由实例、请求上下文数据等),这类实例需要封装为工厂函数,在每次请求的渲染逻辑中单独创建。错误示例(会产生跨请求污染):
// entry-server.js 顶层直接创建store实例 const appStore = createStore() export function render(req) { // 所有请求共享同一个store,后到的请求会覆盖之前写入的用户数据 appStore.setCurrentUser(req.user) return renderToString(app) }正确示例:
// 导出工厂函数,不在顶层创建可复用实例 export function createApp() { const appStore = createStore() const app = createSSRApp(App) return { app, appStore, render: (req) => { // 每个请求单独初始化store数据 appStore.setCurrentUser(req.user) return renderToString(app) } } }
内容的提问来源于stack exchange,提问作者jaran-Www

