微前端在 2016 年 ThoughtWorks 的一个技术雷达上面提出后,不断有团队尝试将单体的前端 web 应用按不同维度进行拆分或者组合,再聚合到一个整体的应用架构下面。
无论从系统体验优化还是技术架构升级的角度,都对微前端的方案提出了各种高要求。本文将围绕 icestark 对于不同场景的思考和设计,来尝试给出解决方案。
在聚焦希望引入微前端技术架构的场景上,不难发现,以下的两类场景的诉求会相对强烈:
1. 工作台的场景,基于产品体验的纬度
2. 大型单体应用,这种场景更侧重于想从技术维度进行优化,能系统可持续的迭代发展
在工作台场景,从产品侧的诉求来看,希望跨系统的操作能够更加简易,能够带来系统操作和体验的一致性;而从技术架构的角度,各个独立的系统缺乏统一的管控手段,许多能力都在重复建设。
面向大型单体应用的场景,也就是我们常见的巨石应用,随着业务需求的迭代,系统的复杂度直线上升。从直观的感受来讲,这个应用构建的速度越来越慢,生成的 bundle 大小越来越大。日常的调试开发体验也收到一定程度的影响。
从另一方面来看,伴随着业务功能体量的提升,也导致了开发和协作成本的上升,常见的问题就是局部技术架构升级会变得非常困难,业务的扩展和其他业务能力的接入都会对单体 SPA 架构带来要求。
对于应用架构的设计上,除了微前端的技术架构外,还有几种场景的技术选型。
巨石应用也就是 SPA/MPA 的技术架构。这里想单独提一下,针对产品体验的场景,一个简单的 SPA 应用、肯定是能够给到一个整体的系统体验,并且能够管控系统的技术复杂度。但如果系统越来越臃肿,就会遇到上面到的技术纬度的问题。
SPA/MPA 是前端技术架构中最为常见的,对于一个独立的系统它能在整体体验和技术复杂度上做到很好的管控。但是为了防止这个项目后续发展成一个上古项目,无论是在功能迭代还是公用模块的管理上都会提出很高的要求。不然随着系统的臃肿,系统健壮性和可持续迭代的复杂度问题都会随之而来。
iframe 在微前端方案流行前,它其实是一个比较好的解决方案。不管是一些二方或是三方的接入,它都能够很好地满足需求。但它存在一个致命的问题,就是用户体验。举个常见的问题,iframe 如果不去做一些特殊处理,嵌入的页面双滚动条、路由无法同步、页面内部存在弹出遮罩交互等问题都是体验纬度上的硬伤。
框架组件,简单来讲就是跨系统复用的公共组件。通常将一些通用的头部或吊顶中常用的逻辑封装成一个组件,然后以 npm 的形式进行维护,通过这种方式能够非常方便的将复用逻辑 / UI 提供出去。但本质上没有解决去解决技术架构持续升级和多系统用户体验优化的问题。
微前端技术架构虽然会映入一些技术的复杂度,但基于上述两种核心场景能够从体验和效率维护去寻找一个平衡点,让前端的协作模式发生改变,功能模块拆分后独立开发,独立部署,最终再集成到一个系统当中。
在应用架构中接入微前端,核心需要处理框架应用和微应用之前的关系。
icestark 将微前端方案中需要处理的技术细节进行屏蔽。框架应用去不用关心路由处理逻辑或者接入微应用的处理,只需要完成微应用的配置及其应用上的一些业务逻辑,比如鉴权、应用埋点等业务逻辑即可。
接下来也将针对 icestark 内部技术架构的设计进行分享。
icestark 里面引入的核心概念,主要两个点:框架应用和微应用。
框架应用通过 icestark
提供的 AppRouter
组件可以快速地完成微应用的挂载
<code class=" language-jsx">
<AppRouter>
<AppRoute
path={['/', '/message', '/about']}
title="通用页面"
url={['//unpkg.com/icestark-child-common/build/js/index.js', '//unpkg.com/icestark-child-common/build/css/index.css']}
/>
<AppRoute
path={['/', '/message', '/about']}
title="通用页面"
entry="https://ice.alicdn.com/icestark/child-common-angular/index.html"
/>
</AppRouter>
</code>
核心配置信息中 path 代表基准路由,声明了访问路由地址是对应微应用将会被加载;url信息代表了应用的 bundle 资源;除此之外,通过 entry 的方式可以把 html 整体引入,不再需要关心页面中加载的 js 资源和 css 样式。
引入微前端架构后的工作流程可以从两个方面发生变化
微应用能按照 SPA 体验根据路由的变化进行加载,取决为 icestark 内部路由管理的设计。
icestark 里面的路由规则非常简单,接触过 react-router 的开发者不难发现两者在配置上其实是有很多相似的地方,比如 path、exact 的配置规则。
当访问框架应用页面时,icestark 内部会去做一个路由的分发。如上图中注册的三个微应用配置:
第一个注册配置中设置了 excat 属性。只有在精准匹配
/seller
路由的时候才会匹配到第一份注册信息
如果在微应用架构里面去设置了 path 为 /
的一个微应用,那它将整个系统的一个兜底路由,所有不匹配已注册的路由配置都会由兜底路由进行渲染。
兜底路由一般情况都会用来渲染通用页面,比如跟框架应用有比较强的耦合页面,比如登陆页面, 404 页面或者说退出登录的页面。所以实践上面我们也将兜底路由作为框架应用自身路由的渲染。
为了能够让 icestark 响应页面路由的变化,并对相应的微应用进行加载,icestark 对两类路由事件进行了劫持:
一旦应用间发生跳转,通过上述事件的劫持能够拿到对应的路由信息,再根据路由的匹配规则来决定哪个微应用进行挂载。
一个微应用可能会有多个路由设置,如果在没有发生应用间跳转的情况下,由于匹配到的是当前的微应用,所以不会再次加载资源,内部路由跳转逻辑则根据微应用自身路由配置决定渲染。
路由劫持发生的时机在整个微前端配置初始化阶段,即
AppRouter
的挂载,一旦AppRouter
卸载对应的劫持也将会移除
从微前端的设计原则上来说,并不希望微应用太多地去依赖框架应用或者其它微应用提供的能力。这样在微应用独立开发的时候需要额外创建一个框架应用环境,不利于技术架构的结偶和维护。
但基于一些轻量的应用场景,比如通过通信机制让框架应用和微应用的多语言设置保持一致,一旦多语言设置发生切换,微应用能够监听到这个变化。
icestark 提供了一个应用通信机制,在实际开发过程中推荐轻量的去使用,不要耦合过多的业务逻辑
@ice/stark-data
中提供了应用通信的能力,核心的实现是一个 EventBus 的机制,框架应用跟微应用之间的通讯,以 window 这样一个全局变量作为桥梁。这样不管是微应用添加的事件或数据,还是框架应用添加的事件或数据都可以访问到。
icestark 在设计隔离的方案时候,有两个基础原则:
基于上述的两个因素,在实践过程中并不一定说是要实现了一个完美的隔离方案之后,然后才在微前端技术架构中去使用。如果当前方案能够满足基础的业务的诉求,那就让这个方案在业务中使用。
通常微前端中的隔离场涉及两个方面:一个就是 CSS 的隔离,另一个就是 JS 的隔离。
样式隔离上面,推荐的方案是基于一些约定的隔离,用低成本的隔离方式,让样式之间不会相互影响。
样式隔离主要分两类:
为什么我们没有直接去使用 shadow DOM,本质的原因还是 shadow DOM 的方案还不够开箱即用。
目前 shadow DOM 对于业务上的改造还是有一定成本和问题:
虽然 shadow DOM 问题还是比较多,但是接下来 icestark 也会这方面继续探索、逐渐完善,争取提供出一个开箱即用的方案,达到启用 shadow DOM 能够没有太大改造的成本。
多个应用的 bundle 多次执行的时候很容易对全局变量造成污染,特别是代码中出现对于 window 全局变量的依赖。
icestark 中通过 proxy 的沙箱机制实现了脚本的隔离。
Prxoy 沙箱的基本原理是通过 with + new Function 的形式阻断代码中对于 window 的直接访问,并通过 Proxy 的方式拦截对于 window 变量的访问和写入,沙箱的隔离使代码不能直接访问到 window 对象,通过ES6 的新特性 Proxy 可以定制 get/set 的逻辑,这样就能对 window 上的一些全局变量变化进行快照记录,以便微应用切换的时候进行恢复。
另外像一些应用初始化时,会在 window 上面设置 setTimeout、setInterval,如果在卸载阶段没有很好的处理,将会影响到下一个挂载微应用的执行。所以在沙箱中针对这类方法进行了特殊处理,在沙箱挂载前对相应的方法进行劫持,在卸载的时候,再对它进行恢复。
对于不授信的三方最简单最安全的隔离方式是 iframe。在 icestark 中可以简单定义好基准路由 path ,再通过自定义渲染的方法 render 将 iframe 相关的内容渲染出来。
微模块的能力其实是对微前端方案的一个补充,通常一个微模块并不会耦合路由,在一个页面中可以随意组合和挂载。它的应用场景主要有以下两种模式:
icestark 对于微模块的应用场景上会有一个明确的定义,微模块其实是不会再去耦合路由的。之前提到的微应用的内部基本上是一个 SPA 它至少有一个路由或者是一个页面,但是微模块的使用上我们希望尽量简单,因为一旦多个模块都大量耦合路由的话,这会使路由处理变得复杂。
在模块的标准上面,微模块是以 UMD 的方式直接打包,通过这种标准模式打包,即便是以 npm 包的形式也可以正常使用。在微模块内部除了默认导出模块方法外,还需要定义挂载(mount)和卸载(unmount)的生命周期。
微模块的应用场景其实是对微应用的一个补充,它更适用于更加细粒度的功能拆分和动态搭建的场景。
根据模块资源在执行的位置渲染模块:
<code class=" language-jsx">
import { MicroModule } from '@ice/stark-module';
const App = () => {
const moduleInfo = {
name: 'moduleName',
url: 'https://localhost/module.js',
}
return <MicroModule moduleInfo={moduleInfo} />;
}
</code>
通过组件方式的挂载,将微模块渲染至指定位置,而上层的切换展示逻辑均由业务进行控制。
微前端的技术架构,它给大型单体应用场景和工作台场景带来技术架构优化的方案,通过引入微前端的技术架构去解决当前系统遇到的问题和瓶颈,也能给上古代码找到一个重新焕发生机的机会。
同时结合微模块的方案,让基于标准化模块的上层方案有更大的想象空间,未来面向的场景和方案也将更加丰富。
如果对于飞冰的前端架构方案和微前端的技术架构有兴趣的,如果项目对你有帮助,欢迎关注 star ICE 技术架构:
https://github.com/ice-lab/icestark
https://github.com/alibaba/ice