如果你还在为"巨石应用"或"跨技术栈集成"犯愁,不妨了解一下微前端吧。微前端借鉴了微服务的理念,将一个单体SPA(单页面应用)拆分为多个小型SPA,微应用之间独立开发、独立部署、独立运行。
一、Web架构演进

Web应用架构的演进可以分为三个阶段:
- 单体应用
在前期的web应用中,前后端代码混是合在一起的(例如JSP)。后端查询数据库,并把数据套入html模板返回给浏览器。缺点很明显,用户每次输入都要刷新html才能看到结果,更适合展示静态网页。
- 前后端分离
随着AJAX技术的出现,使得网页在不刷新html的情况下获取后端数据,并局部刷新页面。至此,前后端分离,前端更专注于用户交互并通过XMLHttpRequeat和后端数据交互,SPA就是这个阶段的代表作。但是,随着业务的发展和代码的堆积,应用复杂度变得不可控,产生了巨石应用。
- 微服务
后端应用根据业务领域拆分为多个独立的微服务,服务之间通过远程调用进行交互,从而实现复杂度可控。前端调用后端接口时,一般会经过API网关或BFF路由到具体的微服务。
面对业务需求膨胀带来的应用复杂度,后端通过微服务进行拆解分治,而前端还处于传统的SPA单体应用,又该如何解决这个问题呢?
二、前端困局
- 巨石应用
用户体验需求升级和前端技术演进,让前端应用的复杂度越来越高。例如,修改代码牵一发而动全身;代码构建和资源加载慢,影响UX(user experience)和DX(developer experience)。
- 跨技术栈集成
历史应用重构无法一次性完成,逐步重构过程中新老应用共存;历史应用整合,把不同技术栈的业务应用集成在一起。难点是不同技术栈,无法统一编码和部署。
上面两个问题,一个是拆分应用,另一个是集成应用。表面看是"八竿子打不着边",但本质上都是"系统解耦"的问题,因此可以借鉴"微服务"的解决思路。
三、微服务和微前端
微服务是一种应用架构,它将一个大型单体应用拆分为多个独立的小型服务,并通过轻量级的通信协议组织起来。重点在于独立 二字,即独立开发、独立部署、独立运行。微服务具备以下优势:
- 系统解耦:每个服务聚焦单一的业务功能,不互相依赖,系统复杂度可控。
- 快速迭代:新增功能或修复bug时,只要更新对应的服务,不用覆盖整个系统。
- 故障隔离:单个服务故障不会导致整个系统崩溃。
- 多技术栈共存:不同服务可以用不同技术栈开发,解决了逐步重构的多技术栈共存难题。
微前端借鉴了微服务的理念,将一个单体SPA拆分为多个小型SPA,并通过路由分发的方式组织起来,微应用之间独立开发、独立部署、独立运行,这就是微前端。

了解了什么是微前端,接下来我们将介绍几个主流的微前端框架,包括使用方法和各自的特点。
四、single-spa
single-spa是微前端框架的开山鼻祖,它开创性地把微应用当做react/Vue组件来管理。每个微应用需要导出挂载、卸载等生命周期函数,single-spa会根据url变化调用这些函数控制微应用的挂载和卸载。
1. 使用方法
- 在基座应用中注册微应用。
javascript
import { registerApplication } from 'single-spa';
registerApplication({
name: 'app1',
app: () => import('src/app1/main.js'),
activeWhen: '/app1',
customProps: {
some: 'value',
}
);
singleSpa.start() // 启动基座应用
举个例子,有一个微应用app1,整个应用被打包成一个main.js文件,当路由跳转到/app1时,框架会执行main.js,把app1渲染到dom树。
- 导出微应用。
javascript
import React from 'react';
import ReactDOM from 'react-dom';
import App from './index.tsx'
export const bootstrap = () => {}
export const mount = () => {
ReactDOM.render(<App/>, document.getElementById('root'));
}
export const unmount = () => {}
微应用要导出bootstrap、mount、unmount等生命周期函数,single-spa会根据url变化调用这些函数控制微应用的加载和卸载。。
2. 局限性
single-spa实现了路由转发和微应用生命周期管理等核心功能,但是也存在以下局限性。
- 基于js entry的资源加载模式
这种模式下需要指定微应用的打包js文件。默认是在编译时加载微应用资源,无法动态加载,即无法独立部署。另外,整个微应用被打包成一个js文件,无法实现按需加载。最后,一旦打包后的文件名变了,还得同步更改基座应用的配置。
- 缺乏应用隔离机制
当有多个子应用时,存在js全局变量冲突和css冲突。,比如:微应用A声明了一个全局变量 window.a,这时候切换到微应用B,B也有一个全局变量window.a,如何保证访问到正确的值?
五、qiankun
1. 使用方法
qiankun是基于single-spa实现的更为完善的微前端框架,使用方法如下:
- 在基座应用中注册微应用。
javascript
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'app1',
entry: 'http://domain1/app1',
container: '#container',
activeRule: '/app1',
},
{
name: 'app2',
entry: 'http://domain2/app2',
container: '#container',
activeRule: '/app2',
},
]);
start();
使用方法跟single-spa差异不大,但细心的同学会发现,entry是微应用入口地址,不再是js文件。qiankun会请求入口地址,获取入口html并解析html,然后构造请求去加载html中的script、style资源 ,这种方案被称为html entry。
- 导出微应用。
javascript
import React from 'react';
import ReactDOM from 'react-dom';
import App from './index.tsx'
export const bootstrap = () => {}
export const mount = (props) => {
ReactDOM.render(<App/>, props.container.getElementById('root'));
}
export const unmount = () => {}
- 微应用打包配置
javascript
const packageName = require('./package.json').name;
module.exports = {
output: {
library: `${packageName}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${packageName}`,
},
};
微应用打包成一个umd库,并设置库名称为注册微应用时的name,这样qiankun框架就能调用微应用js了。
2. 优点
- 基于html entry的资源加载模式
在single-spa中要实现动态加载微应用资源,得借助SystemJS工具。
javascript
import { registerApplication } from 'single-spa';
registerApplication({
name: 'app1',
app: () => System.import('http://domain1/app1/main.js'),
activeWhen: '/app1',
customProps: {
some: 'value',
}
);
singleSpa.start() // 启动基座应用
只有实现了动态加载,微应用才能独立部署。另外也不再需要关注入口js文件,html entry能够根据入口地址自动加载html、js、css资源。
- js沙箱
qiankun通过js沙箱,解决了js全局变量冲突。具体是通过proxy代理window对象,为每个微应用创建一个window对象副本。当切换应用时,微应用通过proxy访问到对应的window对象副本。
- css隔离
通过shadow dom实现css隔离,这是浏览器原生支持的。为每个微应用创建一个shadow dom的父节点,shadow dom内部的dom和css不会影响外部,外部也不会访问到内部的数据。
还有一种方案就是scoped css,在css选择器里加一个前缀,不同微应用的css选择器就不会冲突。
- 应用状态管理
qiankun提供了设置全局状态和监听全局状态的方法,从而实现基座应用和微应用的状态管理。
主应用
javascript
import { initGlobalState, MicroAppStateActions } from 'qiankun';
// 初始化 state
const actions: MicroAppStateActions = initGlobalState(state);
actions.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log(state, prev);
});
actions.setGlobalState(state);
actions.offGlobalStateChange();
微应用
javascript
// 从生命周期 mount 中获取通信方法,使用方式和 master 一致
export function mount(props) {
props.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log(state, prev);
});
props.setGlobalState(state);
}
六、qiankun踩坑记录
- 微应用资源跨域
默认情况下,浏览器用<script>和<style>加载js、css资源,不受同源策略限制。而qiankun是由框架构造XHR请求获取微应用的js、css资源,存在跨域问题(主应用和微应用的域名不一致)。一般修改微应用的nginx配置即可。
javascript
server {
listen 80;
server_name app1.com;
location / {
add_header Access-Control-Allow-Origin main1.com;
}
}
- dialog组件无法找到body节点
qiankun支持两种沙箱方案,一种是shadow dom,虽然shadow dom可以实现彻底的css隔离,但也会导致微应用无法访问html、body等全局DOM节点,例如dialog、Modal等组件无法找到body节点,进而无法挂在到DOM上。所以一般推荐用另一种方案,即scoped css。
javascript
// shadow dom
start({
sandbox: {
strictStyleIsolation: true,
}
})
// scoped css
start({
sandbox: {
experimentalStyleIsolation: true,
}
})
- 微应用hash路由不生效
当基座应用使用history路由,微应用使用hash路由时,会出现在基座应用跳转微应用页面时路由不生效。因为在基座应用是使用的history api,不会触发hash事件。解决方案就是在基座应用路由微应用页面时加上触发hash事件的逻辑。
七、为什么不用iframe
iframe是浏览器原生技术,常用来在网页中嵌入另一个网页,这一点和微前端是不谋而合的。但是它的强隔离性也带来了一系列问题:
- 刷新会丢失路由状态
- dom割裂,iframe内部的弹窗无法全局展示
- 跨域,客户端的登录态无法共享,子应用需要重新登录
- 通信复杂,只能通过postmessage传递消息
八、总结
前端在面临"巨石应用"和"跨技术栈集成"等困局时,借鉴了微服务的理念,将一个单体SPA拆分为多个小型SPA,微应用之间独立开发、独立部署、独立运行,这就是微前端。然后,我们也介绍主流微前端框架single-spa和qiankun的使用方法,以及局限性和优点。最后,也分享了qiankun的相关实践以及iframe的对比。后续,我们也会有文章继续探索微前端的实现原理。
参考资料
一个js库就把你的网页的底裤🩲都扒了------import-html-entry
微前端方案 qiankun 只是更完善的 single-spa