微前端不是单纯的前端框架或者工具,而是一套架构体系,这个概念最早在 2016 年底被提出,可以参考在 Google 上搜索 Micro-Frontends, 排名靠前的 https://micro-frontends.org 的博客文章,提出了早期的微前端模型。
微前端是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将单页面前端应用由单一的单体应用转变为把多个小型前端应用聚合为一的应用。各个前端应用还可以独立开发、独立部署。
微前端架构旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用(Frontend Monolith)后,随之而来的应用不可维护的问题。这类问题在企业级 Web 应用中尤其常见。
微前端的实现意味着对前端应用的拆分。拆分应用的目的并不只是为了在架构上好看,它还可以提升开发效率。比如10万行的代码拆解成10个项目,每个项目1万行代码,要独立维护每个项目就会容易得多。而我们只需要实现应用的自治,即实现应用的独立开发和独立部署,就可以在某种程度上实现微前端架构的目的。
前端技术栈日新月异,推陈出新的速度绝对是冠绝群雄。如何在维护好遗留系统的前提下,不断引入新技术和新框架,提高开发效率、质量、用户体验,成为每个团队需要认真对待的问题。微前端可以很好的实现应用和服务的隔离,互相之间几乎没有影响,可以很好的支持团队引入新技术和新框架。
对于许多组织来说,追求增量升级就是他们迈向微前端的第一步。对他们来说,老式的大型单体前端要么是用老旧的技术栈打造的,要么就充斥着匆忙写成的代码,已经到了该重写整个前端的时候了。一次性重写整个系统风险很大,我们更倾向一点一点换掉老的应用,同时在不受单体架构拖累的前提下为客户不断提供新功能。
为了做到这一点,解决方案往往就是微前端架构了。一旦某个团队掌握了在几乎不影响旧世界的同时为生产环境引入新功能的诀窍,其他团队就会纷纷效仿。现有代码仍然需要继续维护下去,但在某些情况下还要继续添加新功能,现在总算有了解决方案。
到最后,我们就能更随心所欲地改动产品的各个部分,并逐渐升级我们的架构、依赖关系和用户体验。当主框架发生重大变化时每个微前端模块都可以按需升级,不需要整体下线或一次性升级所有内容。如果我们想要尝试新的技术或互动模式,也能在隔离度更好的环境下做试验。
微前端体系下,每个小模块的代码库要比一个单体前端的代码库小很多。对开发者来说这些较小的代码库处理起来更简单方便。而且微前端还能避免无关组件之间不必要的耦合,让代码更简洁。我们可以在应用的限界上下文处划出更明显的界限,更好地避免无意间造成的这类耦合问题。
就像微服务一样,微前端的一大优势就是可独立部署的能力。这种能力会缩减每次部署涉及的范围,从而降低了风险。不管你的前端代码是在哪里托管,怎样托管,各个微前端都应该有自己的持续交付管道;这些管道可以将微前端构建、测试并部署到生产环境中。我们在部署各个微前端时几乎不用考虑其他代码库或管道的状态;就算旧的单体架构采用了固定、手动的按季发布周期,或者隔壁的团队在他们的主分支里塞进了一个半成品或失败的功能,也不影响我们的工作。如果某个微前端已准备好投入生产,那么它就能顺利变为产品,且这一过程完全由开发和维护它的团队主导
解藕代码库、分离发布周期还能带来一个高层次的好处,那就是大幅提升团队的独立性;一支独立的团队可以自主完成从产品构思到最终发布的完整流程,有足够的能力独立向客户交付价值,从而可以更快、更高效地工作。为了实现这一目标需要围绕垂直业务功能,而非技术功能来打造团队。一种简单的方法是根据最终用户将看到的内容来划分产品模块,让每个微前端都封装应用的某个页面,并分配给一个团队完整负责。相比围绕技术或“横向”问题(如样式、表单或验证)打造的团队相比,这种团队能有更高的凝聚力。
不同应用之间依赖的包存在很多重复,由于各应用独立开发、编译和发布,难免会存在重复依赖的情况。导致不同应用之间需要重复下载依赖,额外再增加了流量和服务端压力。
大幅提升的团队自治水平可能会让各个团队的工作愈加分裂。各团队只关注自己的业务或者平台功能,在面向用户的整体交付方面,会导致对用户需求和体现不敏感,和响应不及时。
那么如何提供一套解决方案既具备 SPA 的用户体验,又能够具备 MPA 应用带来的灵活性,并且可以实现应用间同灰度,监控也可能细化到子系统呢。目前在字节跳动内应用的微前端解决方案「Garfish」 ,解决方案主要分为三层:部署侧、框架运行时、调试工具,目前采用的是 SPA 的架构。
部署平台作为微前端研发流程中重要的一环,主要提供了:微前端的服务发现、服务注册、子应用版本控制、多个子应用间同灰度、增量升级子应用、下发子应用信息列表,分析子应用依赖信息提取公共基础库降低不同应用的依赖重复加载。
用于解决微前端中子应用的独立部署、版本控制和子应用信息管理,通过 Serverless 平台提供的接口或在渲染服务中下发主应用的 HTML 内容中包含子应用列表信息,列表中包括了子应用的详细信息例如:应用 id、激活路径、依赖信息、入口资源等信息,并通过对于子应用的公共依赖进行分析,下发子应用的公共依赖,在运行时获取到子应用的信息后注册给框架,然后在主应用上控制子应用进行渲染和销毁。
谈到微前端绕不开的话题就是为什么不适用 iframe 作为承载微前端子应用的容器,其实从浏览器原生的方案来说,iframe 不从体验角度上来看几乎是最可靠的微前端方案了,主应用通过iframe 来加载子应用,iframe 自带的样式、环境隔离机制使得它具备天然的沙盒机制,但也是由于它的隔离性导致其并不适合作为加载子应用的加载器,iframe 的特性不仅会导致用户体验的下降,也会在研发在日常工作中造成较多困扰,以下总结了 iframe 作为子应用的一些劣势:
使用Iframe 会大幅增加内存和计算资源,因为 iframe 内所承载的页面需要一个全新并且完整的文档环境
Iframe 与上层应用并非同一个文档上下文导致
事件冒泡不穿透到主文档树上,焦点在子应用时,事件无法传递上一个文档流
跳转路径无法与上层文档同步,刷新丢失路由状态
Iframe 内元素会被限制在文档树中,视窗宽高限制问题
Iframe 登录态无法共享,子应用需要重新登录
Iframe 在禁用三方 cookie 时,iframe 平台服务不可用
Iframe 应用加载失败,内容发生错误主应用无法感知
难以计算出 iframe 作为页面一部分时的性能情况
无法预加载缓存 iframe 内容
无法共享基础库进一步减少包体积
事件通信繁琐且限制多
尽管难以将 Iframe 作为微前端应用的加载器,但是却可以参考其设计思想,一个传统的 Iframe 加载文档的能力可以分为四层:文档的加载能力、HTML 的渲染、执行 JavaScript、隔离样式和 JavaScript 运行环境。那么微前端库的基础能力也可以参考其设计思想。
从设计层面采取的是基座+子应用分治的概念,部署平台负责进行服务发现和服务注册,将注册的应用列表信息下发至基座,通过基座来动态控制子系统的渲染和销毁,并提供集中式的模式来完成应用间的通信和应用的公共依赖管理,因此 Garfish 在 Runtime 层面主要提供了以下四个核心能力:
加载器(Loader)
预加载能力
-解析子应用导出内容
-沙箱隔离(Sandbox)
-提供代码执行能力,收集执行代码时存在的副作用
-提供销毁收集副作用的能力
-支持沙箱多实例,收集不同实例的副作用
路由托管(Router)
-解决不同应用间的路由不同步问题
-提供路由劫持能力,在主应用上管控子应用路由
-提供路由驱动能力来拼装完整的平台的能力
子应用通信(Store)
整个微前端子应用的生命周期基本可以总结为:
渲染阶段
销毁阶段
sigle-spa
京东micro
阿里乾坤(qiankun)
腾讯无极
字节Garfish
以乾坤为例
● qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。
● 使用简单
○ 技术栈无关,任意技术栈的应用均可 使用/接入,不论是 React/Vue/Angular/JQuery 还是其他等框架。
○ HTML Entry 接入方式,让你接入微应用像使用 iframe 一样简单。
● 功能完备:几乎包含所有构建微前端系统时所需要的基本能力。
○ 样式隔离,确保微应用之间样式互相不干扰。
○ JS 沙箱,确保微应用之间 全局变量/事件 不冲突。
○ ⚡️ 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
○ umi 插件,提供了 @umijs/plugin-qiankun 供 umi 应用一键切换成微前端架构系统。
● 生产可用:qiankun 孵化自蚂蚁金融科技基于微前端架构的云产品统一接入平台,在经过一批线上应用的充分检验及打磨后,我们将其微前端内核抽取出来并开源,希望能同时帮助社区有类似需求的系统更方便的构建自己的微前端系统,同时也希望通过社区的帮助将 qiankun 打磨的更加成熟完善。目前 qiankun 已在蚂蚁内部服务了超过 200+ 线上应用,在易用性及完备性上,绝对是值得信赖的。
● 架构思想:中心化基座模式的微前端典型代表,由一个主应用和一系列业务子应用构成的系统,并由这个主应用来管理其他子应用,包括从子应用的生命周期管理到应用间的通信机制。
主应用不限技术栈,只需要提供一个容器 DOM,然后注册微应用并 start 即可。
这里以 Vue 2 应用为例演示主应用的搭建。
1、使用 Vue CLI 创建 Vue 2 应用
mkdir qiankun-examples
cd qiankun-examples
vue create main
Vue CLI v4.5.15
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, Router
? Choose a version of Vue.js that you want to start the project with 2.x
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? No
cd main
npm run serve
2、安装 qiankun
npm i qiankun -S # 或者 yarn add qiankun
3、注册微应用并启动
import { registerMicroApps, start } from 'qiankun'
registerMicroApps([
{
name: 'reactApp',
entry: '//localhost:3000',
container: '#container',
activeRule: '/app-react',
},
{
name: 'vueApp',
entry: '//localhost:8080',
container: '#container',
activeRule: '/app-vue',
},
{
name: 'angularApp',
entry: '//localhost:4200',
container: '#container',
activeRule: '/app-angular',
},
])
// 启动 qiankun
start()
当微应用信息注册完之后,一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑,所有 activeRule 规则匹配上的微应用就会被插入到指定的 container 中,同时依次调用微应用暴露出的生命周期钩子。
微应用不需要额外安装任何其他依赖即可接入 qiankun 主应用。
微应用分为有 webpack 构建和无 webpack 构建项目,有 webpack 的微应用(主要是指 Vue、React、Angular)需要做的事情有:
注意:运行时的 publicPath 和构建时的 publicPath 是不同的,两者不能等价替代。
主要的修改就是以上四个,可能会根据项目的不同情况而改变。例如,你的项目是 index.html 和其他的所有文件分开部署的,说明你们已经将构建时的 publicPath 设置为了完整路径,则不用修改运行时的 publicPath (第一步操作可省)。
无 webpack 构建的微应用直接将 lifecycles 挂载到 window 上即可。
以 vue-cli 3+ 生成的 vue 2.x 项目为例
在 src 目录新增 public-path.js:
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
入口文件 main.js 修改,为了避免根 id #app 与其他的 DOM 冲突,需要限制查找范围。
import './public-path';import Vue from 'vue';import VueRouter from 'vue-router';import App from './App.vue';import routes from './router';import store from './store';
Vue.config.productionTip = false;
let router = null;let instance = null;function render(props = {}) { const { container } = props; router = new VueRouter({ base: window.__POWERED_BY_QIANKUN__ ? '/app-vue/' : '/', mode: 'history', routes, });
instance = new Vue({ router, store, render: (h) => h(App), }).$mount(container ? container.querySelector('#app') : '#app');}
// 独立运行时if (!window.__POWERED_BY_QIANKUN__) { render();}
export async function bootstrap() { console.log('[vue] vue app bootstraped');}export async function mount(props) { console.log('[vue] props from main framework', props); render(props);}export async function unmount() { instance.$destroy(); instance.$el.innerHTML = ''; instance = null; router = null;}
打包配置修改(vue.config.js)
const { name } = require('./package');
module.exports = {
devServer: {
// 主要 允许跨域********************
headers: {
'Access-Control-Allow-Origin': '*',
},
},
configureWebpack: {
output: {
library: `${name}-[name]`,
libraryTarget: 'umd', // 把微应用打包成 umd 库格式
jsonpFunction: `webpackJsonp_${name}`,
},
},
};
微前端架构
什么是微前端
微前端简介