都1202了,还在维护老代码?快试试神奇的Fes.js微前端插件

1,894 阅读7分钟

老李公司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 /> 。赶紧走起 !!!

基于第二站的实现方式,主应用和微应用的路由需要一一对应,如下图: image.png 这导致了父应用页面如果复杂度很高,一次性迁移工作量巨大。

<MicroAppWithMemoHistory />可以完美的解决这种困扰,实现如下效果: image.png

  • 迁移粒度可控:父应用的同个页面拆分到一个子应用的一个或多个页面中。
  • 多系统共享:子应用的负责的业务划分的更加清晰,方便各系统复用。

<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子应用入口URLstring
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旧时代了,但这就结束了吗?小李回想了下踩过来的坑,或许这才是起点吧,下一个被淘汰的技术栈又是谁呢?