前言
最近面试找工作,写项目经历的时候真的很煎熬,主要是翻来翻去觉得没啥可写的,想必很多人和我一样,当时整天忙于具体业务,无暇顾及其他,当时会议上也听 leader 讲解框架层面的东西,但都是一知半解,云里雾里。那个时候精力都用在理解工作目标、完成工作任务上了。
随着工作经验的增加,回头看过去的项目经历,我们或多或少已经有了一些新的知识储备和上下文信息,收集整理并验证这些信息,然后代入当时的处境里,站在全局的视角看待当时所处的位置和所做的事情,还是有很多可以深挖的点。
以下,以我过去工作参与过的一个微前端改造项目举例,希望给同样需要的你一点启发。
好几年前,在毕业刚进前司的时候,我们的项目迎来了一次微前端的改造,我有幸参与其中,具体的工作是将子项目改造后接入基座应用。
当时情况: YS大佬和GH老大选择的是 qiankun 框架,我主要负责子应用接入,有什么搞不定的地方都是直接请教大佬。
需要重新思考的是:
- 子应用具体是怎么接入主应用的?子应用加载、初始化的时机?
- 当时微前端架构方案有哪些?为什么是qiankun?要解决的问题是什么?
- 时至今日,是否有其他更好的方案?如果现在你遇到同样的问题,你会怎么做?
- qiankun的原理是什么?怎么做样式隔离和JS隔离的?子应用和子应用间,子应用和主应用间是如何通信的?
- 你当时在接入的时候有没有遇到什么问题?怎么解决的?
1. 子应用接入和初始化时机
| 接入方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| HTML Entry | 天然支持CSS隔离 | 需子应用提供完整HTML | 旧项目改造、多技术栈共存 |
| JS Entry | 构建产物更轻量 | 需手动处理CSS隔离 | 新项目、技术栈统一 |
初始化时机:
- 路由匹配:主应用监听路由变化,当路径匹配子应用激活规则时加载。
- 手动触发:通过
loadMicroAppAPI主动加载。
2. 微前端方案对比与选型
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| iframe | 天然隔离,简单易用 | 通信困难,SEO不友好 | 第三方页面嵌入 |
| npm包 | 版本控制严格 | 技术栈耦合,更新需主应用发布 | 内部组件库复用 |
| single-spa | 框架无关,灵活度高 | 需手动处理沙箱隔离 | 技术栈混合项目 |
| qiankun | 开箱即用的沙箱隔离,HTML Entry | 子应用改造成本较高 | 大型复杂应用拆分 |
-
选择qiankun的原因:
- 沙箱隔离:JS/CSS隔离避免污染。
- 技术栈无关:支持不同框架子应用共存。
- 独立部署:子应用可单独发布。
3. webpack 的模块联邦
需要升级到 webpack 5+,
4. qiankun 的核心原理
qiankun的沙箱机制类似于‘虚拟机’,为每个子应用创建独立的运行环境,卸载时自动‘快照恢复’。 基于single-spa 进行的二次封装,核心原理是运行时的沙箱隔离和通信机制:
-
沙箱隔离:
-
JS隔离:通过Proxy代理全局对象(如window),记录/恢复子应用运行时环境。
-
CSS隔离:
- Shadow DOM:严格隔离(qiankun可选配置)。
- Scoped CSS:动态添加/移除样式表。
- 动态样式表隔离存在局限性(如
@font-face污染)
-
-
通信机制: 主应用和子应用通信:
- 全局状态:通过
initGlobalState实现父子应用通信。 - 自定义事件:利用
window.dispatchEvent跨应用通信。
- 全局状态:通过
// 主应用初始化状态
const actions = initGlobalState({ user: null });
// 子应用监听状态变化
actions.onGlobalStateChange((state) => {
console.log('用户信息变更:', state.user);
});
子应用间通信:
// 子应用A发布事件
window.dispatchEvent(new CustomEvent('app-event', {
detail: { type: 'data-update' }
}));
// 子应用B监听
window.addEventListener('app-event', (e) => {
console.log(e.detail); // 避免直接传递函数引用
});
5、遇到的问题
- 子应用修改
window全局对象导致状态污染。针对这个问题,可以从 隔离机制、代码改造 两个方面解决:
- 注册子应用时,可以开启配置严格的沙箱模式,并限制子应用对特定全局变量的访问。
- 尽量使用局部变量,不修改全局变量,如果要共享数据,可以在主应用维护一套eventBus,通过监听变化和回调来。
- 必须使用全局变量时,使用 qiankun 提供的生命周期钩子,在子应用的
mount和unmount周期中管理全局状态:
// 子应用 entry.js
export async function mount(props) {
// 挂载时临时设置全局变量
window.__TEMP_VAR__ = 'value';
}
export async function unmount() {
// 卸载时清理全局变量
delete window.__TEMP_VAR__;
}
- 也可以直接设置 ESLint 规则,禁止直接修改 window。
- 子应用公用的第三方库和依赖版本不一致会导致冲突,需要将公共依赖库的版本调至一致,一般来说就是升级旧版本的依赖包到新版本。如果实在无法升级例如需要vue2 和 vue3 共存,可以使用webpack external进行版本隔离。
- 依赖冲突:
Webpack externals 的配置示例需补充:
// webpack.config.js
externals: {
vue: 'Vue', // 通过CDN引入,避免打包
react: 'React'
}
- 子应用资源跨域的问题, 需要Nginx配置跨域问题。