You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Vite进行服务端渲染时如何避免跨请求状态污染

Vite SSR开发环境跨请求状态污染解决方案

开发环境下跨请求状态污染的核心原因是,默认调用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 09:33:29