阅读 615

未来魔法校的微前端实践

一、 背景


魔法校是tob起家,众所周知tob业务很容易做成巨石应用,近两年来魔法校飞速发展,我们的某个主要的前端项目遇到了瓶颈,那就是项目太大了。


为了减少耦合度加快打包速度,我们选择将一些功能提出来新建项目,然后通过iframe的方式引入到主项目中去。虽然项目体积大的问题得到了解决,但用户体验却随之下降。


因为每次用户切换到iframe的tab时不管优化的再好总要有一瞬间的白屏,整个系统使用起来没有连贯性,而且在iframe里切换页面浏览器的地址栏url并不会变化,给人的感觉就是两个系统。


业务的快速发展迫使我们去寻找一种新的方式-微前端。


二、微前端的基本概念


1、什么是微前端


微前端是近两年比较火的一个概念,这个术语最初来自 2016 年的 ThoughtWorks 技术雷达,它将微服务的概念扩展到了前端领域。目前的趋势是构建一个功能丰富且强大的前端应用,即单页面应用(SPA)。前端层通常由一个单独的团队开发,随着时间的推移,会变得越来越庞大而难以维护。这就是传说中的前端巨无霸Frontend Monolith。


微前端背后的理念是将一个网站或者 Web App 当成特性的组合体,每个特性都由一个独立的团队负责。每个团队都有擅长的特定业务领域或是它关心的任务。这里,一个团队是跨职能的,它可以端到端,从数据库到用户界面完整的开发它所负责的功能。


然而,这个概念并不新鲜,过去它叫针对垂直系统的前端一体化或独立系统。只不过微前端显然是一个更加友好并且不那么笨重的术语。


2、微前端的优势


◾复杂度可控:每一个UI业务模块由独立的前端团队开发,避免代码巨无霸,保持开发时的高速编译,保持较低的复杂度,便于维护与开发效率。

◾独立部署:每一个模块可单独存放,单独部署,不对其他模块有任何影响。

◾技术选型灵活:也是最具吸引力的,在同一项目下可以使用如今市面上所有前端技术栈,也包括未来的前端技术栈。

◾容错:单个模块发生错误,不影响全局。

◾扩展:每一个服务可以独立横向扩展以满足业务伸缩性,与资源的不必要消耗。


3、我们何时需要前端微服务化


◾项目技术栈过于老旧,相关技能的开发人员少,功能扩展吃力,重构成本高,维护成本高。

◾项目过于庞大,代码编译慢,开发体差,需要一种更高维度的解耦方案。

◾单一技术栈无法满足你的业务需求。

◾需要将其它现有项目整合到主项目中。


4、实现微前端的几种方式


◾iframe:iframe是最简单也是最直接的办法,iframe自带沙箱隔离,可使多个应用同时运行在一个用户界面上。

◾路由分发式微前端:即通过路由将不同的业务分发到不同、独立前端应用上,典型代aliyun。

◾结合Web Components 技术构建:使用 Web Components 构建独立于框架的组件,随后在对应的框架中引入这些组件,适合较小的模块。

◾自制框架兼容应用:代表框架有 single-spa、阿里的 qiankun 在页面合适的地方引入或者创建 DOM,用户操作时,加载对应的应用(触发应用的启动),并能卸载应用。


5.单体应用与微前端架构对比


◾单体应用

微前端


三、如何实现微前端应用


1、基本概念


实现一套微前端架构,可以把其分成四部分参考:https://alili.tech/archive/11052bf4/


◾加载器:也就是微前端架构的核心,主要用来调度子应用,决定何时展示哪个子应用, 可以把它理解成电源。

◾包装器:有了加载器,可以把现有的应用包装,使得加载器可以使用它们,它相当于电源适配器。

◾主应用:一般是包含所有子应用公共部分的项目—— 它相当于电器底座。

◾子应用:众多展示在主应用内容区的应用—— 它相当于你要使用的电器。


所以是这么个概念:电源(加载器)→电源适配器(包装器)→️电器底座(主应用)→️电器(子应用)️。


总的来说是这样一个流程:用户访问index.html后,浏览器运行加载器的js文件,加载器去配置文件,然后注册配置文件中配置的各个子应用后,首先加载主应用(菜单等),再通过路由判定,动态远程加载子应用。


2.预备知识


✅SystemJs


SystemJS提供通用的模块导入途径,支持传统模块和ES6的模块。SystemJS有两个版本,6.x版本是在浏览器中使用的,0.21版本的是在浏览器和node环境中使用的,两者的使用方式不同。

参考:https://github.com/systemjs/systemjs

在微服务中主要充当加载器的角色。


值得注意的是我们暂时没有选择用system.js,因为经过调研,目前需要加载的资源很少,有点杀鸡焉用宰牛刀的感觉,目前我们的做法是动态创建script去引用资源,当然正统的做法还是使用system.js。


✅singleSpa

single-spa是一个在前端应用程序中将多个javascript应用集合在一起的框架。主要充当包装器的角色。

参考:https://single-spa.js.org/docs/getting-started-overview.html


四、具体实现步骤


首先需要两个前端项目,一个主项目,一个子项目,主项目有基本的登录、导航模块,剩下的主要业务逻辑在子项目中。

因为我们好未来主要的前端技术栈是vue,我们就用vue来举例说明,当然react和angular也都适用。


1.子项目


首先我们来看下vue.config.js里的修改

publicPath这个选项一般我们做项目的时候写’/'就行了,这个选项表示资源文件从哪个地址进行加载,一般都是从网站的根目录加载,但是现在要写成子项目的存储地址,可以是aliyun oss,也可以是cdn上。

devServer需要新增header头用来跨域,主要是我们在本地开发的时候从主项目的端口号里加载子项目端口号的资源也会跨域。

output选项要指明资源包的名称以及运行环境,这里我们选择了window,注意如果使用system.js的话这里要指明是umd模式。

stats-webpack-plugin这个插件原本是用来描述各个依赖之间的相互引用关系,现在我们用它生成manifest.json文件,这个manifest.json文件里有什么东西呢?我们来看一下。

这里面我们主要用到entrypoints,可以看到这就是启动子项目所需要加载的入口文件,也就是说我们在主项目里请求这个manifest.json文件就知道都需要加载哪些js。

这边我们还给css加了个作用域single-spa-vue,子项目的所以css只有在这个样式前缀下才能生效,保证子项目的样式不会影响到主项目。


◾然后是main.js的修改

这边我们用了一个小插件single-spa-vue,这个插件的主要作用就是导出single-spa框架所需要的三个生命周期bootstrap、mount、unmount,对应的还有single-spa-react、single-spa-angular。
可以看到我们这样的写法可以确保子项目作为一个模块在主项目里运行,也可以单独拿出来打包部署,甚至可以两者并行存在。


◾router.js修改

router要加一个baseUrl,我们的项目叫’learnSystem’,前后都要加’/'这个url前缀是我们在主项目的模块路由前缀,如果不加在主项目刷新页面的时候会匹配不到子项目。


2.主项目


◾新增single-spa-config.js文件

主要步骤就是动态创建script标签,远程加载子项目所需js文件,然后注册微服务模块’learnSystem’。


一开始这个文件是放在main.js中加载的,后来发现如果子项目又可以比主项目加载的还快,这样就会出问题,然后我们把它放在里App.vue的created钩子函数里,用require引入,确保主项目加载完毕之后再加载子项目。


上图中LEARN_URL可以根据环境配置不同的值,如开发环境可以配置localhost+子项目的端口号,生产环境可以配置线上的链接。


◾router.js

router需要加模糊匹配*,否则在子模块当前页面直接刷新会先进入到主项目的404页面


◾App.vue

一开始我们的App.vue只有一个router-view,现在我们要提供一个容器供子项目渲染,还记得single-spa-vue这个作用域吗?确保子项目的样式只会在这个div里生效


3.主项目与子项目通信


通常,我们建议尝试避免这种情况-将这些应用程序耦合在一起。如果您发现自己经常在应用程序之间执行此操作,则可能要考虑那些单独的应用程序实际上应该只是一个应用程序。但是有一些场景是需要用到通信的,比如子项目请求接口发现token过期,需要通知主项目退出登录并跳转到登录界面。


我们用了一种简单的方法,既然这两个项目同属一个窗口,那他们也共有一个window对象,在主项目里注册一个方法并挂载到window对象上,在子项目里调用此方法就能达到目的。


4.优化打包配置


因为主项目与子项目都用到了相同的一部分依赖,可以考虑将公用的依赖不打包进去,改为在主项目引入来提高打包效率
修改vue.config.js

请注意,可共用的资源要保证主项目和子项目所用的依赖版本号一致,或者可以兼容到同一版本,假如某个依赖两个项目之间版本相差过大,那么这就不是一个可以共用的资源。


五、结语


此套方案只能说是一套可行性方案,其实还有很多可以优化的地方,我们可以一起来探讨。


比如:

上述我们解决了子应用与主应用之间css相互之间影响,但是别忘了还有js呢,如何创造像iframe那样的沙箱,使得应用之间互不影响包括全局变量事件等处理,是一个比较重要的点。

我们用vue避免不了使用vuex,那么不同的应用之间能否共享vuex的数据?因为有一些权限之类的数据可能是通用的。或者更深一步,应用能否自身来控制某个state是应用的私有变量或者是其它应用可以访问的公共变量。

我们现在子项目的资源是懒加载,也就是当路由匹配到’learnSystem’时才去加载子项目的资源。其实预加载可能会更好一点,也就是应用空闲时去加载子应用资源。

子应用嵌套,微前端如何嵌套微前端。

主应用如何下发状态给子应用。


虽然它并不完美,但是并不妨碍我们去体验微前端带来的好处,我们将以上这些东西分享给大家,也请大家关注到未来魔法校近年来的成长。