老李公司8年的前端了,最初的项目使用当下流行的angularjs,现在新来的小李,接手了部分angularjs的项目的维护,刚学的vue3确使不上。。。。。。
本来就不会angularjs的小李,只能硬着头皮基础入门下这个框架,总算能解决日常需求,但是遇到一些问题时,解决起来还是比较棘手,一边上网搜索着解决方案,一边如果思考怎么改变旧项目维护难的局面。
某天,刚挤上地铁的小李看了下手机,前几天加入的Fes.js微信群推送了微前端乾坤插件。Fesjs之前使用过就知道是一个好用的前端应用解决方案,现在推出的微前端插件,或许可以一试,于是开启了这段有意思的旅程。
第一站:思考
微前端理念类似于微服务,概念这里不多叙,主要关注下微前端特点:
- 技术栈无关
- 独立开发,独立部署
- 增量升级
- 独立运行时
技术栈无关保证了不再维护古老 angular 的可行性,增量升级则为项目的逐步迁移和功能模块迭代提供了可行性,完全木有后顾之忧呀~小李窃喜,立马着手研究了一番,各迁移方案对比如下:
| 迁移方案 | 难度 | 工作量 |
|---|---|---|
| 不使用微前端,直接迁移 | 一次性迁移,时间周期长,增量需求冲突 | 大 |
| 新框架做主应用, 老项目、新功能做微应用 | 公用逻辑优先迁移到主应用,老项目需要处理与主应用的公共逻辑交互,老功能可拆分迁移、新需求无冲突 | 较大 |
| 老项目做主应用, 新功能做微应用 | 公共逻辑迁移可后置,老功能可拆分迁移,新需求无冲突 | 小 |
无疑,无论是从新老功能的拆分、迭代,还是从工作量来看,第三种方案都是最佳方案。
第二站:实践
小李开始着手实践,将基于 angularjs 的老项目作为主应用,微前端通过Fes.js实现。迁移某功能模块的具体流程如下:
主应用
1.安装qiankun依赖
<script src="/qiankun/dist/index.umd.js"></script>
2.封装加载微应用组件
function micro() {
return {
restrict: 'EA',
scope: {
name: '@', // 微应用名称
entry: '@', // 微应用入口
props: '=' // 传递给微应用的属性
},
template: `
<div class="micro-container">
<div ng-if="vm.error" style="color: red;">{{vm.error}}</div>
</div>
`,
replace: true,
link($scope, $element, $attrs) {
$scope.vm = {};
var vm = $scope.vm;
function getProps() {
var props = {};
var removekeys = Object.keys($scope);
Object.keys($attrs).forEach((key) => {
// $开头和$scope的属性不取
if ((key.startsWith('$') || removekeys.includes(key))) return;
props[key] = $attrs[key];
});
return Object.assign(props, $scope.props);
}
function loadApp() {
if (!window.qiankun) {
vm.error = '发生错误:qiankun依赖不存在';
return;
}
vm.error = '';
var { loadMicroApp } = window.qiankun;
var props = getProps();
vm.appInstance = loadMicroApp({
name: `${$scope.name || 'webMicro'}_${Math.random()}`,
entry: $scope.entry,
container: $element[0],
props
}, {
sandbox: { experimentalStyleIsolation: true }
});
}
loadApp();
$scope.$on('$destroy', () => {
if (vm.appInstance) {
vm.appInstance.unmount();
}
});
}
};
}
angular.module('ui.webank.micro', []).directive('micro', micro);
3.在主应用中按需使用微应用组件
父应用路由页面/#/home/foo
<micro entry="/micro/" props="{id: 11111}"></micro>
父应用路由页面/#/home/bar
<micro entry="/micro/" props="{id: 22222}"></micro>
子应用
Fes.js的已经集成了微前端乾坤插件,先基于Fes.js 新建微前端项目,然后按照文档步骤引入qiankun插件即可:
1.fesjs项目引入依赖【packgae.json】
"dependencies": {
"@fesjs/fes": "2.0.0",
"@fesjs/plugin-qiankun": "2.0.1"
}
2.开启微前端支持【fes.js】
export default {
qiankun: {
micro: {}
}
}
3.创建子应用页面,Fes.js遵循约定大于配置,会自动创建路由
- pages/home/foo.vue ---> /#/home/foo
- pages/home/bar.vue ---> /#/home/bar 如果不想按照父应用路由创建文件,加入下面配置做好匹配即可【app.js】
export function patchRoutes({ routes }) {
routes.unshift({
path: '/home/foo',
component: require('@/pages/foo').default
});
routes.unshift({
path: '/home/bar',
component: require('@/pages/bar').default
});
}
4.作为子应用需要加入生命周期的钩子回调【app.js】
// 父应用传递的属性,可以在下面回调的参数props取到
export const qiankun = {
// 应用加载之前
async bootstrap(props) {
console.log('app1 bootstrap', props);
// 父应用页面是/#/home/foo时,props.id为11111
// 父应用页面是/#/home/bar时,props.id为22222
},
// 应用 render 之前触发
async mount(props) {
console.log('app1 mount', props);
},
// 当 props 更新时触发
async update(props) {
console.log('app1 update', props);
},
// 应用卸载之后触发
async unmount(props) {
console.log('app1 unmount', props);
}
};
5.页面获取属性
配合useModel插件,父应用传入的props会更新到qiankunStateFromMain中,页面中可直接使用
<script>
export default {
setup(){
const mainState = useModel('qiankunStateFromMain');
// 父应用页面是/#/home/foo时,mainState.id为11111
// 父应用页面是/#/home/bar时,mainState.id为22222
return {
mainState
};
}
}
</script>
部署
- 父应用正常打包,按照以前的服务器路径部署
- 子应用正常打包,区别于父应用路径名加上-micro,如父应用是/data/html/test,那么子应用就是test-micro,父应用的微应用组件的entry也对应改下。
经过以上操作,小李完美的让基于 vue3 开发的新功能在 angularjs 老项目里跑起来了。喜大普奔,赶紧喝一口 82 年的快乐水压压惊~以后又可以和 Fesjs、vue3 愉快的玩耍了。
第三站:进阶
愉快的玩耍了一段时间后,业务提了新的需求过来,分析完发现涉及的到的页面非常复杂,如果推到重来微前端实现,就不能按时交付了,如果在就页面上改,又要告别好玩的Fes.js了。尝到了微前端甜头的小李怎么会就此打住,赶紧打开Fes.js乾坤插件文档寻寻宝,果然又淘到了宝藏男孩:<MicroAppWithMemoHistory /> 。赶紧走起 !!!
基于第二站的实现方式,主应用和微应用的路由需要一一对应,如下图:
这导致了父应用页面如果复杂度很高,一次性迁移工作量巨大。
<MicroAppWithMemoHistory />可以完美的解决这种困扰,实现如下效果:
- 迁移粒度可控:父应用的同个页面拆分到一个子应用的一个或多个页面中。
- 多系统共享:子应用的负责的业务划分的更加清晰,方便各系统复用。
<MicroAppWithMemoHistory />是父应用的组件,旧项目不能直接使用。小李扒了源码之后,发现其精髓是Memory VueRouter,小李赶紧先学习下。
Memory VueRouter
创建一个基于内存的历史记录。这个历史记录的主要目的是处理 SSR。它在一个特殊的位置开始,这个位置无处不在。如果用户不在浏览器上下文中,它们可以通过调用 router.push() 或 router.replace() 将该位置替换为启动位置。
在对于vue技术栈的子应用中,将创建路由的方式替换为创建Memory路由即可
const router = VueRouter.createRouter({
// 改为createMemoryHistory
history: VueRouter.createMemoryHistory(),
routes, // `routes: routes` 的缩写
})
创建Memory路由后,子应用的路由跳转不需要和父应用同URL了。
父应用微前端组件
基于Fes.js乾坤插件的子应用已经是支持了Memory VueRouter,父应用只需要给子应用传递url属性,就会自动启用,结合该特性,小李重写了主应用封装的微前端组件:
function micro() {
return {
restrict: 'EA',
scope: {
name: '@',
url: '@',
entry: '@',
props: '='
},
template: `
<div class="micro-container">
<div ng-if="vm.error" style="color: red;">{{vm.error}}</div>
</div>
`,
replace: true,
link($scope, $element, $attrs) {
$scope.vm = {};
var vm = $scope.vm;
function getProps() {
var props = {};
var removekeys = Object.keys($scope);
Object.keys($attrs).forEach((key) => {
// $开头和$scope的属性不取
if ((key.startsWith('$') || removekeys.includes(key))) return;
props[key] = $attrs[key];
});
return Object.assign(props, $scope.props);
}
function loadApp() {
if (!window.qiankun) {
vm.error = '发生错误:qiankun依赖不存在';
return;
}
vm.error = '';
var { loadMicroApp } = window.qiankun;
var props = getProps();
// 指定后,子应用会使用Memory路由
if ($scope.url) {
props.url = $scope.url;
}
props.onRouterInit = function (router) {
vm.router = router;
};
vm.appInstance = loadMicroApp({
name: `${$scope.name || 'webMicro'}_${Math.random()}`,
entry: $scope.entry,
container: $element[0],
props
}, {
sandbox: { experimentalStyleIsolation: true }
});
}
function updateApp() {
var app = vm.appInstance;
if (!app) return;
if (!vm.updatePromise) {
vm.updatePromise = app.mountPromise;
} else {
vm.updatePromise = vm.updatePromise.then(() => {
if (app.update && app.getStatus() === 'MOUNTED') {
var props = getProps();
if ($scope.url) props.url = $scope.url;
return app.update(props);
}
});
}
}
loadApp();
$scope.$watch('url', () => {
if (vm.router && vm.appInstance) {
updateApp();
vm.router.push($scope.url);
}
});
$scope.$watch('props', updateApp);
$scope.$on('$destroy', () => {
if (vm.appInstance) {
vm.appInstance.unmount();
}
});
}
};
}
angular.module('ui.webank.micro', []).directive('micro', micro);
参数
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| name | 子应用名称 | string | 'webMicro' |
| entry | 子应用入口URL | string | |
| url | 子应用页面路由地址,设置后不再与父应用同路由 | string | |
| props | 传递给子应用的属性,可以响应变化 | object | {} |
| attribute | 传递给子应用的其他静态属性值 |
实例
<!-- 子应用路由会匹配父应用的路由 -->
<micro entry="/micro/" props="{id: 11111}" desc="cscm子应用"></micro>
<!-- 独立的路由,不匹配父应用路由 -->
<micro entry="/micro/" url="/cscm/foo" props="{id: 11111}" desc="cscm子应用"></micro>
终点站:结束or开始?
在angularjs项目可以如此玩,那基于其他技术栈的项目也就轻车熟路啦。考虑到主应用在微前端使用上的一致性和可维护性,对于“Fes.js的子应用,不同技术栈的父应用”在实现加载微前端组件,这里给一些统一的规范建议:
- 属性
name: 子应用名称
entry:子应用入口url
url:子应用路由(开启了内存路由专用)
props:子应用属性。可监听变化
其他扩展属性:由attribute提供,不一定可以监听变化。
- loadMicroApp
name:加上hash或者时间戳
url、props、attr、onRouterInit(开启了内存路由,返回路由实例)合并为props
-
监听:url、props改变,atrr可以监听到也要处理。调用微应用实例update方法
-
内存路由特殊处理:监听组件属性url改变,实现父应用控制子应用路由跳转(使用场景较少)
-
销毁需要卸载子应用实例
复杂的页面可以可按功能迁移了,小李露出来喜悦的笑容,后续旧项目将会一步一步的被微前端替换掉,不用在回到angularjs旧时代了,但这就结束了吗?小李回想了下踩过来的坑,或许这才是起点吧,下一个被淘汰的技术栈又是谁呢?