微前端原理(篇三:乾坤)

#微前端

目录

1. 总结

  • 核心能力
    1. 应用加载(HTML Entry)
    2. JavaScript 沙箱
    3. 样式隔离
    4. 生命周期管理
    5. 应用通信
  • 应用加载(HTML Entry)
    • 获取 HTML 内容
    • 解析 HTML,提取资源
    • 加载外部脚本和样式
    • 渲染应用
  • 生命周期管理
    1. bootstrap:应用首次加载时调用
    2. mount:应用激活时调用
    3. unmount:应用切换/卸载时调用
    4. update:应用更新时调用
  • 通讯方式
    1. Props:简单直接,适合父子通信
    2. 全局状态:功能完整,适合复杂场景
    3. 自定义事件:灵活,但需要自行管理
  • 样式隔离
    • strictStyleIsolation:使用 Shadow DOM :严格隔离
      • 隔离更彻底,但兼容性问题多
    • experimentalStyleIsolation:特定前缀
      • 兼容性更好,但隔离不够彻底
  • 性能优化策略
    1. 应用预加载
    2. 资源缓存复用:缓存检测
    3. 并行加载优化
    4. 按需加载策略
  • 错误处理机制
    1. 应用加载错误
    2. 运行时错误
    3. 生命周期错误
    4. 资源加载错误

2. 整体架构

乾坤基于 single-spa 封装,实现了以下核心功能:

// 主应用注册微应用 registerMicroApps([ { name: 'app1', entry: '//localhost:8080', container: '#container', activeRule: '/app1' } ]); // 启动主应用 start();

核心功能模块:

  1. 应用加载(HTML Entry)
  2. JavaScript 沙箱
  3. 样式隔离
  4. 生命周期管理
  5. 应用通信

3. 应用加载机制(HTML Entry)

乾坤使用 HTML Entry 方式加载应用,主要步骤:

  • 获取 HTML 内容
  • 解析 HTML,提取资源
  • 加载外部脚本和样式
  • 渲染应用
async function loadApp(app) { // 1. 获取 HTML 内容 const html = await fetch(app.entry).then(res => res.text()); // 2. 解析 HTML,提取资源 const { template, // DOM 结构 scripts, // JS 脚本 styles // CSS 样式 } = parseHTML(html); // 3. 加载外部脚本和样式 await Promise.all([ loadScripts(scripts), loadStyles(styles) ]); // 4. 渲染应用 const container = document.querySelector(app.container); container.innerHTML = template; }

4. 生命周期管理

乾坤为每个微应用定义了完整的生命周期:

// 微应用需要导出以下生命周期函数 export async function bootstrap() { // 应用初始化 } export async function mount(props) { // 应用挂载 render(props); } export async function unmount() { // 应用卸载 cleanup(); } // 可选 export async function update(props) { // 应用更新 }

生命周期执行流程:

  1. bootstrap:应用首次加载时调用
  2. mount:应用激活时调用
  3. unmount:应用切换/卸载时调用
  4. update:应用更新时调用

5. 路由系统

乾坤的路由系统基于 URL 改变触发应用切换:

  • 支持配置式路由
  • 支持函数动态判断
  • 自动处理应用切换
class RouterEngine { constructor(apps) { this.apps = apps; window.addEventListener('popstate', this.handleRouteChange); } handleRouteChange = () => { const { pathname } = window.location; // 查找匹配的应用 const app = this.apps.find(app => { return this.isActive(app.activeRule, pathname); }); if (app) { // 加载并挂载应用 this.mountApp(app); } } isActive(rule, pathname) { // 支持字符串或函数规则 return typeof rule === 'function' ? rule(pathname) : pathname.startsWith(rule); } }

6. 应用通信机制

乾坤提供多种应用通信方式:

// 1. Props 传递 registerMicroApps([{ name: 'app1', props: { data: 'shared data' } }]); // 2. 全局状态管理 import { initGlobalState } from 'qiankun'; const actions = initGlobalState({ user: 'qiankun' }); // 主应用监听 actions.onGlobalStateChange((state, prev) => { console.log(state, prev); }); // 子应用监听 export function mount(props) { props.onGlobalStateChange((state, prev) => { console.log(state, prev); }); } // 3. 自定义事件 window.addEventListener('app1-event', () => {});

6.1. 通信方式比较

  1. Props:简单直接,适合父子通信
  2. 全局状态:功能完整,适合复杂场景
  3. 自定义事件:灵活,但需要自行管理

7. 样式隔离

乾坤提供两种样式隔离方案:

  • 使用 Shadow DOM :严格隔离
  • experimentalStyleIsolation:特定前缀
// 1. 严格隔离:使用 Shadow DOM registerMicroApps([{ name: 'app1', sandbox: { strictStyleIsolation: true // 使用 Shadow DOM } }]); // 2. 作用域隔离:添加特定前缀 registerMicroApps([{ name: 'app1', sandbox: { experimentalStyleIsolation: true // 添加特定前缀 } }]);

8. qiankun 中这两种样式隔离方式的区别和各自的问题:

8.1. strictStyleIsolation(严格隔离)

这种方式使用 Shadow DOM 实现样式隔离:

// qiankun 内部实现原理 function createShadowDOM(container) { return container.attachShadow({ mode: 'open' }); } // 子应用挂载 const shadowRoot = createShadowDOM(container); shadowRoot.appendChild(subAppRoot);

8.1.1. 存在的问题:

  1. 第三方组件库兼容性问题
// 某些组件库会在 document.body 上挂载元素 // 比如 antd 的 Modal、Drawer、Message 等 const Modal = () => { // 这些组件会被挂载到 body 上 // 而不是 Shadow DOM 内部,导致样式失效 return ReactDOM.createPortal( <div className="ant-modal">...</div>, document.body ); };
  1. 弹窗层级问题
/* Shadow DOM 内部的元素 z-index 无法超过 Shadow DOM 的边界 */ .modal { z-index: 9999; /* 在 Shadow DOM 中不会生效 */ }
  1. 一些特殊 CSS 特性的限制
/* 例如 position: fixed 相对于 Shadow DOM 根节点定位,而不是 viewport */ .fixed-element { position: fixed; top: 0; /* 会相对于 Shadow DOM 定位,而不是浏览器窗口 */ }

8.2. experimentalStyleIsolation(实验性隔离)

这种方式通过给样式添加前缀选择器来实现隔离:

// 原始样式 .title { color: red; } // 转换后 div[data-qiankun="app1"] .title { color: red; }

8.2.1. 工作原理:

// qiankun 内部实现示意 function processCSSRule(rule, appName) { const prefix = `[data-qiankun="${appName}"]`; // 处理选择器 if (rule.selectorText) { rule.selectorText = rule.selectorText .split(',') .map(selector => `${prefix} ${selector}`) .join(','); } }

8.2.2. 优点:

  1. 更好的兼容性
// 弹窗组件可以正常工作 const Modal = () => { return ReactDOM.createPortal( <div className="modal">...</div>, document.body ); };
  1. 支持完整的 CSS 特性
/* position: fixed 可以正常工作 */ .fixed-element { position: fixed; top: 0; /* 会相对于视口定位 */ }

8.2.3. 但也存在一些问题:

  1. 动态生成的样式可能逃逸
// 动态插入的样式可能没有被正确处理 const style = document.createElement('style'); style.textContent = '.dynamic { color: blue; }'; document.head.appendChild(style);
  1. 性能开销
// 需要实时处理样式规则 const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { // 处理新增的样式节点 processNewStyleNodes(mutation.addedNodes); }); });

8.3. 最佳实践建议

8.3.1. 选择建议

// 1. 如果应用比较简单,推荐使用 experimentalStyleIsolation { name: 'app1', entry: '//localhost:8081', container: '#container', props: { experimentalStyleIsolation: true } } // 2. 如果需要完全隔离且不依赖第三方组件库,可以使用 strictStyleIsolation { name: 'app2', entry: '//localhost:8082', container: '#container', props: { strictStyleIsolation: true } }

8.3.2. 混合使用策略

// 可以针对不同子应用采用不同的隔离策略 const apps = [ { name: 'simple-app', // 简单应用使用实验性隔离 props: { experimentalStyleIsolation: true } }, { name: 'complex-app', // 复杂应用使用自定义隔离方案 props: { sandbox: { experimentalStyleIsolation: true, // 添加额外的样式处理 stylePatching: (styles, appName) => { // 自定义样式处理逻辑 } } } } ];

8.3.3. 处理特殊场景

// 对于需要全局生效的样式,可以在主应用中设置 const globalStyles = ` /* 这些样式将对所有应用生效 */ :root { --primary-color: `#1890ff;` } /* 弹窗层级管理 */ .global-modal { z-index: 1000; } `; // 在主应用中注入 const style = document.createElement('style'); style.textContent = globalStyles; document.head.appendChild(style);

8.4. 总结:

  • strictStyleIsolation 隔离更彻底,但兼容性问题多
  • experimentalStyleIsolation 兼容性更好,但隔离不够彻底
  • 建议根据应用场景选择合适的方案,或混合使用
  • 对于复杂场景,可能需要自定义隔离方案

8.5. 实现原理:

8.5.1. Shadow DOM 隔离:

class ShadowDOM { constructor(app) { const container = document.querySelector(app.container); this.shadow = container.attachShadow({ mode: 'closed' }); } mount(html) { this.shadow.innerHTML = html; } }

8.5.2. 作用域隔离:

function scopedCSS(styleSheet, appName) { const prefix = `[data-qiankun="${appName}"]`; return styleSheet.replace(/([^}{]*){/g, (match, selector) => { return `${prefix} ${selector} {`; }); }

9. 性能优化

乾坤采用多种优化策略:

// 1. 预加载 prefetchApps([ { name: 'app1', entry: '//localhost:8080' } ]); // 2. 资源缓存 const cache = new Map(); async function loadWithCache(url) { if (cache.has(url)) { return cache.get(url); } const resource = await fetch(url); cache.set(url, resource); return resource; } // 3. 并行加载 async function loadResources(resources) { return Promise.all( resources.map(url => loadWithCache(url)) ); }

优化要点:

  1. 应用预加载
  2. 资源缓存复用
  3. 并行加载优化
  4. 按需加载策略

10. 错误处理

乾坤提供完整的错误处理机制:

registerMicroApps([{ name: 'app1', entry: '//localhost:8080', loader: (loading) => render(loading), errorBoundary: (error) => { // 渲染错误页面 renderError(error); // 上报错误 reportError(error); } }]); // 全局错误监听 addGlobalUncaughtErrorHandler((event) => { console.log(event); // 处理全局未捕获错误 });

错误处理策略:

  1. 应用加载错误
  2. 运行时错误
  3. 生命周期错误
  4. 资源加载错误

11. 弹框问题常见解决方案:

11.1. 常见弹框问题

  1. 样式丢失问题
    • 当弹框设置了append-to-body时,会脱离子应用的样式作用域
    • 弹框被插入到主应用的 body 下,导致样式失效
    • 特别是在使用Element UI 等组件库的Dialog组件时最为常见
  2. 定位偏移问题
    • 使用fixed定位的弹框可能出现位置偏移
    • 依赖popper.js的组件(如Select下拉框)在弹框中位置错误

11.2. 解决方案

11.2.1. 样式隔离方案

// 方案一:重写append方法 const originAppend = HTMLElement.prototype.append; HTMLElement.prototype.append = function(...args) { // 如果是弹窗元素,则添加特定的类名 if (args[0]?.classList?.contains('el-dialog__wrapper')) { args[0].classList.add('子应用命名空间'); } return originAppend.apply(this, args); };
/* 方案二:添加样式作用域 */ .子应用命名空间 { .el-dialog__wrapper { /* 弹框样式 */ } .el-dialog { /* 弹框内容样式 */ } }

11.2.2. 容器挂载方案

// 方案三:指定挂载容器 // 在子应用中创建专门的弹框容器 const modalContainer = document.createElement('div'); modalContainer.id = 'modal-container'; document.body.appendChild(modalContainer); // 弹框配置 { appendTo: '#modal-container' }

11.2.3. Element UI 弹框解决方案

// 方案四:修改弹框配置 export default { mounted() { // 获取当前子应用的容器 const container = document.querySelector('#子应用容器ID'); this.$dialog = this.$createElement('el-dialog', { props: { // 禁用append-to-body appendToBody: false, // 指定挂载点 modalAppendToBody: false } }); // 手动挂载到子应用容器 container.appendChild(this.$dialog.$el); } }

11.2.4. 全局样式处理

// 方案五:全局样式处理 // 在主应用中 import { registerMicroApps } from 'qiankun'; registerMicroApps([ { name: 'subApp', entry: '//localhost:8080', container: '#container', props: { sandbox: { // 开启严格样式隔离 strictStyleIsolation: true, // 实验性样式隔离 experimentalStyleIsolation: true } } } ]);

11.3. 最佳实践建议

  1. 避免使用append-to-body
// 不推荐 <el-dialog append-to-body> // 推荐 <el-dialog :append-to-body="false">
  1. 使用动态挂载点
// 在子应用中创建动态挂载点 const createModalContainer = () => { const container = document.createElement('div'); container.setAttribute('data-modal-container', ''); return container; }; // 在弹框组件中使用 { mounted() { this.modalContainer = createModalContainer(); document.body.appendChild(this.modalContainer); }, beforeDestroy() { this.modalContainer.remove(); } }
  1. 样式作用域处理
// 使用命名空间 .sub-app-namespace { // 弹框样式覆盖 .el-dialog__wrapper { position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 2000; } // 遮罩层样式 .el-dialog__overlay { position: fixed; top: 0; right: 0; bottom: 0; left: 0; background-color: rgba(0, 0, 0, 0.5); } }
  1. 位置修正
// 处理弹框位置偏移 const fixModalPosition = (modal) => { const { top, left } = modal.getBoundingClientRect(); const offsetTop = window.innerHeight - top; const offsetLeft = window.innerWidth - left; modal.style.transform = `translate(${offsetLeft}px, ${offsetTop}px)`; };

通过以上方案的组合使用,可以有效解决微前端中的弹框问题。建议根据具体场景选择合适的解决方案,并在开发中注意以下几点:

  • 优先考虑不使用append-to-body
  • 合理使用样式隔离机制
  • 注意弹框的层级管理
  • 保持子应用的独立性