前言
在开始微前端内容之前,首先来了解一些关于微服务的概念,以下内容摘自维基百科:
微服务是一种软体架构风格,它是以专注于单一责任与功能的小型功能区块 (Small Building Blocks) 为基础,利用模组化的方式组合出复杂的大型应用程式,各功能区块使用与语言无关 (Language-Independent/Language agnostic)的API集相互通讯。
微前端介绍,因为微前端没有官方定义,以下介绍源自社区:
微前端是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。各个前端应用可以独立运行、独立开发、独立部署。
本篇文章是源自工作实践的一次总结,读完本篇文章希望你能有以下收获:
- 对微前端有一个简单认识,想自己去实现一个demo。
- 尝试在自己项目合适的场景下,完成一次实践落地。
正文
微前端介绍
这里通过3W原则来讲解微前端:
- 什么是微前端?
- 为什么需要使用微前端?
- 怎样去落地微前端?
什么是微前端?
先上图:
(图片来源于网络)
假设我们现在有一个已经迭代很多年的超级app,里面包含首页、分类页、购物车页、订单页等等模块。如果我们后续有一个模块里面的一个功能有新的需求变动,那我们就需要上线整个应用,更新整个项目,想来就很头疼。如果我们将这个大型应用进行拆分,以模块为单位,对拆分后的子项目进行独立发布、部署,就可以实现增量更新。通过这样一个例子,我们接着往下看,微前端到底是什么?
因为微前端没有具体的定义,下面的介绍源自社区:
- 它是一种类似于微服务的架构。
- 它将微服务的理念应用于浏览器端,即将web应用由一个大型的单体应用转变为多个小型前端应用聚合为一的应用。
- 各个子应用可以独立开发、独立运行、独立部署。
- 微前端的核心在于拆,拆完再合。
为什么需要使用微前端?
为什么去使用它,就不得不先看看我们的痛点在那,它是否可以帮我们解决掉这些痛点。
首先来看看我们可能遇到的痛点:
- 不同团队开发同一个应用,技术栈不同怎么办?
- 希望每一个应用模块可以独立开发、独立部署怎么办?
- 。。。
看完我们可能面临的痛点,再来看看微前端的优点:
- 经过拆分,代码更加简洁、解耦、更易维护。
- 就像微服务一样,微前端的一大优势就是可独立部署的能力。
- 局部/增量升级
- 组织更具扩展能力,其团队更加独立自治。
- 可以与时俱进,不断引入新技术/新框架,不受技术的限制。
- 。。。
看完它的优点,那么在合适的场景下使用微前端,是不是可以给我们带来很大的收益呢!
怎么去落地微前端?
在讲落地微前端之前,先聊一聊微前端的历史,
微前端的概念由thoughtworks于2016年提出,此后很快被业界所接受,并在各互联网大厂中得到推广和应用。
2018年 single-spa诞生了,single-spa是一个用于前端微服务化的javascript前端解决方案 (本身没有处理样式隔离、js执行隔离) 实现了路由劫持和应用加载。
2019年qiankun基于single-spa, 提供了更加开箱即用的API(single-spa + sandbox + import-html-entry),它做到了技术栈无关,并且接入简单(有多简单呢,像iframe一样简单)
目前比较常见实现的微前端几种方式:
- 路由分发式:通过HTTP服务器的反向代理功能,将请求路由转发到对应的应用上。
- 前端微服务化:在不同的框架之上设计通信和加载机制,最后合在一个页面内加载对应的应用。
- 微应用化:通过软件工程的方式,在部署构建环境中,把多个独立的应用组合成一个单体应用。
- 微件化:开发一个新的构建系统,将部分业务构建成一个独立的chunk代码,使用时只需要远程加载即可。
- 前端容器化:将iframe作为容器来引入其他前端应用。
- 应用组件:借助于web components技术,来构建框架的前端应用。
微前端会涉及知识点:
先上图:
(图片来源于网络)
如上图所示,微前端背后涉及很庞大的知识体系,例如如何为每一个子应用提供js的独立运行时环境,防止子应用间CSS覆盖,子应用注册、挂载、卸载的触发等等
QMS实践
面临的问题
QMS质量系统原项目存在前端部分采用angular进行开发,需专人维护,新人学习的时间线长,开发成本高,技术合规无法进一步开展等问题。为帮助QMS团队解决所面临的问题,经过团队内部讨论,通过在原项目基础之上,新需求使用react+hiui开发,采用微前端进行整合。
实现方式及架构图
在QMS系统中,项目采用如下方式实现:
如上图,项目分为中心基座应用和一个或多个子应用,在基座应用中存放所有子应用的注册信息;通过在主工程中逻辑处理,对应用实现初始化;single-spa通过监听url的变化,实现路由劫持,应用加载器与子应用连接,通过子应用暴露的生命周期,实现子应用的挂载和卸载。
QMS架构图:
下面以demo代码详细说明:
中心基座
1.应用加载器
import * as singleSpa from 'single-spa'
import config from './manifest.prod.json'
registerApp(config)
singleSpa.start()
function registerApp(conf) {
console.log('---registerApp 参数---', conf)
conf.forEach((application) => {
singleSpa.registerApplication(
application.name,
() => window.System.import(application.name),
pathPrefix(application.activeRule, application.strict),
)
})
}
function pathPrefix(prefix, strict = false) {
return function (location) {
if (strict) {
return location.pathname === prefix
}
return location.pathname.startsWith(`${prefix}`)
}
}
2.应用注册信息表,包含项目所有子应用的注册信息,如打包好的js文件、路由前缀标示(用于挂载、卸载逻辑)等。
[{
"name": "layout-app",
"activeRule": "/",
"strict": false,
"url": "http://localhost:3006/js/main-layout.js"
},
{
"name": "react-app",
"activeRule": "/v2",
"strict": false,
"url": "http://localhost:3007/js/main-v2.js"
},
{
"name": "angular-app",
"activeRule": "/v1",
"strict": false,
"url": "http://localhost:3008/js/main-v1.js"
}
]
3.system.js介绍与配置
systemjs 是一个最小系统加载工具,用来创建插件来处理可替代的场景加载过程,包括加载 css 场景和图片,主要运行在浏览器和 node.js 中。它是 es6 浏览器加载程序的的扩展,将应用在本地浏览器中。通常创建的插件名称是模块本身,要是没有特意指定用途,则默认插件名是模块的扩展名称。
system.js作为模块加载器,提供通用的模块导入途径,支持传统模块和es6的模块。我们需要 system.js 帮我们完成:
- 注册、加载子应用的模块文件
- 注册、加载公共库
在html模版文件中引入system.js文件:
<script type="systemjs-importmap">
<%= htmlWebpackPlugin.options.importmap %>
</script>
<!-- <script src="https://cdn.jsdelivr.net/npm/zone.js@0.10.3/dist/zone.min.js"></script> -->
<script src="https://hd.mi.com/f/zt/hd/20200805/systemjs/system.min.js"></script>
<script src="https://hd.mi.com/f/zt/hd/20200805/systemjs/amd.min.js"></script>
<script src="https://hd.mi.com/f/zt/hd/20200805/systemjs/named-exports.js"></script>
<script src="https://hd.mi.com/f/zt/hd/20200805/systemjs/use-default.min.js"></script>
子应用
每一个子应用中,都需要对外暴露三个生命周期(bootstrap、mount、unmount)和对应挂载的结点。
1.子应用的挂载信息
import React from 'react'
import ReactDOM from 'react-dom'
import singleSpaReact from 'single-spa-react'
import App from './App'
const reactLifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent: App,
domElementGetter,
})
export function bootstrap(props) {
return reactLifecycles.bootstrap(props)
}
export function mount(props) {
return reactLifecycles.mount(props)
}
export function unmount(props) {
return reactLifecycles.unmount(props)
}
function domElementGetter() {
// Make sure there is a div for us to render into
let el = document.getElementById('react-app')
if (!el) {
el = document.createElement('div')
el.id = 'react-app'
document.getElementById('sand-box').appendChild(el)
}
return el
}
2.子应用css隔离
为防止应用之间的css样式覆盖,需要实现子应用间的样式隔离开,常见css的规范:
- bem规范:属于约定规范,需要开发者主动遵守规范
- css-modules:打包时添加哈希值,生成唯一的类名
- shadow dom:真正意义上的隔离
- 第三方插件:项目中使用postcss插件,为子应用css添加前缀
此处以react子应用为例:
module.exports = {
plugins: [
require('autoprefixer'),
require('postcss-flexbugs-fixes'),
require('postcss-preset-env')({
autoprefixer: {
flexbox: 'no-2009',
},
stage: 3,
}),
require('postcss-plugin-namespace')('#react-app'),
],
}
以上就是核心部分的展示,实现了子应用注册、挂载、卸载,应用间的样式隔离。
收获
QMS微前端整合实现原有页面保留angular,新功能使用react + hiui开发,后续无关技术栈开发;通过调整webpack配置,CLI构建速度提升50%(原平均时间16min降到8min),对构建速度提升感兴趣的朋友,可以读一下排除模块串联,实现angular项目优化这篇文章,里面会详细描述在微前端整合过程中对angular项目打包的优化。
总结&后记
微前端优缺点总结
微前端的优点:
- 子应用可以独立开发、独立部署。
- 可以不受技术的限制、在维护好原有系统的同时,也可以引入新的技术,提升开发效率和用户体验。
- 实现局部/增量升级,需求变化时可针对子应用按需升级,不需要对整个项目升级改造。
- 代码更加简洁,便于维护,耦合度降低。
微前端带来的意义和影响都是空前的,越来越多的团队开始思考和使用微前端架构。
微前端的缺点
微前端为我们带来优点的同时,也存在一些缺点:
- 重复依赖:子应用依赖的包重复时,由于应用独立发布、部署,这种情况难以避免。
- 团队更加分裂:系统的拆分细化,每个团队只负责自己的业务功能交付,可能会导致对用户体验的敏感度降低。
不是所有的项目都适合引入微前端,根据自己业务场景合理使用微前端,这样可以把收益最大化,减少不必要的投入。
微前端适用落地场景总结
- 兼容原有系统:如果你的原项目和现在团队的技术栈不匹配,或者框架版本落后太多,就很适合使用微前端,实现技术栈的细度融合。
- 应用聚合:超大型项目的拆分、聚合,类似公司内部的中台系统,希望为员工提供更加便捷高效的服务水平,提升效率,微前端是一个不错的选择。
- 有技术融合或项目融合的需求
最后的总结:合适自己项目需求的微前端方式才是最好的。
(图片来源于网络)
后记
使用single-spa + system.js实现微前端灵活度虽然很高,它实现路由劫持、子应用的挂载、卸载,但是又会遇到很多问题,例如应用间的通信、js沙箱、公用模块提取等等问题都需要自己去实现,这些都是技术难点,也是绕不开的工作,qiankun基于single-spa的基础之上二次封装实现了js沙箱,应用间通信等。如果你现在刚开始做或打算开始做融合,处于技术选型的阶段,如果没有特别的定制化需求,qiankun可以满足大多数场景,我们团队内部的建议是使用qiankun。
single-spa官方维护团队给的答案也是:qiankun是一个不错的微前端解决方案。