微前端调研及简析SPA实现原理
导读
最近对微前端讨论很多,梳理下自己对微前端的理解以及业内的一些微前端尝试反馈。
第零部分:自己对微前端理解
第一部分:基于Single-SPA微前端的一些demo
第二部分:Single-SPA微前端实现原理简析
第三部分:微前端业内一些总结
Part0.
自己对微前端理解
在判断自己项目是否需要使用微前端前,只要记住一句话即可:杀鸡焉用牛刀。
如果项目很简单,请不要没有困难创造困难;
如果项目太大,受够了iframe的种种掣肘,同时你还有一帮陪你肝新玩法的同事,能够准备好面对意想不到的快乐和意想不到的痛苦,深吸一口气,来,我们搞起。
在内部的两次讨论,能够看到不少优点,但同时也需要开发者有一个规范约束,才能发挥微前端的能力。有几个点需要注意:
状态隔离与否 - 状态共享需要规范
样式隔离 - 需要规范
注册应用 - 需要规范
三方依赖不统一
向下兼容方案
Part1.
Single-SPA微前端demo
single-spa-learn-kit:一个基于SPA的基础demo,可以直接run起来
微前端 single-spa:图文并茂,方案有差异,提出几个坑点,留意下:
在配置systemJs引用时会有跨域问题,这时候可以配置nginx的返回头进行解决,详情仓库见。
在构建vue项目时,App.vue文件的主div id必须为你项目构建的id,因为第一次构建后你的html上的div会消失。
Single-Spa微前端实践: 相同方案,有一张生命周期图很好。项目仓库:
https://github.com/zhaiyy/single-spa。
总结优点:
敏捷性 - 独立开发和更快的部署周期:开发团队可以选择自己的技术并及时更新技术栈。一旦完成其中一项就可以部署,而不必等待所有事情完毕。
降低错误和回归问题的风险,相互之间的依赖性急剧下降。
更简单快捷的测试,每一个小的变化不必再触碰整个应用程序。
更快交付客户价值,有助于持续集成、持续部署以及持续交付。
Part2.
Single-SPA微前端实现
如果第一部分的demo已经跑起来,希望较为完整的理解微前端,可阅读此节。如不关心如何实现,跳过此节即可。
了解Single-SPA实现,循序渐进,有几个关键的库需要了解下:
虽然我们的重头戏是single-spa,但是在配置时候会发现都需要加载systemjs。systemjs是什么,简单来说,就是Dynamic ES module loader动态模块加载器,动态加载我们每个依赖的编译后的脚本文件。也正是因为system.js存在,你不会在代码中看到大量script脚本插入的痕迹。
那么问题又来了,我们加载资源都会创建script标签,为什么Systemjs的head中很清爽,答案就是systemjs留了一手,动态创建后又删除了script.
systemJSPrototype.instantiate = function (url, firstParentUrl) {
const loader = this;
return new Promise(function (resolve, reject) {
const script = systemJSPrototype.createScript(url);
script.addEventListener('error', function () {
reject(Error('Error loading ' + url + (firstParentUrl ? ' from ' + firstParentUrl : '')));
});
script.addEventListener('load', function () {
document.head.removeChild(script);
// Note that if an error occurs that isn't caught by this if statement,
// that getRegister will return null and a "did not instantiate" error will be thrown.
if (lastWindowErrorUrl === url) {
reject(lastWindowError);
}
else {
resolve(loader.getRegister());
}
});
document.head.appendChild(script);
});
};
至此引出另外一个问题:动态删除script,全局的变量和函数还会保留吗?答案是肯定的。测试确认如此。
官方实例:systemjs-examples
微前端常用的三个生命周期:bootstrap,mount,unmount,不同框架如何和single-spa关联,答案就是通过single-spa-react中间件实现这几个方法,从而支持下一步single-spa进行注册registerApplication
总算到了我们的重头戏,无论是乾坤,还是我们目前准备的方案,基本都离不开single-spa。
通过上一步的registerApplication注册,将所有的子模块加载到全局变量app数据中,并保存各种状态,用于后边的各种装载和卸载。
single-spa关键的几步是:
初始:默认劫持浏览器事件,等注册应用完成后执行
registerApplication注册应用,触发reroute
start初始化第一次执行,触发reroute
reroute根据不同情况,实现了加载、卸载、更改组件生命周期状态、并延迟执行执行浏览器事件
reroute执行时机:
registerApplication初始化注册应用
start第一次执行
浏览器更新路由
hashchange/popstate - urlReroute(navigation event)
Part3.
微前端业内一些总结
这部分一些是业内的方案介绍和总结,也有国外对微前端的争论。
阿里系,介绍了使用JS Entry vs HTML Entry区别,提出使用的是html entry 方案。内容中有一段介绍微前端的实现:
由于我们的子应用都是 lazy load 的,当浏览器重新刷新时,主框架的资源会被重新加载,同时异步 load 子应用的静态资源,由于此时主应用的路由系统已经激活,但子应用的资源可能还没有完全加载完毕,从而导致路由注册表里发现没有能匹配子应用 /subApp/123/detail 的规则,这时候就会导致跳 NotFound 页或者直接路由报错。
这个问题在所有 lazy load 方式加载子应用的方案中都会碰到,早些年前 angularjs 社区把这个问题统一称之为
Future State :
https://ui-router.github.io/guide/lazyloading#future-states。
解决的思路也很简单,我们需要设计这样一套路由机制:
主框架配置子应用的路由为
subApp: { url: '/subApp/**', entry: './subApp.js' },
则当浏览器的地址为 /subApp/abc 时,框架需要先加载 entry 资源,待 entry 资源加载完毕,确保子应用的路由系统注册进主框架之后后,再去由子应用的路由系统接管 url change 事件。同时在子应用路由切出时,主框架需要触发相应的 destroy 事件,子应用在监听到该事件时,调用自己的卸载方法卸载应用,如 React 场景下
destroy = () =>
ReactDOM.unmountAtNode(container)。
要实现这样一套机制,我们可以自己去劫持 url change 事件从而实现自己的路由系统,也可以基于社区已有的 ui router library,尤其是 react-router 在 v4 之后实现了 Dynamic Routing 能力,我们只需要复写一部分路由发现的逻辑即可。这里我们推荐直接选择社区比较完善的相关实践 single-spa 。
美团的应用,没有开源仓库,不过在两篇文章明显有一个不同观点,就是:是否需要不同子模块间的状态共享。bifost是支持的,上边的是讲究隔离。所以这类可以根据项目决定是否需要配置全局状态。
访谈类
InfoQ:微前端最适合的落地场景是?
艾石光:前端微服务最明显最适合落地的场景是各种中后台项目,尤其是那些传统的 iframe 工程。实际试用过 frames 开发架构的同学会知道真的使用时,如何打造实用可理解的 deeplink 就已经很麻烦了。况且还有很多技术细节。比如那些底层复用、父子通信、session 打通、服务发现等。实际上已经默默承受了很多在微服务里解决得很好且最终效果可以好得多的问题。
前端微服务需要解决的难题涵盖了从集成开发环境到服务转移到部署和流量识别和承载的后端架构,具体涉及到的面很广,细节还是很多的。
我们团队把前端微服务抽象成了服务发现、运行隔离和环境一致三个方面,分别对应了了从开发到发布到线上运行的全部环节
InfoQ:采用微前端有哪些风险与挑战?
艾石光:前端微服务整体上的安全性和可控性还是比较不错的。但是如前面所述目前还没有标准和底层深入支持,所以也新产生了很多独特挑战需要去应对。我觉得最值得提的新引入风险还是来自开发调试过程。
我们认为 docker 等技术是服务端微服务的一个重要优势,容器技术的成熟使微服务在线上线下有非常一致的环境表现。而前端更接近非容器的、类似于各种 BaaS 的模式。像 firebase、GAE 等不借助容器的微服务体系都有整套的调试解决方案和运行监控数据回收方案,可以保证部署前有条件充分调试,也有可靠的测试,能先测试再部署。而微前端的本地开发的环境与调试用的代码和最终上线运行会有不小的区别。不仅如此,线上合并后的环境变化极快,其他平行的模块加起来更新频率非常高。所以这时发现和复现问题都是一个重要的环节。
我们也提供了很深入的解决方案,投入了很大的研发精力去应对这个挑战。我们建设了完整的开发链工具,比如配置代码的修复和消毒,还有融合云打包的脚本植入、自动化验证等。调试方面有整套的代理服务植入到开发环境的浏览器内,有独立的调试命令也有充分与 webpack-dev-server 结合的方式。这些一起实现了让使用者开发时,虽然写的代码仅仅是寄生在 masterpage 内的代码片段,但是开发体验几乎和传统的页面开发完全一致、开发环境看到的运行表现也几乎完全模拟线上。
其次是服务发现过程的模块上下线,是一个需要小心对待的需要极高可用性的中心系统。对此我们做了从 ETCD、Redis 到前端 localstorage 的多层容灾。为了更好地支撑流量,我们还在进一步尝试更多的部署架构。
另外我们也做了更详细的线上错误回收,并且打通到了用户反馈系统。在我们微服务框架里的 console log 都会被收集和整理,并且一起储存时会自动装载上 call stack。
我最直白的感受是实现了项目级别的模块化,把不同项目变成了一个个模块来拼装组合,也就是说模块化从项目内提升到了项目本身
总结一下使用这套架构收到的好处,分为以下几点:
缩小项目打包体积(平均每个子项目bundle不到100k),而整合后的公共资源只需加载一次,性能得到很大提升 (技术角度)
用户体验更好,用户感知不到自己在使用多个不同的项目,更加平顺流畅 (产品角度)
不同git的项目经过改造后,可以随意以项目内每个路由页面为单元拼装成一个新项目,产品灵活性本质上得到提升 (产品/技术角度)
技术尝新,使用业界比较先进的微前端理念,几十个项目,成千上百个功能也能很好的分模块管理。(管理角度)
也是有很多麻烦之处,需要消耗一定成本:
因为多个vue实例在同一个document里,需要避免全局变量污染、全局监听污染、样式污染等,需要制定接入规范。
使用了external抽离公共模块(比如Vue、Vue-router等)后,构造函数(或者Class)的污染也需要避免,比如Vue.mixin、Vue.components、Vue .use等等都需要做一些额外的工作去避免它们产生冲突。
如果你也想要tab切换不刷新(使用keep-alive),那需要做的工作更多,主要是处理缓存,防止堆内存溢出(用chrome自带的performance monitor查看),还有项目间切换时路由钩子等等的处理。
不过跟收益比起来,这些成本就不算什么了~
最后要说一下,并不是所有场景都适合微前端,尤其是项目规模小、数量少的场景不建议使用。 什么样的场景适合这套架构呢?一般有以下特征:
项目很多,规模很大,都是每个项目独立使用git此类仓库维护的、技术栈为vue/react/angular的这类应用
需要整合到统一平台上,你正在寻找可能比iframe更合适的替代方案
项目A有功能A1、A2、A3,项目B有功能B1、B2、B3,产品经理要你把A2、B1、B3组合成一个包含这些功能的新项目
可能你会问:为什么不一开始就把所有需要整合的功能用一个git来维护?
答:理想是美好的,谁也没有先知能力,随着公司业务发展亦或是组织架构的改变、人员更迭,以上场景是几乎不可避免的;我很难想象十多个项目的好几百个功能都在一个git里管理起来有多困难。 可能你还会问,那我把需要整合的业务整合成到一个git仓库呢? 答:这当然是一个解决办法,前提是整合的成本你能接受;并且将来还有这类需求呢?每次都要手动整合业务代码到同一个git仓库吗?假设所有人都只维护这个整合完的git仓库,并行的需求线多了,上线时间会不会拥挤?一个功能产生了致命错误,会不会所有功能跟着出问题?
最后我想说:
我们做这套框架的初衷是解决眼前的问题,然而发现它附带的潜力价值却比想象的多得多。
Microfrontends: the good, the bad, and the ugly
微前端之黄金三镖客:好家伙,丑家伙,坏家伙
国外对微前端的争论,只拉出基本论点。
The Good:
Organizational Flexibility and alignment:分而自治,大项目拆分
Separate deploys for separate services
Ability of autonomous teams to independently iterate and innovate
Ability to organize teams around business units or products
The Bad:
Operational Complexity操作复杂
Needing to have many different applications running in development to test a complete experience
Tracking and debug problems across the entire system
Dealing with versioning across the system
The Ugly:
Performance, incoherent experiences,性能及不统一的体验
With each team making their own technology choices, browsers may end up downloading multiple frameworks & duplicate code
Users don’t experience your company or product in isolation. This is one of the arguments against completely scoping styles to component - the problem may be even worse with completely separated teams.
Some of the implementations of microfrontends (particularly looking at embedding iframes) can cause huge accessibility challenges.
其他:
https://www.xmind.net/embed/e3dv/:
一个xmind图,应该是各个方案的比较,详细,但是没怎么看,需要配合这篇文章来看:微前端如何落地?
本文作者:李卫东
滴滴出行 专家工程师
推荐阅读
用滴滴云Notebook快速上手PyTorch-MINIST手写体