达达快送的运营系统,由各个业务线的子系统组合而成。随着业务的高速发展,子系统数量呈现爆炸趋势,复杂度急剧上升。2018年3月创建时,只有3个子系统。2021年3月拆分时,达到15个子系统。
由于各个业务线的子系统迭代周期不同,所以运营系统每天都有上线。表现出来的现象是:排队上线、无法回滚、构建缓慢。
首先是排队上线:从图中可以看出,夜晚第1和第5个上线之间存在非常明显的排队等待现象。而且这不是纯前端的等待,子系统涉及的后端、测试、产品也要跟着等。
其次是无法回滚:第2天发现昨晚第4个上线有问题需要回滚,只能撤销代码变更后重新上线,导致线上问题持续久。
最后是构建缓慢:子系统代码即使没有变更,也要参与整个打包构建过程。子系统越多,构建耗时越久。
问题有多严重呢?2021年3月:
1.夜晚排队上线21次2.通过重新上线来回滚,耗时28分钟3.构建482次,平均耗时10分钟
解决上述问题:
1.减少排队次数2.可以直接回滚3.构建速度更快
从上图变成下图:
之所以存在排队上线、无法回滚、构建缓慢问题,是因为运营系统的前端属于单体应用架构,所有子系统必须打包在一起后上线。
一旦某个子系统出现问题,就会造成集体回滚。所以为了避免集体回滚,子系统排队上线。先上1个,观察10分钟,没问题再上下1个。
这样做的后果是,后一个上线包含前面所有上线,但是不包含接下来即将发生的上线。一旦回滚中间某次上线,在此之后上线的功能全部消失。为了避免这种情况发生,通常不直接回滚,而是撤销代码变更后重新上线。
每次上线,都要把所有子系统的代码打包在一起。哪怕子系统代码没有变更,也要参与整个打包构建过程。随着业务线的增加,子系统数量在增加,构建耗时也在增加。
既然根本原因是所有子系统必须打包成一个应用上线,那么拆分成多个应用独立上线,问题不就解决了吗?
过去就是这么做的,但是如果运营系统也走这条路,会降低研发效率,破坏产品交互体验。
首先是降低研发效率:代码复用变得困难。过去代码在同一个仓库,直接引入就好。现在代码在不同仓库,需要新建项目单独维护依赖包,通过上传和下载实现代码复用,非常麻烦。
其次破坏产品交互体验:这样拆分后,子系统链接发生变化,过去能打开的链接现在打不开了。子系统切换方式发生变化,过去在浏览器中进行,现在去服务器绕一圈,白屏明显。
所以我们需要更进一步,在运行时把多个独立应用聚合成一个整体。做到开发过程解耦,用户使用聚合。既解决了问题,又不降低研发效率或者破坏产品体验。这就是微前端了。
通过微前端架构划分子系统边界,将系统整体复杂度隔离到各个子系统中,从而避免因子系统迭代周期不同造成的工程协同问题。
不能降低研发效率:
1.避免代码复用变得困难
不能破坏产品交互体验:
1.避免存量链接打不开2.避免页面跳转白屏明显
不能投入太多人力:
1.基础设施有多套环境,多条链路,情况复杂2.业务研发排期紧张,运营系统拆分成本要低
前端应用的渲染方式,大体上可以分为3类6种:
服务器渲染,是指通过后端模版渲染的传统网页。应用之间的切换和应用内的页面跳转,都基于后端路由。页面复用方面,可以是浏览器中通过iframe直接内嵌HTML,也可以是服务器中通过Nginx SSI将多个HTML片段组合后返回浏览器。
浏览器渲染,是指基于React或Vue实现的现代Web应用。用户打开页面后,浏览器从服务器获取没有任何页面内容的HTML,由HTML引入的JS动态创建内容。应用之间的切换有2种:一种是直接打开另一个应用的HTML,基于后端路由进行;另一种是HTML不变,直接打开另一个应用的JS,基于前端路由进行。页面复用方面,2种方式都是基于React、Vue或Web Components格式的组件。
服务端渲染,是指基于React或Vue实现的现代Web应用:在用户第一次打开页面时,直接由服务器运行JS渲染好页面内容后返回,从而提升页面首屏打开速度;在用户后续与页面交互时,直接由浏览器运行JS渲染好页面内容,从而提升用户行为反馈速度。应用之间的切换有2种:一种是直接打开另一应用的HTML,基于后端路由进行;另一种是HTML不变,直接打开另一个应用的JS,基于前端路由进行,并且应用的JS和前端路由都支持在服务器运行。页面复用方面,基于后端或前端路由的应用切换,都可以使用React或Vue的组件格式,但是不能使用Web Components格式。如果应用之间的框架不同,可以通过single-spa的Parcels实现跨React或Vue框架的可复用组件。
3类渲染方式中,运营系统基于浏览器渲染,也就是图中红色高亮部分。如果改为服务器渲染,会破坏产品交互体验。如果改为服务端渲染,会投入太多人力。因此,渲染方式不能变。
浏览器渲染方式中,按照应用切换方式又可以细分为后端路由和前端路由。如果使用后端路由,会破坏产品交互体验。所以使用前端路由最合适,也就是图中绿色高亮部分。相对于现状,只增加一层前端路由。
这个方向的解决方案,有2个特别流行:
1.国外开源的single-spa方案2.国内基于single-spa封装的qiankun方案
他们在多个方面存在差异,需要结合具体业务场景进行技术取舍,没有银弹。
挂载应用的前提是找到应用。当我们把一个大应用拆分成多个小应用之后,如何通过应用名找到应用动态变化的文件链接就变成难题。类似的问题,后端微服务架构中也存在。每个服务名对应一个IP列表,IP列表中的IP动态变化。后端的解决办法是,将服务名和IP列表的对应关系保存在配置中心,这个过程叫做服务注册。当需要调用服务时,从配置中心下载完整的服务列表。从中找出可用的IP,然后调用服务,这个过程叫做服务发现。
这里的服务名就对应着前端的应用名,IP就对应着应用的文件链接。我们可以基于最新的浏览器标准Import maps和ES modules实现前端的服务注册和发现,但是这对浏览器版本有要求,而且需要修改脚手架的配置。所以single-spa方案的思路是,使用浏览器补丁es-module-shims,或者使用浏览器标准兼容的模块加载器SystemJS。至于修改脚手架配置,对于存量项目给出主流脚手架的修改指南,对于新增项目提供官方的脚手架配置。由于我们是把一个大应用拆成多个小应用,而非把多个小应用聚合成一个大应用,所以只用修改一个大应用的脚手架配置,剩下的克隆Git仓库代码即可,因此成本可接受。
如果是把多个小应用聚合成一个大应用,每个应用都改脚手架配置,成本很高,所以有了qiankun基于HTML的方案。它的大致做法是,在代码中指定应用名对应的HTML地址。当需要加载应用时,下载HTML,从中解析出文件链接。好处是几乎不用修改脚手架配置,但也有一些问题:
1.需要在代码中指定HTML的链接。我们有多套环境多条链路,且链路域名随机生成,导致修改HTML的链接有些麻烦2.HTML本身会包含文件链接以外的信息,文件大小可能在几KB甚至几十KB,这会影响页面加载耗时3.从HTML中解析出应用入口的文件链接,需要引入额外的解析模块,也会影响页面加载耗时
由于移动设备的占比达到60%以上,期望引入微前端架构后,页面打开速度不因此变慢,避免页面白屏明显,所以Import maps + ES modules的服务注册和发现方案更合适。
拆分出来的应用,会引入一些相同的模块。这些被依赖的公共模块需要抽离出来,从而减少重复构建和加载,降低代码构建和页面加载的耗时。并且支持不同应用使用同一模块的不同版本,从而避免公共模块的修改,导致所有相关应用全部测试一遍。
对于应用和模块的依赖关系预先明确的应用,可以在加载应用时预先加载它依赖的模块。这样能够解决依赖瀑布问题,优化关键渲染路径。此外,对于高频应用,也可以通过这种方式预先加载,不一定等到用户点击页面内的链接后才开始。
由于服务注册和发现选择了single-spa方案,而single-spa的推荐配置中没有提到版本控制和预先加载,所以我们需要寻找相应的解决方案。版本控制方面,Import maps标准的scopes能够解决这一问题。预先加载方面,SystemJS的depcache能够实现。因此,使用SystemJS这一浏览器标准兼容的模块加载器,能够解决这2个问题。
拆分出来的应用在运行时,会共享同一个运行环境,所以彼此之间可能产生影响,这就需要对应用进行隔离。隔离主要有两种情况,同屏单应用和同屏多应用。
同屏单应用主要是快照的思路。对于变量隔离,在应用挂载前保存运行环境的状态,在应用卸载后恢复这个状态。对于样式隔离,在应用卸载后移除或禁用引入的样式就好。
同屏多应用主要是沙盒的思路。对于变量隔离,通过new Function和Proxy为每个应用创建一个独立的运行环境。对于样式隔离,主要有3种方法:
1.给样式名增加随机字符串,确保样式名唯一2.通过data attribute增加属性选择器,控制样式影响范围,实现类似已废弃的浏览器标准Scoped CSS的效果3.通过Shadow DOM创建完全隔离的DOM节点,节点内外互不影响
对于变量隔离,解决方案比较成熟,没有太多要做的事情。对于样式隔离,没有成熟的解决方案,需要结合具体情况进行取舍:
1.同屏单应用时,基于single-spa的生命周期钩子,实现应用的样式快照2.同屏多应用时,组件库样式名固定,不区分版本,所以在多版本共存时会存在样式污染。3种样式隔离方法中,Shadow DOM不用改样式名,而且能够很好的处理CSS动画,可以满足需求
这样多种方式组合起来,基于各自的优缺点,能够以较低的成本解决问题。
经过一番比较和取舍,方案基本确定,以国外开源的single-spa方案为基础,结合业务场景进行定制:
1.基于Import maps和SystemJS实现服务注册和发现2.基于Import maps的scopes和depcache实现版本控制和预先加载3.流量分发就用框架无关的前端路由single-spa4.变量隔离基于成熟的沙盒方案实现5.样式隔离基于快照和沙盒方案组合实现6.应用间通信基于浏览器自定义事件实现
方案确定后,接下来是具体实践。
农夫山泉23-25届校园招聘供应链类岗位
5306 阅读益海嘉里(金龙鱼)2025校招供应链、物流储备生
4653 阅读特斯拉智能制造校招专项:24-25届物流、供应链类岗位
4632 阅读盒马社招运输经理;店仓管理专员;仓储业务物资管理高级专员;店仓经理
4362 阅读京东社招项目运营岗;客户体验分析;配送运营岗;客户经理岗;营业部负责人岗
4377 阅读亚马逊全球物流团队「物流销售岗位」热招中
4082 阅读曼伦2025校招供应链物流岗位
4061 阅读KK集团2025届全球校园招聘供应链岗位
3989 阅读招贤纳新丨美通招聘这些岗位人才
3771 阅读SHEIN社招资深物流运营专员;战略经营分析专家(供应链);仓储经理(英语);资深供应商管理专员(物流);资深经营分析师(履约)
3564 阅读