微前端沙盒插件,老项目接入微前端,css/js隔离怎么做?

154 阅读8分钟

插件介绍

在微前端架构落地过程中,“隔离” 与 “兼容” 始终是核心痛点 —— 不同技术栈(Vue2/Vue3/React)、不同构建工具(Webpack/Vite)的应用共存时,常面临全局变量污染、样式穿透、路由冲突等问题,而传统沙箱方案要么接入成本高,要么兼容不足(如不支持 Vite 生态的 ES Module),要么隔离不彻底(如组件库弹窗样式丢失)。

「Vite 微前端沙盒插件」专为 Vite 构建体系设计,核心目标是在不侵入业务代码的前提下,实现 “轻量隔离 + 无缝协作” :通过 AST 语法转换与编译时处理,为每个子应用打造独立的 JS 执行上下文与 CSS 作用域,同时兼容 ES Module 特性、第三方 UI 库(如 Element Plus、Ant Design),让多应用像 “搭积木” 一样嵌入主应用,既互不干扰,又能顺畅通信。

插件采用 “构建时转换 + 运行时代理” 的混合方案,无需修改业务逻辑,仅需简单配置即可接入,完美解决 Vite 微前端项目的隔离难题,同时兼顾性能与兼容性。

核心目标

  1. JS 隔离:安全无污染,兼容 Vite 生态

    • 实现全局变量(window/globalThis)访问重定向,每个应用通过独立代理窗口运行,同名变量、方法互不干扰,避免 “全局污染” 与 “变量覆盖” 问题。
    • 兼容 Vite 原生 ES Module 特性,支持动态导入、第三方库按需加载,不牺牲 Vite 的构建与运行性能。
    • 精准控制隔离范围:仅处理全局对象访问,不修改业务自定义变量与函数,避免执行时序问题与 TDZ(暂时性死区)风险。
    • 事件生命周期绑定:全局事件(addEventListener/removeEventListener)与应用上下文关联,便于调试与审计,防止事件混用导致的异常。
  2. CSS 隔离:彻底不穿透,适配组件库

    • 通过编译时添加选择器前缀(默认 :where(.<appCode>)),将每个应用的样式禁锢在独立作用域,不影响宿主与其他子应用样式。
    • 针对性解决 UI 库弹窗 / 下拉框(如 Element Plus 的 ElDialog)直接插入 body 导致的样式丢失问题,自动为这类元素添加双重命名空间,确保样式生效。
    • 兼容 CSS/SCSS/LESS 等预处理语言,支持伪类、伪元素、@keyframes 等复杂语法,不提升选择器权重,避免样式优先级冲突。
    • 可配置包含 / 排除规则,灵活控制需要隔离的样式范围,平衡隔离效果与构建性能。
  3. 零侵入接入:低成本落地,无业务改造

    • 插件化集成,仅需在 Vite 配置中添加依赖与应用唯一标识(appCode),无需修改业务代码、路由配置或项目结构。
    • 保持原生行为一致性:隔离逻辑不改变浏览器原生 API 行为,子应用开发体验与独立项目一致,降低团队学习成本。
    • 多场景兼容:支持单应用独占、多应用同时渲染等场景,适配小程序 / H5/App 多端微前端架构,无额外适配成本。
  4. 无缝协作:隔离不割裂,通信更顺畅

    • 隔离不阻断应用间通信:代理窗口可按需透传宿主全局变量与方法,支持主 - 子应用、子应用间的标准化通信。
    • 路由协同:兼容主流微前端路由方案(如 Single-spa 路由激活),不破坏浏览器前进 / 后退功能,路由状态同步无异常。
    • 第三方依赖复用:支持子应用共享宿主第三方库(如 Vue、React),减少重复加载,提升页面性能。

技术选型

Qiankun

路由隔离- Single-spa

  • 不同path激活不同应用

Js 隔离(with + proxy + document + eval)

  • 重写 document.createElement("script"), 拦截动态加载的 js, 使用 fetch + eval 执行代码, 给代码增加 with 语法
  • With 捕获所有不加 window 的写法, 拦截到 proxy window
    • location、history、a 等
  • Proxy 拦截所有 window 的属性

css 隔离(单应用、shadowdom、运行时)

  • 单应用模式
  • 同时不可出现多个应用, 同时出现的应用互相污染
  • shadow-dom
    • element-plus 弹框、下拉框异常(隔离影响后父子节点、body、root 节点的获取)
    • 运行时增加 scope
    • 实验功能, 性能差
优势:
  • 我们仅会用到 window 全局变量的隔离, 几乎不改变原生行为, 行为可控, 影响点少
缺陷:
  • 接入成本高, 不支持 es module(vite)
  • css 隔离需要自行实现
  • js 执行性能差 30 倍以上

Demo:

  • qiankun 不启动 css 隔离
  • qiankun 启动 shadow dom

Wujie

路由隔离 (iframe + base)

  • 未解决缺陷(路由同步、后退失效等)

js 隔离(iframe、document、注入变量)

  • 代理 document 把 dom 插入 iframe 之外的上层应用
  • Iframe 隔离 window、document、history、location、a 等
  • 重写 createElement("script") + proxy 增加闭包注入假的 window、location 等变量
  • 重写 document + node + element 处理 shadow dom 与组件库配合的问题
  • 重写 eventListener 处理事件隔离或通知
  • 重写其他处理副作用
优点:
  • 接入成本低, esmodule 可用(vite), shadow dom 可用
缺陷
  • 异常, 可解决: 路由丢失、后退失效
  • 异常, 计算偏移问题
  • 异常, esmodule 可用, 但不支持变量注入 location、history、document
  • 维护复杂, 多个 iframe, 多个 window、location、document 等实例
  • 影响范围大, 代理了 window、location、eventListener、document 等各种原生 api , 重写了 element、node 等 dom 原生行为, 为了兼顾组件库与 shadow dom 的配合使用
  • 原生能力问题(location、event、webworker、.....)

sandbox 方案

整体架构

通过 vite plugin 对打包后的内容转成 ast 转换其中代码- JS 隔离:将对 window 的访问重定向至代理窗口,避免直写宿主全局。

  • CSS 隔离:通过选择器前缀(默认 :where(.<appCode>))封装样式作用域。 image.png

设计方案

image.png

js 隔离

  • 实现方式:
    • 在构建与开发阶段对模块进行语法分析,精准替换对 window/globalThis 的访问,将其重定向到代理窗口变量(例如 __vite_sandbox_win__.proxy)。
    • 在模块头部自动注入代理窗口初始化逻辑(通过 getProxyWin(appCode, sandboxOptions)),每个应用按 appCode 拥有独立的代理窗口实例,可复用避免重复创建。
  • 作用范围与判定:
    • 仅替换 window.xxxglobalThis.xxx 以及孤立的 window/globalThis 标识符;不修改业务自定义变量与函数,避免 TDZ 与执行时序问题。
    • 按路径过滤进行控制:默认处理业务源码,排除本包与虚拟模块、打包产物目录、node_modules;如需处理第三方库,建议使用明确白名单。
    • 基于作用域绑定(bound)进行判断:只改写未在当前/上层作用域绑定的全局对象访问,跳过局部遮蔽(如函数参数/局部变量名为 location)。
    • 跳过不确定语义的访问模式:默认不改写别名(const w = window; w.location)、解构(const { location } = window)与动态属性(window[key]),如需更强隔离可按规则开启并评估影响。
  • 事件与副作用管理:
    • 全局事件注册(如 addEventListenerremoveEventListener)通过代理窗口进行绑定,保证事件生命周期与当前应用上下文关联。
    • 可在回调上附加应用标识(如 __dd_app_code)用于调试与隔离审计,避免混用导致的定位困难。
  • 典型效果:
    • 不同应用之间的全局读写不互相污染;例如同名全局变量 a 在各自代理窗口内独立存在。
    • documentlocation 等常用对象的访问在代理上下文中完成,既可读取宿主状态,又能避免误写宿主。
    • 业务代码无需显式改造对 window/globalThis 的使用,隔离由插件自动完成。

css 隔离

  • 常见方案:
    • css 隔离插件就是加一个 namespace, 一般方案如 micro app
    • micro-zoe.github.io/micro-app/d…
    • 构建前 .el-dialog {} --> 构建后 .appname .el-dialog {}
      • 但这种有个弊端, 像 element-plus 这种组件库如果往 body 插入 dialog 等内容, 因为父级是 body, 没有子应用容器 .appname , 所以样式会丢失
  • sandbox 方案
    •       <body>
                <div class="el-dialog">
      
    • 我们的插件针对 el-dialog 这种直接插入 body 的 class, 同时加两种 namespace
    • 构建前 .el-dialog {} -> 构建后 .appname .el-dialog, .appname.el-dialog {}
    •       <body>
                  <div class="appname el-dialog">
      
  • 实现方式:
    • 在样式编译阶段为选择器统一包裹前缀(默认 :where(.<appCode>),可自定义函数或字符串),保证引入隔离同时尽量不提升选择器权重。
    • 通过包含/排除规则控制需要前缀化的选择器集合;对 :roothtmlbody 等全局选择器可按规则排除,避免破坏宿主基础样式。
  • 作用范围与兼容:
    • 支持 css/scss/less 等常见预处理语言;对伪类、伪元素与复杂选择器保持兼容。
    • 不修改 @keyframes 名称与动画定义,避免跨作用域冲突;如需隔离动画命名,可额外开启命名空间策略。
  • 典型效果:
    • 每个应用的样式限制在自身前缀作用域,不影响宿主或其他应用;嵌套组件样式在前缀内正常工作。

    • 可按需指定组件选择器或局部区域进行前缀化,平衡隔离与性能;对第三方 UI 库可通过包含列表选择性隔离。

接入sandbox

安装

npm i @dd-code/dd-sandbox -D

使用

在vite.config中 plugins中添加插件,并且修改appCode(唯一标识)

import vitePluginSandbox from '@dd-code/dd-sandbox';
export default defineConfig(({ mode, command }: ConfigEnv) => {
    plugins: [
    ...,
    vitePluginSandbox({appCode: 'xxxx'})
    ]
})

gitlab

dd-sandbox