微前端原理(篇一)
#微前端
目录
- 1. 总结
- 2. 微前端的核心原理
- 3. 常见的 JavaScript 沙箱方案
- 4. 样式隔离方案与 JS 隔离
- 5. 常见的微前端方案及对比
- 6. 实现一个主应用和子应用之间的通信系统
- 7. qiankun 的实现原理
- 8. 路由分发原理
- 9. 其他微前端框架和实现原理
- 10. 微前端中JS沙箱具体使用场景
- 11. 主应用和多个子应用并存时的沙箱处理方案:
- 12. 更多
1. 总结
- 微前端核心原理:
- ==①== 应用之间隔离:确保应用间 JS CSS DOM 互不干扰
- js 隔离
- css 隔离
- ==②== 生命周期管理
- 主应用统一管理:加载 → 挂载 → 更新 → 卸载
- ==③== 通信机制:主应用和子应用通讯
- event bus:事件总线
- props
- 全局状态库
- 自定义事件
- ==④== 路由分发
- 主应用统一管理,分发到不同子应用
- ==①== 应用之间隔离:确保应用间 JS CSS DOM 互不干扰
- 常用的 JavaScript 沙箱
- ==①== 快照沙箱:
- 启动时机
启动时
记录全局状态,在沙箱关闭时
恢复全局状态- 启动:记录当前window对象快照
- 关闭:还原window对象
- ==进入一个新的容器,那么就使用启动,否则关闭==
- 缺点:无法支持多个子应用同时运行
- 性能:快照和恢复操作可能比较耗时
- 启动时机
- ==②== 代理沙箱
- ==原理==:
- 使用
Proxy
代理对象来实现沙箱,可以精确控制对全局对象的访问和修改
- 使用
- 优点:
- 支持多个实例、多个应用
- ==优先==从自己的环境中取值
- 否则从全局取值
- 性能好,隔离性强
- 支持多个实例、多个应用
- 缺点
- proxy 的兼容问题
- ==原理==:
- ==③== Legacy 沙箱:
- 基于 with + eval + new Function
- ==④== iframe 沙箱:
- 天然的沙箱
- ==⑤== Node.js 的
vm2 模块
或者 go 的 js 沙箱模块 - ==⑥== 组合沙箱 (Composite Sandbox)
- ==①== 快照沙箱:
- 样式隔离方案
- Shadow DOM 隔离
- 应用切换时:动态样式表切换
- 性能开销大,会出现闪烁
- css modules
- BFM 命名规范
- 子应用添加唯一前缀等
- 应用间通讯方式
- 事件总线,比如 event-bus
- 状态管理
- 结合状态管理库比如 redux 或者 pinia 的==订阅==
- 共享实例
- 直接调用
- props 传递等
- qiankun 实现原理
- 主应用:
- 注册子应用,比如
- entry
- container
- acitveRule
- 监听全局路由,匹配需要加载、切换、卸载子应用等
- fetch index.html → 抽取 js ,然后 eval
- fetch 需要同域
- 需要 umd 格式,故需要修改 webpack
- 应用间相关跳转时,需要及时卸载
- 样式隔离,两种方案
- 命名空间,类似于 vue scope style
- webcomponent
- fetch index.html → 抽取 js ,然后 eval
- 注册子应用,比如
- 主应用:
- js 沙箱启动时机
- 子应用==加载时==启动沙箱
- 子应用==挂载时==启动沙箱
- 基于路由的沙箱管理
- 找到要进入的路由对应的应用名
- 如果果离开的是子应用,==关闭其沙箱==
- 如果进入的是子应用,==启动其沙箱==
- 可配置是否启动沙箱
- 当容器进入视口时启动沙箱
- 使用
IntersectionObserver
可优化性能
- 使用
- 问:如何实现
window.addEventListener('micro-app-message', handler)
new CustomEvent('micro-app-message'
- 然后 再
window.dispatchEvent
- 然后 再
- js 沙箱==性能优化==的要点
- 按需启动沙箱
- 使用
WeakMap
存储状态,避免内存泄漏 - 使用
requestIdleCallback
进行==初始化== - 资源共享:比如 React、vue 、各类组件等
- sandbox.setGlobalVariables(globalVars);
- 基于DOM节点的隔离:子应用使用容器沙箱
container.attachShadow({ mode: 'closed' });
另外可参考
2. 微前端的核心原理
- 应用隔离:确保各个子应用之间的 JavaScript、CSS、DOM 互不干扰
- JavaScript 沙箱:
- 确保各个子应用的 JS 运行环境相互隔离
- CSS 隔离:
- 防止样式冲突
- 全局变量隔离:
- 避免全局变量污染
- JavaScript 沙箱:
- 生命周期管理:主应用统一管理各个子应用的加载、挂载、卸载等生命周期
- 加载(bootstrap)
- 挂载(mount)
- 卸载(unmount)
- 更新(update)
- 通信机制:在主应用和子应用之间建立通信渠道
- 基于事件总线
- 基于 Props 传递
- 基于全局状态管理
- 基于自定义事件
- 路由分发:
- 统一的路由管理,将不同路由分发到对应的子应用
3. 常见的 JavaScript 沙箱方案
主要解决子应用,污染了主应用的 window 对象
3.1. 快照沙箱 (Snapshot Sandbox)
快照沙箱的核心思想是在沙箱启动时
记录全局状态,在沙箱关闭时
恢复全局状态。
- 比如,==进入一个新的容器,那么就使用启动,否则关闭==
- 优点:
- 实现简单
- 缺点
- 无法支持多个子应用同时运行
- 性能:
- 快照和恢复操作可能比较耗时
class SnapshotSandbox {
constructor() {
this.snapshot = {}; // 存储快照
this.modifyPropsMap = {}; // 存储被修改的属性
}
// 激活沙箱
active() {
// 1. 记录当前window对象的快照
for (const prop in window) {
this.snapshot[prop] = window[prop];
}
// 2. 恢复之前被修改的属性
Object.keys(this.modifyPropsMap).forEach(prop => {
window[prop] = this.modifyPropsMap[prop];
});
}
// 关闭沙箱
inactive() {
// 1. 记录被修改的属性
for (const prop in window) {
if (window[prop] !== this.snapshot[prop]) {
this.modifyPropsMap[prop] = window[prop];
// 2. 恢复原来的属性值
window[prop] = this.snapshot[prop];
}
}
}
}
// 使用示例
const sandbox = new SnapshotSandbox();
sandbox.active(); // 激活沙箱
// 在沙箱中运行代码
window.newVar = "test";
sandbox.inactive(); // 关闭沙箱
console.log(window.newVar); // undefined
3.2. 代理沙箱 (Proxy Sandbox)
使用 Proxy 代理对象来实现沙箱,可以精确控制对全局对象的访问和修改。
- 优点:
- 支持多个实例,多个应用
- 性能较好
- 隔离性强
- 缺点:
- 不支持低版本浏览器(需要 Proxy 支持)
- 某些特殊场景可能存在兼容性问题
class ProxySandbox {
constructor() {
this.running = false;
this.proxyWindow = {};
const fakeWindow = Object.create(null);
const proxy = new Proxy(fakeWindow, {
set: (target, prop, value) => {
if (this.running) {
target[prop] = value;
return true;
}
return false;
},
get: (target, prop) => {
// 优先从代理对象中取值
if (prop in target) {
return target[prop];
}
// 否则从真实window对象中取值
const value = window[prop];
return typeof value === 'function'
? value.bind(window)
: value;
},
has: (target, prop) => {
return prop in target || prop in window;
}
});
this.proxy = proxy;
}
active() {
this.running = true;
}
inactive() {
this.running = false;
}
}
// 使用示例
const sandbox = new ProxySandbox();
sandbox.active();
sandbox.proxy.newVar = "test";
console.log(window.newVar); // undefined
console.log(sandbox.proxy.newVar); // "test"
3.3. Legacy 沙箱 (基于 with + eval)
使用 with 语句
和 eval
来实现简单的沙箱隔离。
class LegacySandbox {
constructor(context = {}) {
this.context = context;
}
run(code) {
const contextStr = Object.keys(this.context)
.map(key => `let ${key} = this.context.${key}`)
.join(';');
return new Function(`
with (this.context) {
${contextStr};
return eval(\`${code}\`);
}
`).call(this);
}
}
// 使用示例
const sandbox = new LegacySandbox({
name: 'test',
log: console.log
});
sandbox.run(`
log(name); // 输出: test
log(window); // window 是未定义的
`);
3.4. iframe 沙箱
利用 iframe 的天然隔离特性实现沙箱。
class IframeSandbox {
constructor() {
this.iframe = document.createElement('iframe');
this.iframe.style.display = 'none';
document.body.appendChild(this.iframe);
this.global = this.iframe.contentWindow;
}
run(code) {
// 注入代码到 iframe 环境中执行
const script = this.iframe.contentDocument.createElement('script');
script.text = code;
this.iframe.contentDocument.body.appendChild(script);
}
destroy() {
document.body.removeChild(this.iframe);
}
}
// 使用示例
const sandbox = new IframeSandbox();
sandbox.run(`
window.testVar = "hello";
console.log(window.testVar);
`);
console.log(window.testVar); // undefined
3.5. VM 沙箱 (基于 vm2)
使用 Node.js 的 vm2 模块
实现更安全的沙箱(仅在 Node.js 环境中可用)。
const { VM } = require('vm2');
class VMSandbox {
constructor(context = {}) {
this.vm = new VM({
timeout: 1000,
sandbox: context
});
}
run(code) {
return this.vm.run(code);
}
}
// 使用示例
const sandbox = new VMSandbox({
name: 'test',
console: console
});
sandbox.run(`
console.log(name); // 输出: test
// 以下代码将抛出错误,因为process是未定义的
// console.log(process.env);
`);
无论是 Go 或者其他语言其实都有 JS 沙箱的实现,因为要基于它来做很多其他事情
一些低代码平台,为了增强编排的能力等,后端使用了 JS 沙箱,支持更强更灵活的配置编排能力
3.6. 组合沙箱 (Composite Sandbox)
在实际应用中,我们可能需要组合多种沙箱技术来实现更完善的隔离。
class CompositeSandbox {
constructor() {
this.proxySandbox = new ProxySandbox();
this.snapshotSandbox = new SnapshotSandbox();
this.running = false;
}
active() {
if (!this.running) {
this.snapshotSandbox.active();
this.proxySandbox.active();
this.running = true;
}
}
inactive() {
if (this.running) {
this.snapshotSandbox.inactive();
this.proxySandbox.inactive();
this.running = false;
}
}
// 获取代理对象
get proxyWindow() {
return this.proxySandbox.proxy;
}
}
// 使用示例
const sandbox = new CompositeSandbox();
sandbox.active();
const proxyWindow = sandbox.proxyWindow;
proxyWindow.newVar = "test";
console.log(window.newVar); // undefined
console.log(proxyWindow.newVar); // "test"
sandbox.inactive();
4. 样式隔离方案与 JS 隔离
4.1. Shadow DOM 隔离
- 完全隔离,最彻底的方案
- 浏览器原生支持
- 弹窗类组件需要特殊处理 ❓
- 其实不用,正好可以解决弹窗问题呀?
4.2. 动态样式表切换
在应用切换时动态切换样式表,
- 常用于
qiankun
等方案中, 性能开销较大,可能出现样式闪烁
4.3. CSS Modules 方案
- 优点
- 编译时处理,运行时零开销
- 局部作用域,避免冲突
- 缺点
- 需要修改构建配置
- 所有样式需要模块化处理
4.4. BEM 命名约定、css-In—js、子应用添加唯一前缀 等
通过规范的命名约定来避免样式冲突
5. 常见的微前端方案及对比
特性 | 无界(wujie) | qiankun | micro-app | single-spa | iframe | Module Federation |
---|---|---|---|---|---|---|
基础实现 | WebComponent + iframe | single-spa + import-html-entry | WebComponent | 路由劫持 | 原生 iframe | Webpack 5 模块联邦 |
隔离方案 | CSS/JS 完全隔离 | 快照沙箱/代理沙箱 | Shadow DOM | 无 | 天然隔离 | 无 |
性能 | 优秀 | 一般 | 较好 | 较好 | 一般 | 优秀 |
预加载 | 支持 | 支持 | 支持 | 不支持 | 不支持 | 支持 |
通信方式 | props + 发布订阅 | props + 发布订阅 | CustomEvent + 数据属性 | 发布订阅 | postMessage | 模块导入导出 |
子应用改造 | 极少 | 中等 | 较少 | 较多 | 无 | 中等 |
主应用改造 | 较少 | 中等 | 较少 | 较多 | 无 | 中等 |
技术栈限制 | 无 | 无 | 无 | 无 | 无 | 需使用 Webpack 5 |
子应用共享依赖 | 支持 | 支持 | 不支持 | 支持 | 不支持 | 原生支持 |
样式隔离 | 完全隔离 | 动态样式表切换 | Shadow DOM | 无 | 完全隔离 | 无 |
JS 沙箱 | iframe + Proxy | Proxy/Snapshot | iframe Proxy | 无 | 天然隔离 | 无 |
CSP 策略 | 友好 | 不友好 | 较友好 | 友好 | 友好 | 友好 |
子应用调试 | 便捷 | 一般 | 便捷 | 一般 | 便捷 | 便捷 |
6. 实现一个主应用和子应用之间的通信系统
这个通信系统
提供了三种主要的通信方式:
- 事件总线:用于发布-订阅模式的事件通信
- 状态管理:用于共享数据和状态
- 直接调用:用于直接调用其他应用提供的方法
主要特点:
- 支持多种通信方式
- 类型安全
- 统一的API
- 支持事件解绑和状态取消订阅
- 错误处理
- 支持所有框架(框架无关)
在实际使用中,可以根据需求选择合适的通信方式,并可以进一步扩展功能,如:
- 添加通信日志
- 实现通信加密
- 添加权限控制
- 实现通信超时处理
- 添加消息队列
- 实现通信重试机制
7. qiankun 的实现原理
更多见个人流程图整理: figjam
基座应用,需要做以下事情
- ① 负责注册子应用,示例如下
- entry 子应用 HTML 的入口去哪儿拿
- container:==渲染到哪儿==
- activeRule:路由匹配规则
- ② 基座里,需要监听全局路由,然后找到匹配子应用,然后加载子应用,再然后卸载或切换等
- fetch 子应用的入口文件
index.html
,然后需要抽取 js , eval 执行它 - 所以,需要==处理成兼容的 umd 格式==,故需要修改 webpack
- fetch 所以要求同域
- 执行完子应用的脚本后,需要挂载
#app
上,但可能会直接覆盖丢主应用;- 所以才会要求子应用有自己的
container
属性,这也是为什么建议子应用 name/id 唯一;
- 所以才会要求子应用有自己的
- 图片路径可能 404,所以需要注入正确的子应用 public path
- 两个子应用相互跳转时,如果不及时卸载,可能会出现两个子应用同时展示的情况
- 关于样式隔离,两种方案
- 命名空间,类似于 vue style scope
- webcomponet 方案
- fetch 子应用的入口文件
8. 路由分发原理
9. 其他微前端框架和实现原理
9.1. iframe
9.2. systemjs : type=systemjs-importmap
9.3. micro-app
10. 微前端中JS沙箱具体使用场景
10.1. JS沙箱启动时机
10.1.1. 常见的启动时机
- 子应用==加载时==启动沙箱
- 子应用==挂载时==启动沙箱
// 1. 子应用加载时启动沙箱
class MicroApp {
async loadApp(appConfig) {
// 创建沙箱实例
const sandbox = new Sandbox(appConfig.name);
// 启动沙箱
sandbox.start();
try {
// 加载子应用资源
await loadScript(appConfig.entry);
} catch (error) {
console.error('Load app failed:', error);
}
}
}
// 2. 子应用挂载时启动沙箱
class Sandbox {
async mount() {
// 记录当前快照
this.snapshotBefore = this.takeSnapshot();
// 激活沙箱
this.active = true;
// 执行子应用的 mount 钩子
await this.app.mount();
}
async unmount() {
// 执行子应用的 unmount 钩子
await this.app.unmount();
// 关闭沙箱
this.active = false;
// 还原环境
this.restoreSnapshot(this.snapshotBefore);
}
}
10.2. 混合页面场景处理
10.2.1. 基于路由的隔离:匹配到路由了,启动
router.beforeEach
中检测是否需要启动
// 路由配置
const routes = [
{
path: '/main/*', // 主应用路由
component: MainApp,
sandbox: false // 不启用沙箱
},
{
path: '/sub/*', // 子应用路由
component: SubApp,
sandbox: true // 启用沙箱
}
];
// 路由守卫
router.beforeEach((to, from, next) => {
const needSandbox = to.matched.some(record => record.sandbox);
if (needSandbox) {
// 启动沙箱
sandbox.start();
} else {
// 关闭沙箱
sandbox.stop();
}
next();
});
10.2.2. 基于DOM节点的隔离:子应用使用容器沙箱
class DomSandbox {
constructor(container) {
this.container = container;
this.shadowRoot = container.attachShadow({ mode: 'closed' });
}
mount(component) {
// 在 Shadow DOM 中渲染子应用
this.shadowRoot.innerHTML = '';
this.shadowRoot.appendChild(component);
}
}
// 使用示例
const mainApp = document.querySelector('#main-app');
const subApp = document.querySelector('#sub-app');
// 子应用容器使用沙箱
const sandbox = new DomSandbox(subApp);
sandbox.mount(subAppComponent);
关于
{ mode: 'closed' }
,更多参考 8. Shadow DOM 中的 closed mode 和 open mode
10.3. 不同类型的沙箱实现
10.3.1. 快照沙箱(适用于单个子应用)
class SnapshotSandbox {
constructor() {
this.snapshot = {};
this.modifyPropsMap = {};
}
start() {
// 记录当前window对象快照
for (const prop in window) {
this.snapshot[prop] = window[prop];
}
}
stop() {
// 还原window对象
for (const prop in this.modifyPropsMap) {
if (this.snapshot[prop] === undefined) {
delete window[prop];
} else {
window[prop] = this.snapshot[prop];
}
}
}
}
10.3.2. 代理沙箱(适用于多个子应用)
如何适用多个子应用的?
- ==优先==从自己的环境中取值
- 否则从全局取值
class ProxySandbox {
constructor() {
const fakeWindow = {};
const proxy = new Proxy(fakeWindow, {
get: (target, prop) => {
// 优先从自己的环境中取值
if (prop in target) {
return target[prop];
}
// 否则从全局取值
return window[prop];
},
set: (target, prop, value) => {
target[prop] = value;
return true;
}
});
this.proxy = proxy;
}
start() {
// 将代理对象作为子应用的全局对象
window.__PROXY__ = this.proxy;
}
stop() {
// 清理代理对象
window.__PROXY__ = undefined;
}
}
10.3.3. 组合沙箱(更完整的隔离)
class CompositeSandbox {
constructor() {
this.proxySandbox = new ProxySandbox();
this.domSandbox = new DomSandbox();
this.eventSandbox = new EventSandbox();
}
async start() {
// 启动所有沙箱
this.proxySandbox.start();
this.domSandbox.start();
this.eventSandbox.start();
}
async stop() {
// 停止所有沙箱
this.eventSandbox.stop();
this.domSandbox.stop();
this.proxySandbox.stop();
}
}
10.4. 特殊场景处理
10.4.1. 共享依赖处理 → 比如共享 React、ReactDom 等全局类库
class SharedDependencySandbox {
constructor(shared = {}) {
this.shared = shared;
}
start() {
// 注入共享依赖
Object.keys(this.shared).forEach(key => {
window[key] = this.shared[key];
});
}
}
// 使用示例
const sandbox = new SharedDependencySandbox({
React: window.React,
ReactDOM: window.ReactDOM
});
10.4.2. 通信机制
class MessageSandbox {
constructor() {
this.listeners = new Map();
}
// 发送消息
postMessage(type, data) {
const event = new CustomEvent('micro-app-message', {
detail: { type, data }
});
window.dispatchEvent(event);
}
// 监听消息
addEventListener(type, callback) {
const listener = (event) => {
if (event.detail.type === type) {
callback(event.detail.data);
}
};
this.listeners.set(callback, listener);
window.addEventListener('micro-app-message', listener);
}
// 移除监听
removeEventListener(callback) {
const listener = this.listeners.get(callback);
if (listener) {
window.removeEventListener('micro-app-message', listener);
this.listeners.delete(callback);
}
}
}
10.5. 最佳实践建议
10.5.1. 性能优化
- 使用
WeakMap
存储状态,避免内存泄漏 - 使用
requestIdleCallback
进行初始化
class OptimizedSandbox {
constructor() {
// 使用 WeakMap 存储状态,避免内存泄漏
this.state = new WeakMap();
// 使用 requestIdleCallback 进行初始化
requestIdleCallback(() => {
this.init();
});
}
init() {
// 初始化沙箱环境
}
}
10.5.2. 错误处理
class ErrorBoundarySandbox {
async executeInSandbox(code) {
try {
// 在沙箱中执行代码
const result = await this.proxy.eval(code);
return result;
} catch (error) {
// 错误处理
console.error('Sandbox execution error:', error);
// 通知主应用
this.reportError(error);
// 尝试恢复
this.recover();
}
}
}
10.5.3. 生命周期管理
class LifecycleSandbox {
constructor() {
this.status = 'inactive';
this.hooks = new Map();
}
registerHook(name, fn) {
this.hooks.set(name, fn);
}
async start() {
this.status = 'starting';
await this.executeHook('beforeStart');
// 启动沙箱逻辑
this.status = 'active';
await this.executeHook('afterStart');
}
async stop() {
this.status = 'stopping';
await this.executeHook('beforeStop');
// 停止沙箱逻辑
this.status = 'inactive';
await this.executeHook('afterStop');
}
}
使用建议:
- 根据应用场景选择合适的沙箱类型
- 注意性能影响,避免频繁创建销毁沙箱
- 合理处理共享资源和通信机制
- 实现完善的错误处理和恢复机制
- 做好沙箱的生命周期管理
- 考虑浏览器兼容性问题
11. 主应用和多个子应用并存时的沙箱处理方案:
11.1. 基于路由的沙箱管理(最常用)
关键点:
- 找到要进入的路由对应的应用名
- 如果果离开的是子应用,==关闭其沙箱==
- 如果进入的是子应用,==启动其沙箱==
// 路由监听
router.beforeEach((to, from, next) => {
// 找到要进入的路由对应的应用名
const toAppName = to.matched[0]?.appName;
// 找到要离开的路由对应的应用名
const fromAppName = from.matched[0]?.appName;
// 如果离开的是子应用,关闭其沙箱
if (fromAppName) {
sandboxManager.stopSandbox(fromAppName);
}
// 如果进入的是子应用,启动其沙箱
if (toAppName) {
sandboxManager.startSandbox(toAppName);
}
next();
});
11.1.1. 简单的沙箱管理类
// 简单的沙箱管理类
class SandboxManager {
constructor() {
// 存储所有子应用的沙箱实例
this.sandboxes = new Map();
}
// 启动某个子应用的沙箱
startSandbox(appName) {
const sandbox = this.sandboxes.get(appName);
if (sandbox) {
sandbox.active = true;
}
}
// 关闭某个子应用的沙箱
stopSandbox(appName) {
const sandbox = this.sandboxes.get(appName);
if (sandbox) {
sandbox.active = false;
}
}
}
// 路由配置示例
const routes = [
{
path: '/', // 主应用路由
component: MainApp, // 不需要沙箱
},
{
path: '/app1/*', // 子应用1
component: MicroApp1,
appName: 'app1' // 需要沙箱
},
{
path: '/app2/*', // 子应用2
component: MicroApp2,
appName: 'app2' // 需要沙箱
}
];
11.2. 基于 DOM 结构的沙箱管理(混合页面场景)
// HTML 结构
<div id="main-app">
<!-- 主应用内容 -->
<header>主应用的头部</header>
<!-- 子应用容器 -->
<div id="sub-app1"></div>
<div id="sub-app2"></div>
</div>
// JavaScript 代码
class SimpleSandbox {
constructor(appName, container) {
this.appName = appName;
this.container = container;
}
// 启动沙箱
start() {
console.log(`${this.appName} sandbox started`);
// 这里添加沙箱隔离逻辑
}
// 关闭沙箱
stop() {
console.log(`${this.appName} sandbox stopped`);
// 这里添加清理逻辑
}
}
11.2.1. 当容器进入视口时启动沙箱:IntersectionObserver
为了性能优化
// 初始化子应用
function initSubApp(appName, containerId) {
const container = document.getElementById(containerId);
const sandbox = new SimpleSandbox(appName, container);
// 当容器进入视口时启动沙箱
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
sandbox.start();
} else {
sandbox.stop();
}
});
});
observer.observe(container);
}
// 初始化所有子应用
initSubApp('app1', 'sub-app1');
initSubApp('app2', 'sub-app2');
11.3. 主子应用通信场景
// 简单的消息通道
class MessageChannel {
constructor() {
this.listeners = new Map();
}
// 发送消息
send(from, to, message) {
const event = new CustomEvent('micro-app-message', {
detail: { from, to, message }
});
window.dispatchEvent(event);
}
// 接收消息
listen(appName, callback) {
const handler = (event) => {
const { from, to, message } = event.detail;
if (to === appName || to === '*') {
callback(message, from);
}
};
window.addEventListener('micro-app-message', handler);
this.listeners.set(appName, handler);
}
}
// 使用示例
const messageChannel = new MessageChannel();
// 主应用发送消息给子应用
messageChannel.send('main', 'app1', { type: 'update', data: {...} });
// 子应用监听消息
messageChannel.listen('app1', (message, from) => {
console.log(`收到来自 ${from} 的消息:`, message);
});
11.4. 实际应用建议
11.4.1. 按需启动
// 只在必要时启动沙箱
if (isSubApp(appName)) {
sandbox.start();
}
11.4.2. 资源共享
// 可以设置一些公共资源不进入沙箱
const globalVars = ['React', 'Vue', 'jQuery'];
sandbox.setGlobalVariables(globalVars);
11.4.3. 性能优化
// 使用延迟加载
const sandbox = new Proxy({}, {
get(target, property) {
// 只在实际使用时初始化
if (!target.instance) {
target.instance = new Sandbox();
}
return target.instance[property];
}
});
这样的实现方式更加清晰和实用,主要关注点在于:
- 何时启动沙箱(进入子应用时)
- 何时关闭沙箱(离开子应用时)
- 如何处理多个子应用(每个子应用独立的沙箱实例)
- 主子应用如何通信(消息通道)
这种方式可以确保:
- 主应用正常运行不受影响
- 子应用间相互隔离
- 资源可以按需加载和释放
- 维护成本相对较低
12. 更多
- 再把之前整理的草稿流程图看看,详见 figjam
- https://www.garfishjs.org/blog/architecture.html
- https://juejin.cn/post/7113503219904430111