作者 | 大果
2017 年中旬,飞冰(ICE)团队接到一个叫做「阿里创作平台」的项目,这个产品为创作者提供了入驻、帐号管理、内容管理、内容发布、粉丝运营、数据分析等等非常完备的功能,页面数 50+、项目一期有 3-6 个前端外加部分后端同学同时开发、业务未来有二方业务接入的需求……针对这些诉求,传统的单页面应用方案实在有点力不从心,因此在详细的技术评估之后我们自研了一套叫做 AppLoader 的方案,AppLoader 即应用加载器,我们将整个系统拆分成多个应用(仓库)然后通过 AppLoader 进行管理加载保证路由的正常渲染,最终实现对一个巨型系统的解耦。
AppLoader 诞生之后,先后服务过几个业务,但因为场景比较特殊因此我们一直未将这个方案对外开源,直到 2019 年年初,一方面社区中微前端的概念逐渐普及,另一方面在阿里内外也出现越来越多的类似业务场景,因此我们对 AppLoader 做了一次能力和品牌的升级,同时面向社区开源,升级之后的品牌便是我们开源在 GitHub 上的 icestark,欢迎 star/pr 或者提 issue。
技术方案对比
如前文所说,两年多之前我们接到「阿里创作平台」这个项目,这个产品承载了创作者从入驻到创作完整的生命周期,相比于普通业务有以下两个不同点:页面数非常多、频繁的多人协作与多需求并行、未来有二方业务接入的需求,针对这些差异点我们也对当下主流的一些方案做了一轮分析对比。
单/多页面应用
无论是 SPA 还是 MPA,本质上都是一个唯一的项目仓库,即便我们将一些公共组件抽离出去单独维护,最终也是需要在项目仓库中进行打包构建,这样的单项目仓库所带来的问题:
- 代码量达到一定量的时候,单次构建时间很长,开发&发布效率极低
- 涉及到二方/三方的依赖升级会影响整个应用,加之页面数很多导致回归成本极高,长期下去面对类似 React 版本升级这样的技术诉求很难落地
- 对于多人协作、多需求并行非常不友好,需要花费大量的精力在协作流程上
iframe
将系统拆分成多个应用,每个应用独立开发独立部署,然后通过 iframe 的方式将这些应用嵌入到 portal 系统中,这个方案解决了很多协作、隔离之类的问题,但 iframe 的体验问题一直是个难以解决的问题:
- iframe 体验问题:页面加载慢;双滚动条问题;内部蒙层无法遮罩到外部框架,同时布局无法居中;内部跳转后外部无响应,刷新后又会回到 iframe 首页……
- 每个应用依然需要依赖服务部署,需要有域名,应用的部署成本偏高
关于 iframe 的体验问题,有的可以解决,有的几乎无法解决,因此结合体验的重要性我们最终放弃了 iframe 这个方案。
封装框架组件
封装一个统一的框架 UI 组件,发布到 NPM,然后每个应用自行接入框架组件,但是这个方案有以下几个问题:
- 用户访问入口不统一,本质还是多个系统,只是看起来框架是一致的
- 每个应用依然需要依赖服务部署,需要有域名,应用的部署成本偏高
这个方案更适合于多个业务间有统一的框架诉求,但从业务、前端、后端服务都比较独立,没有中心化管理的需求。
AppLoader 与 icestark
2017 年中旬我们还没有了解到「微前端」的概念,因此结合业务诉求自研了 AppLoader 的类微前端解决方案,再到 2019 年将整体品牌升级到 icestark。
关于 icestark
这个章节核心介绍 icestark 的设计思路以及如何使用。
架构与概念
上图即整体的设计与分层:
- 框架应用:又叫主应用、宿主应用、底座应用,负责系统整体布局、微应用的注册加载渲染、微模块的管理
- 微应用:又叫子应用,一般是一个 SPA 应用,可以独立运行,也可以注册到框架应用中运行
- 微模块:针对多个微应用共享的模块或者页面上的一些二方模块,微模块与传统的业务组件类似,特殊点是微模块通过动态加载资源并渲染,而普通的业务组件一般是通过 npm 引入静态打包编译
之所以将微应用和微模块的概念独立开来,是因为两者的使用场景和表现差异都是比较大的,微应用只会同时存在一个,需要考虑路由的问题,微模块会同时存在多个因此需要核心考虑沙箱隔离、公共依赖提取等问题。
框架应用
框架应用的 API 设计我们参考了 react-router,react-router 是页面级路由,而 icestark 是应用级路由。同时为了保证开发体验,目前框架应用我们跟 React 做了耦合,让开发者可以通过 jsx 的方式声明应用路由,因此框架应用必须基于 React,未来我们也会结合用户诉求提供 vue 版本的 icestark。
// src/app.js
import React from 'react';
import { AppRouter, AppRoute } from '@ice/stark';
export default class App extends React.Component {
render() {
return (
<BasicLayout>
<AppRouter>
<AppRoute
path="/seller"
title="商家平台"
url={[
'//unpkg.com/icestark-child-seller/build/js/index.js',
'//unpkg.com/icestark-child-seller/build/css/index.css',
]}
/>
<AppRoute
path="/settings"
title="设置"
entry="//unpkg.com/icestark-child-seller/build/index.html"
/>
<AppRoute
path="/"
title="通用页面"
render={() => {
return (
<ReactRouter>
<Switch>
<Route path="/home" component={Home}>
<Route path="/about" component={About}>
</Switch>
</ReactRouter>
)
}}
>
</AppRouter>
</BasicLayout>
);
}
}
如上所述,每个 AppRoute 就对应一个微应用,微应用支持几种不同的注册方式:
- url:对应渲染微应用需要的资源,针对微应用资源情况比较明确的情况
- entry:对应微应用构建出来的 html,针对微应用资源情况不明确,比如有 external、 不同的 vendor、资源地址有 hash 等场景
- render:可以渲染一个自定义的 React 组件,比如渲染一个 iframe,甚至框架应用自身的一个 SPA 应用
微应用加载
微应用通常是一个 SPA 应用,支持 React/Vue/Angular 等不同的应用类型,在传统 SPA 应用基础上添加相关的应用声明周期注册方法即可:
import React from 'react';
import ReactDOM from 'react-dom';
import { isInIcestark, getMountNode, registerAppEnter, registerAppLeave } from '@ice/stark-app';
import App from './App';
if (isInIcestark()) {
registerAppEnter(() => {
ReactDOM.render(<App />, getMountNode());
});
registerAppLeave(() => {
ReactDOM.unmountComponentAtNode(getMountNode());
});
} else {
ReactDOM.render(<App />, document.getElementById('ice-container'));
}
对于微应用的加载与渲染,除了将其正常渲染出来,核心要解决的是路由跳转的问题,基本思路:
- 建立微应用和路由的映射关系,为每个微应用分配一个基准路由如
/seller
,这个微应用保证所有的路由定义在/seller
下,那么当从其他路由跳转到/seller
路由时我们就可以加载渲染/seller
对应的子应用 bundle 了。 - 通过劫持
history.pushState
和history.replaceState
两个 API,同时监听popstate
事件,保证能够捕获到到所有路由变化。当捕获到路由变化时,根据路由查找对应的微应用,如果对应的还是当前这个微应用则什么事情都不做,如果对应的是新的一个微应用则卸载之前的微应用,同时加载新的微应用并渲染。 - 框架应用中包含系统 Layout,我们需要将微应用渲染到 Layout 里面,但是单页面应用都是直接通过
ReactDOM.render(<App />, document.getElementById('#root'))
的方式渲染,如果直接执行那么渲染的位置是无法被控制的,因此 icestark 提供了一个getMountNode()
的 API 保证微应用能够渲染到正确的节点里。
微模块加载
微模块主要是针对一些跨应用的共享模块、二方接入模块等场景,此部分能力正在建设中,具体可参考文末的 issue 链接。
沙箱隔离
目前 icestark 主要服务的是二方可控的业务场景,因此从整体设计上我们优先关注与传统 SPA 应用的开发体验一致性,次要关注沙箱隔离。如果有三方接入的场景,icestark 也支持了 render + iframe 的方案。
脚本隔离
基于 Proxy 能力对 window 做了一层代理,保证每个微应用/微模块渲染时使用的都是独立的 window 对象,进而实现全局 window 的隔离,放置应用污染 window 导致系统出错,我们将这部分能力封装为 @ice/sandbox
,因此在其它类似的场景也可以独立使用。
样式隔离
- 针对框架应用与微应用基础组件样式冲突的问题,推荐框架应用上通过工程能力将基础组件的 Class 前缀替换掉。
- 微应用层面建议使用 CSS Modules,通过工程保障不引入全局样式。
存在的问题
当然,跟其他方案一样,微前端的方案也有自身的缺陷,因此还是需要结合业务情况来判断是否使用,而不是一股脑什么东西都往微前端上靠,这里列举两个问题:
- 存在一定的技术复杂度,结合对社区同学的一些答疑,微前端整体还是增加了一些复杂度的,因此在落地微前端方案需要确保有一个相对资深的同学进行整体的把控,避免引入新的问题
- 微前端的框架应用本质是一个中心化的系统,我们建议微应用尽量降低对中心化系统的依赖度,比如提供 React、各种 API 甚至一些组件,因为依赖越多意味着中心化系统的变更风险越高
业务场景与价值
大型系统
面向文章开头说的大型系统,微前端带来的价值:
- 多人协作成本:按照业务域区分成不同的应用,每个应用独立开发独立部署,保证开发效率
- 稳定性:单个应用可升级,不需要依赖全局
- 技术选择灵活性:新的应用可以选择新版本的技术体系(比如 Vue@3.0),甚至某些简单的应用也可以选择可视化搭建的方式
- 二方开放能力:二方业务可按照规范开发微应用
portal 系统
很多中小公司里会有很多小系统,这些系统很多时候都是独立开发部署的,系统间没有任何复用的能力,如果引入微前端:
- 部署成本:单个应用只需要发布资源,不需要考虑域名、Nginx 托管之类的问题
- 通用能力的复用:诸如通用的登录、鉴权等逻辑,不需要每个应用重复实现
- 可监控可管理:能够收集到所有应用的信息、技术框架、版本等信息,进而可以做到更好的管控
业务落地
icestark 目前在阿里内部落地了 20 个左右的平台型系统,不同系统可能有 5、10 甚至数量更多的微应用。
阿里创作者平台
包含 20+ 子应用,其中 5-8 个子应用由二方业务开发。
阿里健康-熙牛医疗云医院信息系统
淘系小二工作台
面向淘系运营小二的后台都将已子应用的方式接入小二工作台,打造面向运营小二的操作系统。
未来
icestark 目前整体的能力已经趋于完善,接下来我们会结合业务诉求持续完善解决方案,比如更好的权限方案、埋点方案等等,帮助业务更加简单的落地微前端方案。欢迎大家关注 icestark ,关注我们的进展。
相关链接
关注「Alibaba F2E」
把握阿里巴巴前端新动向