微前端介绍
微前端
- 微前端介绍
- 微前端定义
- 微前端特点
- 微前端常见框架
- 路由分发式微前端
- iframe
- single-spa
- qiankun
- webpack5:module federation
- Web Component
- 微前端问题总结
- js隔离
微前端介绍
微前端定义
微前端(Micro-Frontends)是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。
简单的说:微前端就是在一个Web应用中独立运行其他的Web应用。
微前端特点
- 技术栈无关:主框架不限制接入应用的技术栈,微应用具备完全自主权;
- 独立开发、独立部署:微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新;
- 增量升级:在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略;
- 独立运行时:每个微应用之间状态隔离,运行时状态不共享;
- 环境隔离:应用之间 JavaScript、CSS 隔离避免互相影响;
- 消息通信:统一的通信方式,降低使用通信的成本;
- 依赖复用:解决依赖、公共逻辑需要重复维护的问题;
微前端常见框架
路由分发式微前端
路由分发式微前端,即通过路由将不同的业务分发到不同的独立前端应用上。最常用的方案是通过 HTTP 服务的反向代理来实现。
下面是一个基于路由分发的 Nginx 配置:
http {
server {
listen 80;
server_name xxx.xxx.com;
location /api/ {
proxy_pass http://localhost:3001/api;
}
location /web/admin
- List item
{
proxy_pass http://localhost:3002/api;
}
location / {
proxy_pass /;
}
}
}
优点:
- 实现简单;
- 不需要对现有应用进行改造;
- 完全技术栈无关;
缺点:
- 用户体验不好,每次切换应用时,浏览器都需要重新加载页面;
- 多个子应用无法并存;
- 局限性比较大;
- 子应用之间的通信比较困难;
- 子应用切换时需要重新登录;
iframe
iframe 作为一项非常古老的技术,也可以用于实现微前端。通过 iframe,我们可以很方便的将一个应用嵌入到另一个应用中,而且两个应用之间的 css 和 javascript 是相互隔离的,不会互相干扰。
优点:
实现简单;
- css 和 js 天然隔离,互不干扰;
- 完全技术栈无关;
- 多个子应用可以并存;
- 不需要对现有应用进行改造;
缺点:
- 用户体验不好,每次切换应用时,浏览器需要重新加载页面;
- UI 不同步,DOM 结构不共享;
- 全局上下文完全隔离,内存变量不共享,子应用之间通信、数据同步过程比较复杂;
- 对 SEO 不友好;
- 子应用切换时可能需要重新登录,体验不好;
single-spa
single-spa 提供了新的技术方案,可以帮忙我们实现类似单页应用的体验。
在 single-spa 方案中,应用被分为两类:基座应用和子应用。其中,子应用就是文章上面描述的需要聚合的子应用;而基座应用,是另外的一个单独的应用,用于聚合子应用。
和单页应用的实现原理类似,single-spa 会在基座应用中维护一个路由注册表,每个路由对应一个子应用。基座应用启动以后,当我们切换路由时,如果是一个新的子应用,会动态获取子应用的 js 脚本,然后执行脚本并渲染出相应的页面;如果是一个已经访问过的子应用,那么就会从缓存中获取已经缓存的子应用,激活子应用并渲染出对应的页面。
优点:
- 切换应用时,浏览器不用重载页面,提供和单页应用一样的用户体验;
- 完全技术栈无关;
- 多个子应用可并存;
- 生态丰富;
缺点:
- 需要对原有应用进行改造,应用要兼容接入 sing-spa 和独立使用;
- 有额外的学习成本;
- 使用复杂,关于子应用加载、应用隔离、子应用通信等问题,需要框架使用者自己实现;
- 子应用间相同资源重复加载;
- 启动应用时,要先启动基座应用;
qiankun
和 single-spa 一样,qiankun 也能给我们提供类似单页应用的用户体验。qiankun 是在 single-spa 的基础上做了二次开发,在框架层面解决了使用 single-spa 时需要开发人员自己编写子应用加载、通信、隔离等逻辑的问题,是一种比 single-spa 更优秀的微前端方案。
优点:
- 切换应用时,浏览器不用重载页面,提供和单页应用一样的用户体验;
- 相比 single-spa,解决了子应用加载、应用隔离、子应用通信等问题,使用起来相对简单;
- 完全和技术栈无关;
- 多个子应用可并存;
缺点:
- 需要对原有应用进行改造,应用要兼容接入 qiankun 和独立使用;
- 有额外的学习成本;
- 相同资源重复加载;
- 启动应用时,要先启动基座应用;
webpack5:module federation
webpack5,提供了一个新的特性 - module federation。基于这个特性,我们可以在一个 javascript 应用中动态加载并运行另一个 javascript 应用的代码,并实现应用之间的依赖共享。
通过 module federation,我们可以在一个应用里面动态渲染另一个应用的页面,这样也就实现了多个子应用的聚合。
优点:
- 不需要对原有应用进行改造,只需改造打包脚本;
- 切换应用时,浏览器不用重载页面,提供和单页应用一样的用户体验;
- 多个子应用可并存;
- 相同资源不需要重复加载;
- 开发技术栈无关;
- 应用启动后,无需加载与自己无关的资源;
- 免登友好;
缺点:
- 构建工具只能使用 webpack5;
- 有额外的学习成本;
- 对老项目不友好,需要对 webpack 进行改造;
Web Component
基于 Web Component 的 Shadow Dom 能力,我们也可以实现微前端,将多个子应用聚合起来。
Shadow Dom 的用法如下:
const shadow = document.querySelector('#hostElement').attachShadow({mode: 'open'});
// url 为应用的地址,基于 fetch,我们可以获取到应用的 html 模板,添加到指定节点下
fetch(url).then(res => {
shadow.innerHTML = res
});
优点:
- 实现简单;
- css 和 js 天然隔离,互不干扰;
- 完全技术栈无关;
- 多个子应用可以并存;
- 不需要对现有应用进行改造;
缺点:
- 主要是浏览器兼容性问题;
- 开发成本较高;
微前端问题总结
微前端框架中,js隔离、样式隔离、元素隔离是必须解决的三个问题,下面我们就来分别说说这三个问题是什么?怎么解决?
js隔离
问题:
情况1:都对全局变量赋值
应用A,写 window.r = 1;然后有应用B,又写 window.r = 2,这就乱套了
情况2:都设置事件
应用A,window.addEventListener(‘click’,()=>console.log(‘A’));
应用B,window.addEventListener(‘click’,()=>console.log(‘B’));,这就乱套了
解决:
方法一用 Proxy 代理
es2015 Reflect属于一个静态类或者设置属性等用法
const rawWindow = window
const proxyWindow = new Proxy({},{
get: (target, key): unknown => {
// 原 target 上有就返回,否则返回 rawWindow 属性
return Reflect.has(target, key) ? Reflect.get(target, key) : Reflect.get(rawWindow, key)
},
set: (target, key, value): boolean => {
if(!Object.prototype.hasOwnProperty.call(target, key) && Object.prototype.hasOwnProperty.call(rawWindow, key)){
const descriptor = Object.getOwnPropertyDescriptor(rawWindow, key)
const { configurable, enumerable, writable, set } = descriptor!
// set value because it can be set
rawDefineProperty(target, key, {
value,
configurable,
enumerable,
writable: writable ?? !!set,
})
} else {
Reflect.set(target, key, value)
}
}
})
将 window.r = 1
和 window.addEventListener('click',()=>console.log('A'))
包括到自执行函数里面
A和B互不干扰
;(function(window){
window.r = 1
window.addEventListener('click',()=>console.log('A'))
})(proxyWindowA)
;(function(window){
window.r = 2
window.addEventListener('click',()=>console.log('B'))
})(proxyWindowB)
法二 用快照
快照隔离有个前提条件是,当前还有一个应用显示,不能出现多个应用并存显示在界面上,应用A,B切换时,比如当前应用是A,现在要切入到应用B
- 暂存起来应用A的全局变量和事件
- 恢复全局变量和事件到应用A之前
- 检查之前是否保持有应用B的全局变量和事件,如果有,则载入
样式隔离
问题
同理,各个应用之前可能相互设置标签样式,会相互影响,或者影响全局样式,比如应用A给body设置样式,应用B也给body设置样式
方法一 样式增加不同前缀
每个应用通过前缀独立区分开,京东micro-app默认是采用的这个策略,唯一注意的一个小点是,基座样式会影响子应用的样式,所以需要注意基座中不要写太多样式
方法二 ShadawDom
大多数Html标签都有 attachShadow()
方法给指定的元素挂载一个 Shadow DOM。参数是open
或closed
ShadawDom 样式绝对隔离,不用加前缀,如下图
//open 是外界可以访问到Element.shadowRoot再访问到内部元素,closed就是完全不能访问内部元素
var shadowroot = element.attachShadow('open|closed')
元素隔离
元素隔离是 基座应用和子应用都有一个元素<div id='root'></div>
,此时子应用通过document.querySelector('#root')
,因为js隔离已经做了代理,此时document.querySelector
只是子应用本身了