文章

qiankun原理深度解析

qiankun原理深度解析

一句话概括

qiankun 是基于 single-spa 封装的微前端框架,通过 HTML Entry 方式加载子应用、Proxy/快照沙箱隔离全局变量和样式作用域,以及完善的生命周期管理,解决了微前端落地中最核心的隔离和加载问题。本文从源码层面拆解 qiankun 的沙箱实现原理、子应用加载流程、样式隔离策略和通信机制,并给出完整的适配示例和性能优化实战。

背景与意义

qiankun 是国内微前端领域使用最广泛的框架,由蚂蚁集团开源。它的核心贡献在于:在 single-spa 的 JS Entry 基础上提出了 HTML Entry 方案,并内置了完善的沙箱隔离机制。理解 qiankun 的原理,不仅是掌握一个框架的使用,更是理解”浏览器环境中如何实现应用隔离”这一底层计算机问题。面试中,qiankun 的问题通常比 single-spa 更深入,典型问题包括:”qiankun 的沙箱是怎么实现的?”“为什么 qiankun 要用 HTML Entry?”“qiankun 如何解决样式污染?”

概念与定义

HTML Entry

子应用以 HTML 文件为入口,qiankun 通过 fetch 获取 HTML,解析其中的 JS、CSS 资源并动态加载。相比 single-spa 的 JS Entry(子应用自行打包为 UMD),HTML Entry 让子应用几乎零改造即可接入。

沙箱隔离 (Sandbox)

qiankun 在子应用激活时创建一个隔离的运行环境,拦截子应用对 window 的读写操作,防止全局变量污染主应用和其他子应用。

快照沙箱 (SnapshotSandbox)

基于”进入时保存快照,退出时恢复”的沙箱模式,适用于不支持 Proxy 的旧浏览器。

Proxy 沙箱 (LegacyProxySandbox)

基于 ES6 Proxy 的沙箱模式,通过 Proxy 拦截 window 操作,将子应用的全局变量隔离到一个”假的 window”中。

样式隔离 (Scoped CSS)

qiankun 通过实验性的 experimentalStyleIsolationstrictStyleIsolation 选项,在子应用挂载后自动为 CSS 规则加上选择器前缀。

核心知识点拆解

1. HTML Entry 加载机制

qiankun 最具标志性的特性就是 HTML Entry。它让子应用以”几乎零改造”的方式接入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
// qiankun HTML Entry 加载流程的核心实现(简化版)
class HTMLContentLoader {
  constructor() {
    this.cachedHTML = new Map();
  }

  // 1. 获取子应用的 HTML 内容
  async loadEntry(appName, entryUrl) {
    console.log(`[HTML Entry] 加载子应用 ${appName} 入口: ${entryUrl}`);
    
    // 缓存策略
    if (this.cachedHTML.has(appName)) {
      return this.cachedHTML.get(appName);
    }

    // 请求 HTML
    const response = await fetch(entryUrl, {
      credentials: 'include',  // 携带 cookie
    });
    
    if (!response.ok) {
      throw new Error(`加载子应用入口失败: ${response.status}`);
    }
    
    const html = await response.text();
    
    // 2. 解析 HTML
    const { template, scripts, styles } = this.parseHTML(html, entryUrl);
    
    const result = { template, scripts, styles };
    this.cachedHTML.set(appName, result);
    
    return result;
  }

  // 2. 解析 HTML → 提取 template + scripts + styles
  parseHTML(html, entryUrl) {
    const parser = new DOMParser();
    const doc = parser.parseFromString(html, 'text/html');
    
    // 获取入口 URL 的目录部分(用于解析相对路径)
    const baseUrl = entryUrl.substring(0, entryUrl.lastIndexOf('/') + 1);
    
    // 提取所有 script 标签
    const scripts = [];
    doc.querySelectorAll('script').forEach((script) => {
      const src = script.getAttribute('src');
      if (src) {
        // 处理相对路径
        scripts.push({
          src: src.startsWith('http') ? src : baseUrl + src,
          type: script.getAttribute('type') || 'text/javascript',
          async: script.hasAttribute('async'),
          defer: script.hasAttribute('defer'),
        });
      }
    });

    // 提取所有 link 样式和 style 标签
    const styles = [];
    doc.querySelectorAll('link[rel="stylesheet"]').forEach((link) => {
      const href = link.getAttribute('href');
      if (href) {
        styles.push({
          href: href.startsWith('http') ? href : baseUrl + href,
        });
      }
    });
    
    doc.querySelectorAll('style').forEach((style) => {
      styles.push({
        content: style.textContent,
      });
    });

    // 提取 body 内容作为模板,并处理资源路径
    const bodyContent = doc.querySelector('body');
    let template = '';
    if (bodyContent) {
      template = bodyContent.innerHTML;
      // 将模板中的相对路径转为绝对路径
      template = template.replace(/(src|href)=["'](?!http|\/\/|data:)([^"']+)["']/g, 
        (match, attr, path) => `${attr}="${baseUrl}${path}"`);
    }

    return { template, scripts, styles };
  }

  // 3. 将模板和样式渲染到容器
  renderTemplate(container, template, styles) {
    const containerEl = document.querySelector(container);
    if (!containerEl) return;

    // 先加载样式
    styles.forEach((style) => {
      if (style.href) {
        // 外部样式
        const link = document.createElement('link');
        link.rel = 'stylesheet';
        link.href = style.href;
        link.setAttribute('data-app', 'qiankun-style');
        containerEl.appendChild(link);
      } else if (style.content) {
        // 内联样式
        const styleEl = document.createElement('style');
        styleEl.textContent = style.content;
        styleEl.setAttribute('data-app', 'qiankun-style');
        containerEl.appendChild(styleEl);
      }
    });

    // 渲染 HTML 模板
    containerEl.innerHTML = template;
  }

  // 4. 顺序执行脚本(维护执行顺序)
  async executeScripts(scripts, appName) {
    // 先加载所有脚本内容
    const scriptContents = await Promise.all(
      scripts.map(async (script) => {
        if (script.src) {
          const response = await fetch(script.src, {
            credentials: 'include',
          });
          if (response.ok) {
            return { ...script, content: await response.text() };
          }
        }
        return null;
      })
    );

    // 然后按顺序执行(保证依赖关系)
    for (const script of scriptContents) {
      if (!script) continue;
      
      // 使用 eval 执行(在沙箱环境中)
      // 注意:这里简化了,实际 qiankun 会在沙箱上下文中执行
      (0, eval)(script.content);
    }
  }

  // 完整流程
  async loadAndRender(appName, entryUrl, container) {
    const { template, scripts, styles } = await this.loadEntry(appName, entryUrl);
    
    this.renderTemplate(container, template, styles);
    
    // 脚本加载需要在沙箱上下文执行
    // 实际 qiankun 中会有更复杂的沙箱集成
    console.log(`[HTML Entry] 子应用 ${appName} 渲染完成,待执行 ${scripts.length} 个脚本`);
    
    return { template, scripts, styles };
  }
}

// qiankun 中 registerMicroApps 的使用示例
import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
  {
    name: 'react-app',
    entry: '//localhost:3000',   // HTML Entry URL
    container: '#sub-app-container',
    activeRule: '/app/react',
    props: {
      // 自定义属性
      globalBaseUrl: '/base',
    },
  },
  {
    name: 'vue-app',
    entry: '//localhost:8080',
    container: '#sub-app-container',
    activeRule: '/app/vue',
  },
]);

start({
  sandbox: {
    experimentalStyleIsolation: true,
  },
  singular: true,    // 同一时间只显示一个子应用
  prefetch: 'all',   // 预加载所有子应用
});

2. 沙箱隔离机制(核心中的核心)

沙箱隔离是 qiankun 最具技术含量的部分。它解决了微前端中最令人头疼的”全局变量污染”问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
// qiankun 沙箱核心实现源码解析

// === 沙箱基类 ===
class Sandbox {
  constructor(appName) {
    this.appName = appName;
    this.sandboxRunning = false;
    this.addedPropsMap = new Map();     // 子应用新增的全局变量
    this.modifiedPropsMap = new Map();  // 子应用修改的全局变量
    this.currentSandboxProxy = null;
  }

  active() { this.sandboxRunning = true; }
  inactive() { this.sandboxRunning = false; }
}

// === 快照沙箱(SnapshotSandbox)- 兼容旧浏览器 ===
// 原理:进入时复制一份 window 快照,退出时恢复
class SnapshotSandbox extends Sandbox {
  constructor(appName) {
    super(appName);
    this.windowSnapshot = {};
    this.modifyPropsMap = {};
  }

  active() {
    // 保存 window 快照
    this.windowSnapshot = {};
    for (const prop in window) {
      if (window.hasOwnProperty(prop)) {
        this.windowSnapshot[prop] = window[prop];
      }
    }

    // 恢复上一次的修改
    Object.keys(this.modifyPropsMap).forEach((prop) => {
      window[prop] = this.modifyPropsMap[prop];
    });

    super.active();
  }

  inactive() {
    // 对比 window 的变化,记录修改
    this.modifyPropsMap = {};
    for (const prop in window) {
      if (window.hasOwnProperty(prop)) {
        if (window[prop] !== this.windowSnapshot[prop]) {
          // 记录子应用对 window 的修改
          this.modifyPropsMap[prop] = window[prop];
          // 恢复为快照值
          window[prop] = this.windowSnapshot[prop];
        }
      }
    }

    super.inactive();
  }
}

// === Proxy 沙箱(LegacySandbox)- 主流方案 ===
// 原理:用 Proxy 拦截 window 操作,将修改映射到一个假的 window 上
class LegacySandbox extends Sandbox {
  constructor(appName) {
    super(appName);

    // 存储被修改的属性和它们的值
    this.modifiedPropsMap = new Map();
    // 当前沙箱新增的属性
    this.addedPropsMap = new Map();
    // 原始值的备份
    this.originalPropsMap = new Map();

    const sandbox = this;

    // 创建一个假的 window(使用 Proxy)
    const proxy = new Proxy(window, {
      // 拦截 get 操作
      get(target, prop, receiver) {
        if (!sandbox.sandboxRunning) {
          // 沙箱未激活时,直接访问真 window
          return target[prop];
        }

        // 先从修改映射中读取
        if (sandbox.modifiedPropsMap.has(prop)) {
          return sandbox.modifiedPropsMap.get(prop);
        }

        // 再从新增映射中读取
        if (sandbox.addedPropsMap.has(prop)) {
          return sandbox.addedPropsMap.get(prop);
        }

        // 最后从真 window 读取
        const value = target[prop];
        return value;
      },

      // 拦截 set 操作
      set(target, prop, value, receiver) {
        if (!sandbox.sandboxRunning) {
          target[prop] = value;
          return true;
        }

        // 检查属性原来是否存在
        if (!target.hasOwnProperty(prop)) {
          // 新增属性
          sandbox.addedPropsMap.set(prop, value);
        } else if (!sandbox.modifiedPropsMap.has(prop)) {
          // 首次修改:保存原始值
          sandbox.originalPropsMap.set(prop, target[prop]);
          sandbox.modifiedPropsMap.set(prop, value);
        } else {
          // 后续修改:更新修改值
          sandbox.modifiedPropsMap.set(prop, value);
        }

        return true;
      },

      // 拦截 delete 操作
      deleteProperty(target, prop) {
        if (!sandbox.sandboxRunning) {
          delete target[prop];
          return true;
        }

        if (target.hasOwnProperty(prop)) {
          sandbox.modifiedPropsMap.set(prop, undefined);
        } else {
          sandbox.addedPropsMap.delete(prop);
        }
        return true;
      },

      // 拦截 has 操作(`prop in window`)
      has(target, prop) {
        return sandbox.addedPropsMap.has(prop) || 
               sandbox.modifiedPropsMap.has(prop) || 
               prop in target;
      },

      // 拦截 getOwnPropertyDescriptor
      getOwnPropertyDescriptor(target, prop) {
        if (sandbox.addedPropsMap.has(prop)) {
          return {
            configurable: true,
            enumerable: true,
            value: sandbox.addedPropsMap.get(prop),
            writable: true,
          };
        }
        if (sandbox.modifiedPropsMap.has(prop)) {
          return {
            configurable: true,
            enumerable: true,
            value: sandbox.modifiedPropsMap.get(prop),
            writable: true,
          };
        }
        return Object.getOwnPropertyDescriptor(target, prop);
      },

      // 拦截 Object.keys
      ownKeys(target) {
        const keys = Reflect.ownKeys(target);
        return [...keys, ...sandbox.addedPropsMap.keys()];
      },
    });

    this.proxy = proxy;
  }

  active() {
    // 不激活状态下直接访问真 window
    if (!this.sandboxRunning) {
      // 子应用入口执行时,通过将 window 指向 proxy 完成隔离
      this.currentSandboxProxy = this.proxy;
    }
    super.active();
  }

  inactive() {
    // 退出沙箱:恢复原始值
    this.originalPropsMap.forEach((value, prop) => {
      window[prop] = value;
    });

    // 清除新增的属性
    this.addedPropsMap.forEach((_, prop) => {
      delete window[prop];
    });

    super.inactive();
  }
}

// === 多实例沙箱(用于同时加载多个子应用) ===
class MultiInstanceSandbox extends Sandbox {
  constructor(appName) {
    super(appName);
    this.sandboxProxy = new Proxy(window, {
      // 与 LegacySandbox 类似的实现
      // 但支持多个沙箱实例同时存在
      // 每个子应用有自己的 proxy
    });
  }
}

// qiankun 中沙箱的使用方式
function createSandbox(appName, useLegacySandbox = false) {
  if (window.Proxy) {
    // 支持 Proxy:使用更高效的 LegacyProxySandbox
    return useLegacySandbox 
      ? new LegacySandbox(appName)
      : new MultiInstanceSandbox(appName); // qiankun 2.x 默认使用
  } else {
    // 不支持 Proxy(如 IE11):使用快照沙箱
    return new SnapshotSandbox(appName);
  }
}

3. 样式隔离方案

qiankun 提供了两种样式隔离机制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
// qiankun 样式隔离实现分析

// === 方法 1: experimentalStyleIsolation(实验性样式隔离)===
// 原理:为子应用的 CSS 选择器添加容器前缀
// 例如:.btn { color: red } → div[data-qiankun-app-name] .btn { color: red }
function experimentalStyleIsolation(appName, containerSelector) {
  const container = document.querySelector(containerSelector);
  if (!container) return;

  // 给容器添加一个自定义属性作为命名空间
  container.setAttribute('data-qiankun', appName);

  // 劫持 style 标签的插入
  const observer = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      mutation.addedNodes.forEach((node) => {
        if (node.nodeName === 'STYLE' || node.nodeName === 'LINK') {
          // 确保样式在容器内
          if (container.contains(node)) {
            if (node.nodeName === 'STYLE') {
              // 给 CSS 规则加前缀
              scopeStyleRule(node, containerSelector);
            }
          }
        }
      });
    });
  });

  observer.observe(container, {
    childList: true,
    subtree: true,
  });

  return observer;
}

function scopeStyleRule(styleElement, scopeSelector) {
  const sheet = styleElement.sheet;
  if (!sheet) return;

  try {
    // 遍历所有 CSS 规则
    for (let i = 0; i < sheet.cssRules.length; i++) {
      const rule = sheet.cssRules[i];
      if (rule.selectorText) {
        // 为每个选择器添加容器前缀
        const newSelector = rule.selectorText
          .split(',') // 处理组合选择器
          .map((selector) => {
            // 排除 body/html 等全局选择器
            if (selector.trim() === 'body' || selector.trim() === 'html') {
              return selector;
            }
            // 不包含容器选择器时再加前缀
            if (!selector.includes(scopeSelector)) {
              return `${scopeSelector} ${selector.trim()}`;
            }
            return selector.trim();
          })
          .join(', ');

        rule.selectorText = newSelector;
      }
    }
  } catch (e) {
    // CSSStyleSheet.cssRules 在跨域时不可访问
    console.warn('样式隔离失败(可能跨域):', e);
  }
}

// === 方法 2: strictStyleIsolation(严格样式隔离)===
// 原理:使用 Shadow DOM 的天然隔离
function strictStyleIsolation(appName, containerSelector) {
  const container = document.querySelector(containerSelector);
  if (!container) return;

  // 创建 Shadow Root
  const shadowRoot = container.attachShadow({ mode: 'open' });

  // 将子应用的 DOM 移动到 Shadow Root 中
  // 子应用的样式被限制在 Shadow DOM 内,与外界完全隔离
  // 缺点:弹窗、Tooltip 等挂载到 body 的 DOM 会逃逸
  
  return {
    shadowRoot,
    // 接管子应用的挂载目标
    getMountContainer() {
      const mountPoint = document.createElement('div');
      shadowRoot.appendChild(mountPoint);
      return mountPoint;
    },
  };
}

// === 样式隔离的局限和补偿方案 ===
// 即使使用了 strictStyleIsolation,子应用的弹窗元素
// 挂载到 document.body 时仍然会跑到 Shadow DOM 外面
// 解决方案:
// 1. 子应用将所有动态挂载的元素挂载到 shadowRoot 内
// 2. 使用 Portal(React)或 Teleport(Vue)指定挂载目标

// React Portal 修复弹窗隔离
function FixedModal({ children, shadowRoot }) {
  return ReactDOM.createPortal(
    children,
    shadowRoot || document.getElementById('modal-root')
  );
}

// qiankun 配置中的样式隔离选项
start({
  sandbox: {
    // 两种样式隔离
    experimentalStyleIsolation: false, // CSS 选择器前缀
    strictStyleIsolation: true,        // Shadow DOM
  },
});

4. 生命周期管理与应用通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// qiankun 生命周期的完整流程
// 注册 → 加载 → 挂载 → 更新(可选) → 卸载 → 销毁

// 子应用需要导出的生命周期
// ./src/micro-entry.js
let appInstance = null;

export async function bootstrap() {
  // 【阶段1: 引导】应用第一次加载时执行一次
  // 不要在这里做 DOM 操作
  console.log('[QiankunSubApp] bootstrap');
  // 适合做:全局配置初始化、获取运行时配置
}

export async function mount(props) {
  // 【阶段2: 挂载】每次激活时执行
  console.log('[QiankunSubApp] mount', props);
  const { container, onGlobalStateChange, setGlobalState } = props;
  
  // 创建应用实例
  appInstance = createApp(App);
  appInstance.use(router);
  
  // 监听全局状态
  onGlobalStateChange((state, prev) => {
    console.log('[QiankunSubApp] 全局状态变化:', state, prev);
    // 更新应用内的 store
    store.dispatch('globalStateUpdated', state);
  }, true);

  // 挂载到容器
  appInstance.mount(container.querySelector('#app'));
}

export async function unmount(props) {
  // 【阶段3: 卸载】切换走时执行
  console.log('[QiankunSubApp] unmount', props);
  appInstance.unmount();
  
  // 清理副作用
  appInstance = null;
}

export async function update(props) {
  // 【阶段4: 更新】(可选)子应用状态更新时执行
  console.log('[QiankunSubApp] update', props);
}

// 基座中的通信使用
import { initGlobalState } from 'qiankun';

// 初始化全局状态
const actions = initGlobalState({
  user: { name: '张三', id: 123 },
  theme: 'dark',
  token: 'xxxxx',
});

// 监听状态变化
actions.onGlobalStateChange((state, prev) => {
  console.log('[主应用] 全局状态变化:', state, prev);
});

// 更新全局状态
function updateUser(userInfo) {
  actions.setGlobalState({ user: userInfo });
}

实战案例:完整 qiankun 集成

以下是接入 qiankun 的一个完整案例,包含基座改造、子应用改造、沙箱配置和性能优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
// ============ 1. 主应用改造(基座)============
// micro-host/src/app.js

import { registerMicroApps, start, initGlobalState } from 'qiankun';
import { LoadingComponent } from './components/Loading';

// 1.1 初始化全局状态
const initialState = {
  user: null,
  token: localStorage.getItem('token'),
  theme: 'light',
};

const actions = initGlobalState(initialState);
actions.onGlobalStateChange((state, prev) => {
  console.log('[基座] 全局状态变更:', state, prev);
});

// 1.2 注册子应用配置(支持预加载和错误处理)
const apps = [
  {
    name: 'dashboard',          // 必填,唯一
    entry: '//localhost:3001',  // 必填,子应用 HTML 入口
    container: '#subapp-viewport',  // 必填,挂载容器
    activeRule: '/dashboard',   // 必填,激活规则
    props: { baseUrl: '/dashboard', actions }, // 传递给子应用
    // 加载状态
    loader: (loading) => {
      // 显示/隐藏加载指示器
      if (loading) {
        LoadingComponent.show();
      } else {
        LoadingComponent.hide();
      }
    },
  },
  {
    name: 'products',
    entry: '//localhost:3002',
    container: '#subapp-viewport',
    activeRule: '/products',
    props: { baseUrl: '/products', actions },
  },
  {
    name: 'checkout',
    entry: '//localhost:3003',
    container: '#subapp-viewport',
    activeRule: '/checkout',
    props: { baseUrl: '/checkout', actions },
  },
];

// 1.3 注册应用
registerMicroApps(apps, {
  // 生命周期钩子(全局)
  beforeLoad: [
    (app) => {
      console.log('[基座] beforeLoad:', app.name);
    },
  ],
  beforeMount: [
    (app) => {
      console.log('[基座] beforeMount:', app.name);
    },
  ],
  afterUnmount: [
    (app) => {
      console.log('[基座] afterUnmount:', app.name);
    },
  ],
});

// 1.4 启动(关键配置)
start({
  sandbox: {
    sandbox: true,                    // 开启沙箱
    experimentalStyleIsolation: true, // 实验性样式隔离
    strictStyleIsolation: false,      // 不开启 Shadow DOM
  },
  singular: true,         // 单实例模式(默认)
  fetch: window.fetch,    // 自定义 fetch,可以添加 header
  prefetch: 'all',        // 预加载所有子应用
  urlRerouteOnly: true,   // 仅 URL 变更时重新路由
});

// ============ 2. 子应用改造(以 React 为例)============
// micro-sub-app/src/public-path.js

// 2.1 动态设置 webpack publicPath
if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

// micro-sub-app/src/index.js
import './public-path';
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

let root = null;

function render(props) {
  const { container } = props;
  const dom = container 
    ? container.querySelector('#root') 
    : document.getElementById('root');
  
  root = ReactDOM.createRoot(dom);
  root.render(
    <React.StrictMode>
      <App basename={props.baseUrl || '/'} />
    </React.StrictMode>
  );
}

// 独立运行时(开发模式)
if (!window.__POWERED_BY_QIANKUN__) {
  render({});
}

// qiankun 生命周期导出
export async function bootstrap() {
  console.log('[子应用 React] bootstrap');
}

export async function mount(props) {
  console.log('[子应用 React] mount', props);
  // 接收基座传递的通信 API
  props.onGlobalStateChange?.((state, prev) => {
    console.log('[子应用] 全局状态变更:', state);
    // 更新 redux store
  }, true);
  
  render(props);
}

export async function unmount(props) {
  console.log('[子应用 React] unmount');
  if (root) {
    root.unmount();
    root = null;
  }
}

// 2.3 webpack 配置(关键)
// webpack.config.js
const packageName = require('./package.json').name;

module.exports = {
  output: {
    library: `${packageName}-[name]`,
    libraryTarget: 'umd',   // 必须使用 UMD
    chunkLoadingGlobal: `webpackJsonp_${packageName}`,  // 防止 JSONP 冲突
    globalObject: 'window',
  },
  devServer: {
    headers: {
      // CORS 必须允许跨域(微前端场景)
      'Access-Control-Allow-Origin': '*',
    },
  },
};

// ============ 3. 性能优化配置 ============
// 3.1 子应用预加载(用户 Hover 时触发)
function prefetchOnHover(linkSelector, appName) {
  document.querySelector(linkSelector)?.addEventListener('mouseenter', () => {
    // qiankun 的 prefetch 会在空闲时自动触发
    // 也可以通过手动加载强化
    import('qiankun').then(({ loadMicroApp }) => {
      loadMicroApp(apps.find(a => a.name === appName));
    });
  });
}

// 3.2 子应用缓存(keep-alive)
// qiankun 默认切换子应用时会卸载并清理 DOM
// 如果需要 keep-alive 效果,需要额外实现
// 方案:切换时隐藏而非卸载
function keepAliveApp(name) {
  const app = document.querySelector('#subapp-viewport');
  app.style.display = 'none'; // 隐藏
}

function showApp(name) {
  const app = document.querySelector('#subapp-viewport');
  app.style.display = 'block'; // 显示
}

底层原理

qiankun 如何让子应用的 JS 在沙箱上下文中执行?

核心技巧是”改造 eval”。当子应用的脚本被 fetch 回来后,qiankun 不会直接通过 <script> 标签加载,而是使用 eval() 在沙箱代理中执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// qiankun 沙箱中执行脚本的核心代码(简化)
function evalInSandbox(code, sandboxProxy) {
  // 1. 构造一个函数,参数列表包含所有全局变量
  // 将 window 替换为 sandboxProxy
  const keys = Object.keys(sandboxProxy);
  const values = keys.map(key => sandboxProxy[key]);
  
  // 2. 使用 Function 构造函数创建执行函数
  // 参数: window, self, globalThis → sandboxProxy
  // document → 真实 document(共享)
  // location → 真实 location
  const fn = new Function(
    'window', 'self', 'globalThis',
    'document', 'location',
    code
  );
  
  // 3. 在沙箱上下文中执行
  return fn.call(
    sandboxProxy,           // this
    sandboxProxy,           // window 参数
    sandboxProxy,           // self 参数
    sandboxProxy,           // globalThis 参数
    document,               // document(真实 document)
    location,               // location(真实 location)
  );
}

这里的关键是 new Function(...) 的第 4 个技巧——documentlocation 是共享的真实对象,因为 DOM 操作需要访问真实的 document,而路由切换需要 location。这也是 qiankun 沙箱的一个”不完美”之处——子应用如果直接操作 document.body,仍然会影响全局。

为什么 qiankun 选择 HTML Entry 而非 JS Entry?

JS Entry(single-spa 的默认方案)要求子应用将整个应用打包为一个单独的文件,并注册到 single-spa 注册表中。这意味着子应用需要修改构建配置输出 UMD 格式。HTML Entry 的核心优势:

  1. 零改造成本——子应用无需修改代码,只需提供 HTML 入口 URL
  2. 完整的资源管理——HTML 中的 CSS、JS 都会被正确加载
  3. 兼容性——任何技术栈、任何构建工具都能生产一个 HTML 文件
  4. 微前端无关——子应用本身不需要知道自己在微前端环境中运行

高频面试题解析

面试题 1:qiankun 的沙箱是如何防止子应用污染全局 window 的?Proxy 和快照两种方案的优缺点分别是什么?

答案要点: 快照沙箱(SnapshotSandbox)的原理是:激活时快照当前 window,退出时 diff 差异并恢复。优点是兼容所有浏览器(包括 IE11),缺点是污染了真 window(子应用运行期间对 window 的修改会影响一切),而且单次对比 window 的 CPU 开销大(window 可能有数千个属性)。Proxy 沙箱(LegacySandbox)的原理是:创建一个 Proxy 代理 window,子应用对 window 的操作全部指向这个 proxy,真正读写与 window 隔离。优点是不会污染真 window、性能更好,缺点是不支持 IE11。

面试题 2:qiankun 的样式隔离(experimentalStyleIsolation)的局限是什么?

答案要点: 1)CSSRule 的 selectorText 在跨域样式表上不可访问(CSSOM 安全性限制),因此跨域 CDN 样式无法 scoped;2)动态插入的 style 标签可能逃逸——MutationObserver 可能漏掉部分样式插入;3)子应用使用 CSS-in-JS(styled-components)时,样式通过 JS 运行时生成,scoped 机制无法覆盖;4)子应用中使用 appendChild 向 body 添加样式时,不会经过容器的 MutationObserver。

面试题 3:qiankun 的子应用通信机制是怎么实现的?有哪些局限?

答案要点: qiankun 通过 initGlobalState 创建一个全局状态实例,返回 setGlobalStateonGlobalStateChange 方法。基座通过 set 修改状态后,会遍历所有子应用的监听器并调用回调。局限:1)只能传递简单对象(无法传递函数/React 组件等复杂值);2)通信是一对多的广播模式,不支持精确的定向通知;3)没有状态持久化——刷新后状态消失;4)大数据量通信时会造成性能问题(所有子应用都触发 re-render)。

面试题 4:从 single-spa 到 qiankun,qiankun 做了哪些关键增强?

答案要点: 1)HTML Entry——子应用无需改造构建配置输出 UMD;2)沙箱隔离——JS 沙箱 + CSS scoped 解决了隔离问题;3)应用预加载——prefetch: 'all' 在空闲时提前加载子应用;4)错误边界——内置的 loader/error 处理机制;5)更完善的通信——initGlobalState 提供了一套全局状态管理;6)开箱即用——start 函数封装了所有复杂配置。

面试题 5:qiankun 如何处理子应用 JSONP 请求的全局回调函数污染?

答案要点: JSONP 会在 window 上注册回调函数(如 callback_123456),如果不加处理,多个子应用的同名回调会冲突。qiankun 的解决方案是:子应用的 window 被代理后,JSONP 的回调函数注册到的是沙箱的 proxy window 上,不同子应用的回调函数互不干扰。但是对于 JSONP 中使用了 document.createElement('script') 并依赖于全局 window 回调的情况,qiankun 会将 script 标签的全局上下文绑定到沙箱的 scope 中。

总结与扩展

知识体系图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
qiankun 核心
├── 加载机制(HTML Entry)
│   ├── Fetch HTML → 解析 template/script/style
│   ├── 动态创建 script 标签执行
│   └── 资源路径自动修正
├── 沙箱隔离
│   ├── 快照沙箱(兼容 IE,性能差)
│   ├── Proxy 沙箱(现代浏览器,高性能)
│   └── 多实例沙箱(同时加载多个子应用)
├── 样式隔离
│   ├── experimentalStyleIsolation(CSS 前缀)
│   │   └── MutationObserver + CSSRule.selectorText
│   └── strictStyleIsolation(Shadow DOM)
├── 生命周期
│   ├── bootstrap(引导,只执行一次)
│   ├── mount(挂载,每次激活执行)
│   ├── unmount(卸载,切换走时执行)
│   └── update(更新,可选)
├── 通信机制
│   ├── initGlobalState(基座创建)
│   ├── onGlobalStateChange(子应用订阅)
│   └── setGlobalState(任何一方修改)
└── 工程配置
    ├── registerMicroApps + start
    ├── prefetch 预加载
    ├── singular / fetch 定制
    └── UMD 输出配置

延伸阅读

  1. qiankun 官方文档: GitHub 搜索 umijs/qiankun — 官方源码和示例
  2. qiankun 沙箱源码分析: 深入 sandbox.tsproxySandbox.ts
  3. HTML Entry 源码分析: import-html-entry 包,qiankun 的核心依赖
  4. single-spa vs qiankun 对比: 理解两者的设计哲学差异
  5. 微前端样式隔离实践: CSS Scoped vs Shadow DOM vs CSS Modules
本文由作者按照 CC BY 4.0 进行授权