什么是微前端?常见应用场景有哪些?
什么是微前端?
微前端(Micro-Frontends)是一种将单体应用程序分解为较小、更独立的部分的架构模式。它的目标是使团队可以独立开发、部署和扩展应用程序的不同部分,而不会影响整体应用程序的稳定性和性能。
微前端的主要特点包括:
-
模块化:应用程序被分解为较小的模块,每个模块可以独立开发和部署。 -
独立性:每个模块可以使用不同的技术栈和框架,而不会影响整体应用程序的稳定性和性能。 -
集成:不同的模块可以集成在一起,形成一个完整的应用程序。 -
可扩展性:团队可以根据需要添加或删除模块,以满足不同的业务需求。
微前端的实现方式包括:
-
iframe:将不同的模块嵌入到主应用程序的iframe中。 -
Web Components:使用Web Components技术将不同的模块封装为自定义元素。 -
服务端集成:使用后端集成技术将不同的模块集成到一个应用程序中。
微前端的优点包括:
-
提高开发效率:团队可以独立开发和部署模块,减少开发和部署的时间和成本。 -
提高可维护性:模块化的架构使得应用程序更易于维护和扩展。 -
提高可复用性:不同的模块可以在不同的应用程序中共享和重用。 -
提高性能:只加载需要的模块,减少整体应用程序的加载时间和资源消耗。
微前端的缺点包括:
-
复杂性:微前端需要更多的技术和架构设计,增加了开发和维护的复杂性。 -
安全性:使用iframe或Web Components时,需要考虑安全性问题,避免跨域攻击和XSS漏洞。
微前端的应用
在Vue3和React项目中,我们仍可以通过各种方式运用微前端:
-
使用webpack或Rollup等构建工具将应用程序分解为较小的模块,每个模块可以独立开发和部署。 -
使用iframe或web components技术将不同的模块集成到同一个应用程序中。具体表现为:在Vue3中,可以使用Vue Custom Elements插件将Vue组件封装为Web Components;在React中,可以使用React Custom Elements库将React组件封装为Web Components。 -
使用路由或事件机制在不同的模块之间进行通信。在Vue3中,可以使用provide/inject API或EventBus来实现组件之间的通信;在React中,可以使用React Context或Redux等状态管理库来实现组件之间的通信。 -
使用微前端框架,例如Single-SPA或qiankun,来管理不同模块之间的依赖关系和生命周期。这些框架提供了一个统一的入口和路由系统,使得不同的模块可以独立开发和部署,并且可以在运行时动态加载和卸载。
微前端的常见应用场景
-
电商平台:将商品列表、购物车、订单管理等不同的模块独立开发和部署,通过微前端框架(例如qiankun)集成到一个应用程序中。这样可以提高开发效率和可维护性,同时可以根据需求灵活添加或删除模块。 -
大型企业门户网站:将不同的业务模块(例如人力资源、财务、行政、物流等)独立开发和部署,通过微前端框架(例如Single-SPA)集成到一个应用程序中。这样可以实现跨团队协作和灵活的业务拓展,同时可以提高可维护性和可复用性。 -
社交媒体平台:将用户个人主页、消息中心、动态发布等。 -
在线教育平台:将课程列表、视频播放、在线测验等。 -
微服务架构系统:将不同的微服务(例如用户认证、支付、推荐等)独立开发和部署,通过微前端框架集成到一个应用程序中。
路由系统及 Future State
我们在一个实现了微前端内核的产品中,正常访问一个子应用的页面时,可能会有这样一个链路

此时浏览器的地址可能是·https://app.alipay.com/subApp/123/detail·,如果手动刷新浏览器的话将会是很复杂的情况。
由于子应用都是lazy load的,刷新时主框架的资源会被重新加载,同时异步load子应用的静态资源。而激活子应用后,资源没有全部加载完毕,导致路由注册表中可能没有匹配子应用/subApp/123/detail
的规则,这时候会直接报错。
这个问题在所有 lazy load 方式加载子应用的方案中都会碰到,早些年前 angularjs 社区把这个问题统一称之为 Future State。
主框架配置子应用的路由为 subApp: { url: '/subApp/**', entry: './subApp.js' }
,则当浏览器的地址为 /subApp/abc
时,框架需要先加载 entry 资源,待 entry 资源加载完毕,确保子应用的路由系统注册进主框架之后,再去由子应用的路由系统接管 url change 事件。同时在子应用路由切出时,主框架需要触发相应的 destroy 事件,子应用在监听到该事件时,调用自己的卸载方法卸载应用,如 React 场景下 destroy = () => ReactDOM.unmountAtNode(container)
。
微前端有哪些框架
基于上述对微前端整体概念和理论的阐述,目前业界已经有不少框架来帮助开发者轻松的集成微前端架构,例如下面这些:
-
Mooa:基于Angular的微前端服务框架 -
Single-Spa:最早的微前端框架,兼容多种前端技术栈。 -
Qiankun:基于Single-Spa,阿里系开源微前端框架。 -
Icestark:阿里飞冰微前端框架,兼容多种前端技术栈。 -
Ara Framework:由服务端渲染延伸出的微前端框架。
以qiankun为例
以下是 qiankun 提供的特性:
-
实现了子应用的加载,在原有 single-spa 的 JS Entry 基础上再提供了 HTML Entry
复习一下single-spa是怎么注册子应用的
singleSpa.registerApplication(
'appName',
() => System.import('appName'),
location => location.pathname.startsWith('appName'),
);
即采用JS ENTRY 的方式接入微应用,也就是输出一个JS -> bootstrap -> mount -> unmount函数。
而,提供HTML ENTRY 入口来接入子应用的方式,更加简便
registerMicroApps([
{
name: 'react app', // 子应用名
entry: '//localhost:7100', // 子应用 html 或网址
container: '#yourContainer', // 挂载容器选择器
activeRule: '/yourActiveRule', // 激活路由
},
]);
start();
然而,HTML Entry 并不是给个 HTML 的 url 就可以直接接入整个子应用这么简单了。滞留的各种tag依然需要处理。所以就有了import-html-entry
import importHTML from 'import-html-entry';
importHTML('./subApp/index.html')
.then(res => {
console.log(res.template); // 拿到 HTML 模板
res.execScripts().then(exports => { // 执行 JS 脚本
const mobx = exports; // 获取 JS 的输出内容
// 下面就是拿到 JS 入口的内容,并用来做一些事
const { observable } = mobx;
observable({
name: 'kuitos'
})
})
});
综上,加载子应用的伪代码如下
// 解析 HTML,获取 html,js,css 文本
const {htmlText, jsText, cssText} = importHTMLEntry('https://xxxx.com')
// 创建容器
const $= document.querySelector(container)
$container.innerHTML = htmlText
// 创建 style 和 js 标签
const $style = createElement('style', cssText)
const $script = createElement('script', jsText)
$container.appendChild([$style, $script])
-
CSS和 JS 隔离
ShadowDOM :它可以让一个 dom 拥有自己的“影子” DOM 树,这个 DOM 树不能在主文档中被任意访问,可以拥有局部样式规则,天然实现了样式隔离,如上图所示,被代理的 dom节点称为 shadow host,shadow tree 中的根节点称为 shadow root。
SnapshotSandbox 快照沙箱
let sandbox = new SnapshotSandbox();
var a = '主应用A';
var c = '主应用C';
console.log('主应用原来的 Window:',a, c);
function beforeMounted(){
sandbox.active();
console.log("加载子应用前");
}
function beforeUnMounted(){
sandbox.inactive();
console.log("卸载子应用前");
}
function app1(){
beforeMounted();
window.a = 'app1A'; // 修改
window.c = null; // 删除
window.d = 'app1D'; // 新增
console.log("子应用的 Window:",window.a, window.c, window.d);
beforeUnMounted();
}
app1();
console.log('主应用现在的 Window:', a, c, d);
主应用中声明两个变量 a 和 c,分别赋值主应用 A 和主应用 C,然后加载子应用之后对全局变量 a c 进行修改,并且新增 d,最后卸载时再打印 a c d。
沙箱快照的核心思想如下:在子应用挂载前,对当前主应用的全局变量保存,然后恢复之前的子应用环境,在子应用运行期间则正常 get 和 set,在卸载时保存当前变量恢复主应用变量,整个过程类似于中断和中断恢复。
但这里也有一个比较明显的缺点就是每次切换时需要去遍历 window,这种做法会有较大的时间消耗。在不支持 Proxy 的场景下会降级为 snapshotSandBox,如同他的名字一样。
-
更多的生命周期:beforeMount, afterMount, beforeUnmount, afterUnmount
这五个钩子可以在主应用中注册子应用时使用。
和 single-spa 一样的是子应用的接入必须暴露三个生命周期(毕竟是基于 single-spa 实现的):
-
Bootstrap: 在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存。 -
Mount:触发应用的渲染方法。 -
Unmount:卸载微应用的应用实例。
-
子应用预加载
子应用预加载是一种优化策略,使用 requestIdleCallback 通过时间切片的方式去加载静态资源,在浏览器空闲时间去执行回调函数,避免浏览器卡顿
-
全局状态管理
各个子应用需要和主应用进行通信,以获得必要的信息,子应用之间也可能会有少量的通信需要,在 qiankun 中使用的是一种订阅发布模式的通信方法。
-
全局错误处理
当运行中发生错误时,需要对其进行捕获,这里主要监听了 error 和 unhandledrejection 两个错误事件。
window.addEventListener('error', errorHandler);
window.addEventListener('unhandledrejection', errorHandler);
跨域攻击和XSS漏洞的规避
漏洞的实例代码:
-
配置CORS(跨域资源共享)策略:
在服务端配置CORS策略,限制跨域请求的来源和请求方法,例如:
// Node.js Express示例
const express = require('express');
const app = express();
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000'); // 允许来自localhost:3000的请求
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE'); // 允许的请求方法
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); // 允许的请求头
next();
});
app.listen(8080, () => console.log('Server started on port 8080'));
-
使用HTTPOnly Cookie:
在设置Cookie时,将其标记为HTTPOnly,例如:
// Node.js Express示例
app.use((req, res, next) => {
res.setHeader('Set-Cookie', 'sessionId=123; HttpOnly'); // 将sessionId标记为HTTPOnly
next();
});
-
对用户输入进行过滤和转义:
在前端和后端对用户输入进行过滤和转义,例如:
// 前端示例:使用DOMPurify库对用户输入进行过滤
import DOMPurify from 'dompurify';
const userInput = '<script>alert("XSS attack!");</script>';
const filteredInput = DOMPurify.sanitize(userInput);
console.log(filteredInput); // 输出:alert("XSS attack!");
// 后端示例:使用OWASP Java Encoder库对用户输入进行转义
import org.owasp.encoder.Encode;
String userInput = "<script>alert(\"XSS attack!\");</script>";
String filteredInput = Encode.forHtml(userInput);
System.out.println(filteredInput); // 输出:<script>alert("XSS attack!");</script>
-
使用Content Security Policy(CSP):
在HTTP响应头中设置CSP策略,限制页面中可以加载的资源和脚本,例如:
// Node.js Express示例
app.use((req, res, next) => {
res.setHeader('Content-Security-Policy', "default-src 'self' https://cdnjs.cloudflare.com"); // 允许加载来自同一域名和https://cdnjs.cloudflare.com的资源
next();
});
-
使用HTTPS:
使用HTTPS协议进行通信,例如:
<!-- 前端示例:使用https协议加载资源 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
-
对模块进行权限控制:
对不同的模块进行权限控制,限制不同的用户或角色可以访问哪些模块,例如:
// 前端示例:使用Vue Router对路由进行权限控制
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{ path: '/', component: Home },
{ path: '/dashboard', component: Dashboard, meta: { requiresAuth: true } }, // 需要登录才能访问的路由
{ path: '/admin', component: Admin, meta: { requiresAdmin: true } }, // 需要管理员权限才能访问的路由
];
const router = createRouter({
history: createWebHistory(),
routes
});
router.beforeEach((to, from, next) => {
const isAuthenticated = checkAuth(); // 检查用户是否已登录
const isAdmin = checkAdmin(); // 检查用户是否是管理员
if (to.meta.requiresAuth && !isAuthenticated) {
next('/login'); // 如果需要登录才能访问,但用户未登录,则跳转到登录页面
} else if (to.meta.requiresAdmin && !isAdmin) {
next('/access-denied'); // 如果需要管理员权限才能访问,但用户不是管理员,则跳转到访问拒绝页面
} else {
next(); // 否则允许访问
}
});
总而言之,微前端这种开发理念特别适合一个部门把自己一系列的产品都集合在一起,也特别适合项目分模块重构等等。