引言
严格来说,这篇文章应该在 2020年 年底发表出来,迟迟没有写原因很简单 - "懒"
纸上谈兵
微前端时间线
什么是微前端
微前端是一个设计理念,不是一个库或者框架
微前端的概念由 thoughtworks 在 2016 年提出。其核心思路是借鉴后端微服务架构思想,将一个庞大的巨石前端应用拆分为多个简单独立的前端工程。每个前端工程可以独立开发、测试、部署。最终再由一个主应用应用将所有子应用合并到一起,以一个完整的网站形式展现给用户。简单来说,微前端 【一拆一合】,拆的是复杂度、合的是视图和子应用之间的通用能力!
- 独立开发、降低代码耦合: 每个子应用由独立的前端团队开发,一般是独立仓库,这使得在代码维护、代码编译方面更加友好
- 独立部署: 团队可以自行控制负责子应用的研发、编译、测试、部署等,上线后自动更新内容且不影响其他模块。
- 技术栈无关、增量升级: 对于一个古老的项目来说这是很具有吸引力的,可以对项目进行增量技术迭代
- 独立运行时: 每个微应用之间状态隔离,运行时状态不共享
你可能不需要微前端
满足这部分的同学,可以控制台执行window.close()了
- 系统由单个小团队开发,拥有技术的绝对话语权
- 对于老系统,你们有足够精力是做技术改造 或 不去做技术改造
- 系统强耦合,拆分应用会带来更高的人力成本
为什么不是iframe
可以容忍下面几条的的同学,也可以控制台执行window.close()了
- url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。
- UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中..
- 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。
- 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。
(引用自 www.yuque.com/kuitos/gky7…
qiankun
源起
qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。在我看来和single-spa的主要区别在于html-enty、隔离,后面会讲到。
核心
- 📦 基于 single-spa 封装,提供了更加开箱即用的 API。
- 📱 技术栈无关,任意技术栈的应用均可 使用/接入,不论是 React/Vue/Angular/JQuery 还是其他等框架。
- 💪 HTML Entry 接入方式,让你接入微应用像使用 iframe 一样简单。
- 🛡 样式隔离,确保微应用之间样式互相不干扰。
- 🧳 JS 沙箱,确保微应用之间 全局变量/事件 不冲突。
- ⚡️ 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
说下js沙箱
SnapshotSandbox
:基于快照,适用于不支持 Proxy 的浏览器,有性能和污染全局 window 问题。LegacySandbox
:基于 Proxy 的单例模式,在激活和失活时进行环境恢复。ProxySandbox
:基于 Proxy 的单例和多例模式,通过对 fakewindow 代理实现隔离。
说下css沙箱
qiankun
的 css
沙箱的原理是重写 HTMLHeadElement.prototype.appendChild
事件,记录子项目运行时新增的 style/link
标签,卸载子项目时移除这些标签。
子应用之间样式隔离
子应用卸载会去掉dom(包含样式表),所以子应用之间不存在样式污染
父子应用之间样式隔离
- 父子应用qiankun也提供了解决方案,但是如果使用样式库会出现各种奇奇怪怪的问题。
- 需要借助样式库的命名空间,比如element-plus的 ElConfigProvider
- 如果组件库不支持定制命名空间,那你需要一款样式定制的插件了,比如post-css-namespace-plugin
主应用
1. 安装 qiankun
yarn add qiankun # 或者 npm i qiankun -S
2. 在主应用中注册微应用
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'react app', // app name registered
entry: '//localhost:7100',
container: '#yourContainer',
activeRule: '/yourActiveRule',
},
{
name: 'vue app',
entry: { scripts: ['//localhost:7100/main.js'] },
container: '#yourContainer2',
activeRule: '/yourActiveRule2',
},
]);
start();
当微应用信息注册完之后,一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑,所有 activeRule 规则匹配上的微应用就会被插入到指定的 container 中,同时依次调用微应用暴露出的生命周期钩子。
如果微应用不是直接跟路由关联的时候,你也可以选择手动加载微应用的方式:
import { loadMicroApp } from 'qiankun';
loadMicroApp({
name: 'app',
entry: '//localhost:7100',
container: '#yourContainer',
});
子应用
微应用不需要额外安装任何其他依赖即可接入 qiankun 主应用。
1. 导出相应的生命周期钩子
微应用需要在自己的入口 js (通常就是你配置的 webpack 的 entry js) 导出 bootstrap
、mount
、unmount
三个生命周期钩子,以供主应用在适当的时机调用。
/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
export async function bootstrap() {
console.log('react app bootstraped');
}
/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount(props) {
ReactDOM.render(<App />, props.container ? props.container.querySelector('#root') : document.getElementById('root'));
}
/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/
export async function unmount(props) {
ReactDOM.unmountComponentAtNode(
props.container ? props.container.querySelector('#root') : document.getElementById('root'),
);
}
/**
* 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
*/
export async function update(props) {
console.log('update props', props);
}
qiankun 基于 single-spa,所以你可以在这里找到更多关于微应用生命周期相关的文档说明。
无 webpack 等构建工具的应用接入方式请见这里
2. 配置微应用的打包工具
除了代码中暴露出相应的生命周期钩子之外,为了让主应用能正确识别微应用暴露出来的一些信息,微应用的打包工具需要增加如下配置:
webpack:
webpack v5:
const packageName = require('./package.json').name;
module.exports = {
output: {
library: `${packageName}-[name]`,
libraryTarget: 'umd',
chunkLoadingGlobal: `webpackJsonp_${packageName}`,
},
};
webpack v4:
const packageName = require('./package.json').name;
module.exports = {
output: {
library: `${packageName}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${packageName}`,
},
};
踩坑
请移步qiankun官网,很良心的文档!👍🏻
微前端在互联网大厂S级项目实战 - 2020年
背景
- 坐标:某互联网一线大厂
- 职责:主导微前端架构落地
- 项目背景
- 该项目是一个可预见的硕大工程/巨石应用
- 业务方面由三个团队共同建设,每个团队大致会出3-4人参与业务研发
- 类似历史项目存在 能力实现冗余、技术升级困难、团队对接成本高、构建时间长、发布风险高、系统体验差等问题 (这几个问题作为前端应该都能感同身受吧)
- 相同的能力实现N次,实现方式不一,升级困难
- 单次build 8分钟
- 构建出现OOM
- 热更新6秒+
- 单次发布上百页面
选型
架构设计-初步方案
- 责任划分:按照团队和业务将巨石应用拆分为一个主应用和三个子应用,主应用负责基础框架、子应用逻辑控制、系统UI结构
- 部署环境:完整的系统展示给用户
- 开发环境:只启动子应用,通过将header和sideBar抽象为npm组件使得本地开发可以有完整的UI视图(也仅仅是这样了)
方案并不完美
- 基本符合要求:按照常规方案来看,该方案已经可以落地了,咨询了我厂其他部门、外部大厂一些同学,基本上也都是这么落地,但我觉得这个方案有些鸡肋了...或者说只是用了,并没有用好
- 存在的问题:能力实现冗余、技术升级困难、团队对接成本高这三个问题并没有解决。简单来说,因为本地开发只启动子应用,不启动主应用,所以子应用间有很多通用能力不能很好的做出抽象和复用,带来了能力冗余、维护成本高等问题。
- 是否有一种完美方案,能在上述基础上,抽象子应用通用能力统一实现,并能和子应用友好的"交互"
思考
如果本地开发和部署环境运行时代码高度一致,我们抽象通用能力在主应用中实现,统一下发到子应用,那么问题将迎刃而解。
本地开发不需要启动主应用,但是却能使用主应用的能力,听起来匪夷所思,但、却是可行的!
方案升级-带壳开发
方案核心 - 本地开发时,使用部署环境的主应用加载子应用本地的devServer服务;部署环境加载远端子应用html
本地开发时页面url由http://localhost:8084
改为使用http://域名.com?LOCAL_PORT=8084
最终方案
最终方案实践
最终方案更多的是一种架构建设思路,旨在解决项目实际痛点,当时实践过程中遇到一些问题,篇幅问题直接写问题和解法,感兴趣可以私聊
踩坑1:子应用本地开发不支持HMR
原因:webpack devserver默认读取的socketHost是
/
,修改为localhost即可
解决方案:
// Webpack配置举例
devServer: {
sockHost: 'localhost'
}
踩坑2:部署环境无法加载本地静态资源
原因:webpack基于安全考虑禁止远端加载本地静态资源
解决方案:
// Webpack配置举例
devServer: {
allowHosts: ['.abc.com', '可信任域名'] // 支持通配符
}
踩坑3:部署环境加载静态资源地址不对
原因:webpack devserver 默认加载静态资源根目录是
/
解决方案:
// Webpack配置举例
output: {
publickPath: `/localhost:${port}`
}
收益
- 成功落地S级项目
- 抽象10+项基础能力,节省30+PD(9个子应用),且后续技术升级均平滑无阻塞
- 成功解耦【技术架构】和 【业务研发】,让业务同学专注于业务建设
- 三个团队共建巨石应用,共计9个子应用,上线三年无低效协同事宜
- 单次构建时间平均1min,HMR平均0.5s内
- 个人获得S绩效和个人之星奖
小结
没有最好的技术,只有更适合的技术,突破思想束缚,打造更适合项目的技术架构