插件介绍
在微前端架构落地过程中,“隔离” 与 “兼容” 始终是核心痛点 —— 不同技术栈(Vue2/Vue3/React)、不同构建工具(Webpack/Vite)的应用共存时,常面临全局变量污染、样式穿透、路由冲突等问题,而传统沙箱方案要么接入成本高,要么兼容不足(如不支持 Vite 生态的 ES Module),要么隔离不彻底(如组件库弹窗样式丢失)。
「Vite 微前端沙盒插件」专为 Vite 构建体系设计,核心目标是在不侵入业务代码的前提下,实现 “轻量隔离 + 无缝协作” :通过 AST 语法转换与编译时处理,为每个子应用打造独立的 JS 执行上下文与 CSS 作用域,同时兼容 ES Module 特性、第三方 UI 库(如 Element Plus、Ant Design),让多应用像 “搭积木” 一样嵌入主应用,既互不干扰,又能顺畅通信。
插件采用 “构建时转换 + 运行时代理” 的混合方案,无需修改业务逻辑,仅需简单配置即可接入,完美解决 Vite 微前端项目的隔离难题,同时兼顾性能与兼容性。
核心目标
-
JS 隔离:安全无污染,兼容 Vite 生态
- 实现全局变量(window/globalThis)访问重定向,每个应用通过独立代理窗口运行,同名变量、方法互不干扰,避免 “全局污染” 与 “变量覆盖” 问题。
- 兼容 Vite 原生 ES Module 特性,支持动态导入、第三方库按需加载,不牺牲 Vite 的构建与运行性能。
- 精准控制隔离范围:仅处理全局对象访问,不修改业务自定义变量与函数,避免执行时序问题与 TDZ(暂时性死区)风险。
- 事件生命周期绑定:全局事件(addEventListener/removeEventListener)与应用上下文关联,便于调试与审计,防止事件混用导致的异常。
-
CSS 隔离:彻底不穿透,适配组件库
- 通过编译时添加选择器前缀(默认
:where(.<appCode>)),将每个应用的样式禁锢在独立作用域,不影响宿主与其他子应用样式。 - 针对性解决 UI 库弹窗 / 下拉框(如 Element Plus 的 ElDialog)直接插入 body 导致的样式丢失问题,自动为这类元素添加双重命名空间,确保样式生效。
- 兼容 CSS/SCSS/LESS 等预处理语言,支持伪类、伪元素、@keyframes 等复杂语法,不提升选择器权重,避免样式优先级冲突。
- 可配置包含 / 排除规则,灵活控制需要隔离的样式范围,平衡隔离效果与构建性能。
- 通过编译时添加选择器前缀(默认
-
零侵入接入:低成本落地,无业务改造
- 插件化集成,仅需在 Vite 配置中添加依赖与应用唯一标识(appCode),无需修改业务代码、路由配置或项目结构。
- 保持原生行为一致性:隔离逻辑不改变浏览器原生 API 行为,子应用开发体验与独立项目一致,降低团队学习成本。
- 多场景兼容:支持单应用独占、多应用同时渲染等场景,适配小程序 / H5/App 多端微前端架构,无额外适配成本。
-
无缝协作:隔离不割裂,通信更顺畅
- 隔离不阻断应用间通信:代理窗口可按需透传宿主全局变量与方法,支持主 - 子应用、子应用间的标准化通信。
- 路由协同:兼容主流微前端路由方案(如 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>))封装样式作用域。
设计方案
js 隔离
- 实现方式:
- 在构建与开发阶段对模块进行语法分析,精准替换对
window/globalThis的访问,将其重定向到代理窗口变量(例如__vite_sandbox_win__.proxy)。 - 在模块头部自动注入代理窗口初始化逻辑(通过
getProxyWin(appCode, sandboxOptions)),每个应用按appCode拥有独立的代理窗口实例,可复用避免重复创建。
- 在构建与开发阶段对模块进行语法分析,精准替换对
- 作用范围与判定:
- 仅替换
window.xxx、globalThis.xxx以及孤立的window/globalThis标识符;不修改业务自定义变量与函数,避免 TDZ 与执行时序问题。 - 按路径过滤进行控制:默认处理业务源码,排除本包与虚拟模块、打包产物目录、
node_modules;如需处理第三方库,建议使用明确白名单。 - 基于作用域绑定(bound)进行判断:只改写未在当前/上层作用域绑定的全局对象访问,跳过局部遮蔽(如函数参数/局部变量名为
location)。 - 跳过不确定语义的访问模式:默认不改写别名(
const w = window; w.location)、解构(const { location } = window)与动态属性(window[key]),如需更强隔离可按规则开启并评估影响。
- 仅替换
- 事件与副作用管理:
- 全局事件注册(如
addEventListener、removeEventListener)通过代理窗口进行绑定,保证事件生命周期与当前应用上下文关联。 - 可在回调上附加应用标识(如
__dd_app_code)用于调试与隔离审计,避免混用导致的定位困难。
- 全局事件注册(如
- 典型效果:
- 不同应用之间的全局读写不互相污染;例如同名全局变量
a在各自代理窗口内独立存在。 - 对
document、location等常用对象的访问在代理上下文中完成,既可读取宿主状态,又能避免误写宿主。 - 业务代码无需显式改造对
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>),可自定义函数或字符串),保证引入隔离同时尽量不提升选择器权重。 - 通过包含/排除规则控制需要前缀化的选择器集合;对
:root、html、body等全局选择器可按规则排除,避免破坏宿主基础样式。
- 在样式编译阶段为选择器统一包裹前缀(默认
- 作用范围与兼容:
- 支持
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'})
]
})