需求背景
- 公司各开放平台接入网联平台,网联平台逐渐变成“巨石”应用
- 各平台接入后的开发方式,如何共享公共资源,如平台网页的头部、底部
- 如何统一设计风格
- 是否有必要统一技术栈
什么是微前端
Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently.
译:与多个团队一起构建一个可以独立发布功能的现代web应用程序的技术、策略和诀窍。
The idea behind Micro Frontends is to think about a website or web app as a composition of features which are owned by independent teams. Each team has a distinct area of business or mission it cares about and specialises in. A team is cross functional and develops its features end-to-end, from database to user interface.
译:微前端背后的设计思想:网站或web应用由独立团队提供的特性组合而成。每个团队各自关注不同的业务或目标,每个团队跨职能从后端到前端完成特性开发。
核心思想:
- 技术栈无关
- 隔离团队代码
- 确定团队前缀(即Namespace,减少命名冲突之类的问题)
- 倾向于浏览器原生API而非自定义API(父子应用机制,不是自建发布/订阅系统)
- 韧性,(即保持特性可用性,渐进式提升体验)
微前端方案介绍
-
qiankun
-
Single-spa
-
Ara-framework
-
webpack5 模块联邦
方案对比
因各个方案都涉及到publicPath的问题,即加载静态资源的路径,建议将静态资源都放到CDN管理。
Portal应用提供公共资源
优点:
- 微应用接入无感
- SPA,用户体验上是一个整体站点
- 各微应用和portal应用独立部署
- 父子关系的架构更容易理解,数据流转更清晰
缺点:
- 需提前知道微应用的部署地址(nginx反向代理解决)
- 各微应用需使用运行时publicPath或构建时指定静态资源位置
- 父子应用样式会相互影响(qiankun有实验性的隔离配置,也可以利用Namespace)
- 不能通过独立域名访问到子应用(保证有公共资源的情况下,或子应用在非微前端环境下做降级处理)
公共资源作为微应用
优点:
- 公共资源独立发布,可以只更新公共资源
- 各应用可以通过独立的域名访问
- 各应用不需要处理publicPath的问题
缺点:
- 公共资源作为独立站点发布,部署/维护麻烦(在咱们的CI/CD环境下还好)
- 仓库较多,特别是公共资源较多的情况下
- 应用开发麻烦,作为主应用,需要更多额外的编码
- 与公共资源的通信变成子-->父,不好理解
- 对公共资源(子应用)的样式影响可能性增加
模块联邦(Webpack 5)
优点:
- 无需引入新框架,学习成本更小
- 像引入第三方库一样方便
- 无需维护额外的仓库,各个应用的资源都可以相互共享
- 应用间松耦合,各应用平行的关系
缺点
- 需升级到Webpack 5.x版本
- 技术栈必须保持一致
- 应用内必须实现降级方案(加载资源失败的场景),新应用接入需要修改实现
- publicPath需为绝对路径
组件库+模块联邦
将组件库以服务的形式发布,以模块联邦的方式调用,实现统一的更新。公共资源以组件方式提供,需要考虑组件如何与应用进行通信。
Demo演示
以下内容基于qiankun框架
开发流程
部署方案
静态资源交互
其他
实践中遇到的问题
方案二:公共资源作为微应用
- 状态问题:共用的一些信息,如用户状态,权限等,每个独立应用里都需要维护,代码冗余。如果还要再通信,会更麻烦,比如在一个微应用(用户信息维护)中修改用户头像,在另一个微应用(页面头部)需要更新头像,会带来额外的通信负担,在一个主应用里做通用功能会减少跨应用的通信。
- 路由问题:如果是在单页应用内打开,共用页面的需要操控主应用的路由,且打开的微应用也需要在主应用内注册,不合理。在其他窗口内打开的话,需要把微应用做成一个“完整”的应用。
- 仓库问题:要维护多个仓库,且会存有冗余代码。带来额外的维护难度。
- umi.js 3.x支持微前端插件,而我们当前的版本是2.x。因umi.js的高度封装,对webpack的定制比较麻烦,升到3.x又可能引入其他问题。最方便的方案还是把当前的应用当作微应用。
管理平台portal应用架构设计
微前端下如何管理菜单的权限?在这种架构下,管理平台portal负责大板块和公共管理需求的权限控制,如为qingmobile管理员分配只有qingmobile管理权限+部分公共业务管理权限的角色,为qingai管理员分配只有qingai管理权限+部分公共业务管理权限的角色;由各微应用负责管理各自的权限控制,如控制具体的小程序审核权限,企业审核权限等。
微应用无法加载时页面展示优化
当前版本中,没有加载失败的处理方案,因此我通过自定义fetch,在失败时返回自定义的异常页面,这样来当作加载失败的降级方案。
fetch(url: string) {
const oldFecth = window.fetch;
const appname = data.find((item: any) => item.entry === url);
return new Promise((resolve, reject) => {
oldFecth.apply(window, [url]).then((...res) => {
resolve(...res);
}).catch((e) => {
// 异常时,模拟fetch响应,返回一个失败的页面
resolve({
text: () => `
<script entry>
const render = (props) => {
var container = props.container;
container.innerHTML = '加载失败!';
return Promise.resolve();
};
(global => {
global["${appname.name}"] = {
bootstrap: () => {
return Promise.resolve();
},
mount: (props) => {
props.setLoading(false);
return render(props);
},
unmount: (props) => {
props.container.innerHTML = '';
return Promise.resolve();
}
}
})(window);
</script>
`
});
});
});
}
一些静态资源如何处理
css会被qiankun提取出来,以style标签的方式插入到文档里,所以css文件不会有这样的问题 常用的如图片、字体等,在构建之后无法通过运行时publicPath修改路径,需要使用CDN。
CDN的方案在当前部署场景下难以满足需求,主要体现在两点:
- CDN服务器及其资源管理。我们可以申请BOS作为CDN服务器,在构建时将静态资源上传到BOS。然而每次都上传,必然会占用更多的资源或者覆盖之前的上传,这会带来额外的资源管理负担。如果采用覆盖上传的方案,多个环境(feature/dev/master)下如何保证资源一致性,这也是一个问题。
- 环境迁移时,CDN静态资源是否需要迁移。当前的发布系统在环境发布时,不会再有构建的过程。这就需要在发布时将CDN资源也迁移到对应的环境,如从sit环境迁移到perf环境。这会增加发布系统的工作。
既然CDN是利用域名区分资源,我们能否给各个微应用分配一个固定前缀来解决静态资源的问题?
方案如下:
假设有微应用A和主应用P:
- 为微应用A分配路由前缀:/assets-A,A在构建时,将output.publicPath设置为/assets-A,这样构建出来的所有静态资源引用便以‘/assets-A’开头。
- 微应用A部署时,通过nginx rewrite将路由中的/assets-A移除,这样微应用A的静态资源即可正常访问。
- 在集成到主应用P中时,P中根据分配出去的路由前缀生成nginx配置,将请求转发到对应的微前端上。再经过微应用的路由重写,即可访问到正确的资源。
- 同样,开发环境也在devServer中配置对应的路由重写和转发规则即可。
- 环境迁移时,通过环境变量,动态修改nginx配置。
微应用配置规则
- 主应用为父应用分配APPName,如‘sub’
- 子应用路由以‘/sub’开始,在nacos中配置时应该严格按照此规则
- 子应用构建时,以'/assets-sub'作为前缀(即配置webpack publicPath=‘/assets-sub’),并在nginx配置中将/assets-sub请求重写为/
- 父应用在nginx配置中,将命中'/assets-sub'的请求转发到对应的服务器(通过环境变量动态修改)
微应用接入注意事项
微应用静态资源需支持跨域
微前端框架会跨域加载微应用的html,js等,所以需支持跨域访问。如果接入微应用开发环境进行调试,则开发环境也需要支持跨域
nginx配置如下:
location / {
# * 需要在生产环境修改为具体域名
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, OPTIONS';
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
if ($request_method = 'OPTIONS') {
return 204;
}
try_files $uri $uri/ /index.html;
}
静态资源访问
开发环境
开发环境通过运行时publicPath,在根目录下添加public-path.js,并在入口文件(一般是main.js)中顶部(第一行)导入。public-path.js内容如下:
// public-path.js
if (window.__POWERED_BY_QIANKUN__) {
if (process.env.NODE_ENV === "development") {
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
}
生产环境
因为静态资源的问题,故在生产环境不采用运行时publicPath的方案,而是通过给微应用分配前缀的方式,通过路由重写和反向代理,来解决资源加载的问题。
- 在构建时,配置publicPath,代码示例如下:
// 示例为create-react-app中,通过@rescripts/cli来修改webpack配置,其他环境参考修改
const { name } = require('./package');
module.exports = {
webpack: config => {
// publicPath为主应用分配的固定前缀,为防止冲突,注意命名
config.output.publicPath = '/assets-micro-app1/';
config.output.library = `${name}-[name]`;
config.output.libraryTarget = 'umd';
config.output.jsonpFunction = `webpackJsonp_${name}`;
config.output.globalObject = 'window';
return config;
},
...
}
此时,打包后的文件引用已带上前缀/assets-micro-app1
前缀;
- nginx配置路由重写,将静态资源再次指向微应用服务器。示例代码如下:
location ^~/assets-micro-app1/ {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-NginX-Proxy true;
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, OPTIONS';
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
if ($request_method = 'OPTIONS') {
return 204;
}
rewrite ^/assets-qing-card/(.*)$ /$1 break;
}
后端API访问
如果微应用内不写死域名,通过反向代理的方式转发到对应的域名,在挂载到主应用之后,请求会发往主应用所在域名,如果主应用没有配置反向代理到对应的服务器,则会导致请求404。即子应用没法正确访问资源。
两种方式:
- 写死域名配置,所有请求添加域名作为前缀。
- 同静态资源访问的方案,以主应用分配的固定前缀作为请求前缀,这样主应用会将该请求转发到微应用,微应用再通过路由重写,即可正确反向代理对应的服务器。