# 微前端
# 目标
核心目标是将巨石应用拆解成若干可以自治的松耦合微应用,这样才能确保微应用真正具备独立开发、独立运行的能力(防止巨石应用)
# 介绍
微前端按业务功能将一整块前端应用分解成一系列更小的、更内聚的微前端应用,同时通过明确的交互协议来管理这些应用间的依赖关系,实现不同业务的解耦合。并将每个前端应用交给独立团队负责,各自独立开发、独立部署、充分利用并行性。
- 微前端是一种架构风格,monorepo 是一种开发策略,模块联邦是一种打包和部署方式
- 微前端将大型的前端应用分解成一组小的、独立的前端服务,每一个小的前端服务可以由不同的团队独立开发、部署和维护。微前端的目标是提高大型前端项目的可维护性、可扩展性和灵活性
- monorepo 是一种开发策略(技巧),它将多个项目或服务的代码放在一个单一的版本控制系统仓库中,有助于简化依赖管理、代码共享和团队协作。对于工具库能更好的暴露单独模块。
- 模块联邦是 Webpack5 引入的一个特性,允许将多个 Webpack 项目远程接入、联合起来,形成一个单一的应用,每个模块可以独立开发和部署,同时共享彼此的代码,模块联邦是一种新的打包和部署前端应用的方式。
# 微前端应用
# EMP
模块联邦技术,微前端构建方案,是基于 webpack 5 module federation 的微前端方案。
特点
- webpack 联邦编译可以保证所有子应用依赖解耦;
- 应用间去中心化的调用、共享模块;
- 模块远程 ts 支持;
不足
- 对 webpack 强依赖,老旧项目不友好;
- 没有有效的 css 沙箱和 js 沙箱,需要靠用户自觉;
- 子应用保活、多应用激活无法实现;
- 主、子应用的路由可能发生冲突;
底层原理 这个东西有点类似于拆包,也可以叫模块共享,例如 React 有个模块可以共享给 Vue 项目用 Vue2 的组件可以共享给 Vue3 用。
# single-spa
single-spa 是微前端的基础框架,乾坤框架就是基于 single-spa 来实现的,在 single-spa 的基础上做了一层封装,也解决了 single-spa 的一些缺陷。
原理
- single-spa 原理是通过网络请求请求到文件资源(使用 import 导入语法时,需要在 importmap 中注册包名称和地址)
- 对于样式隔离,single-spa 推荐的方法有:Scoped CSS 和 shadow DOM
分析
single-spa 实现了一个微前端框架需要具备的各种功能,但是实现的又不够彻底,遗留了很多需要解决的问题。
# 乾坤
乾坤是在 single-spa 的基础之上进行的进一步封装
特点
- html entry 的方式引入子应用,相比 js entry 极大的降低了应用改造的成本;
- 完备的沙箱方案,js 沙箱做了 SnapshotSandbox、LegacySandbox、ProxySandbox 三套渐进增强方案,css 沙箱做了 strictStyleIsolation、experimentalStyleIsolation 两套适用不同场景的方案;
- 做了静态资源预加载能力;
不足
- 适配成本比较高,工程化、生命周期、静态资源路径、路由等都要做一系列的适配工作;
- css 沙箱采用严格隔离会有各种问题,js 沙箱在某些场景下执行性能下降严重;
- 无法同时激活多个子应用,也不支持子应用保活;
- 无法支持 vite 等 esmodule 脚本运行;
# 无界
特点
- 接入简单只需要四五行代码
- 不需要针对 vite 额外处理
- 预加载
- 应用保活机制
不足
- 隔离 js 使用一个空的 iframe 进行隔离
- 子应用 axios 需要自行适配
- iframe 沙箱的 src 设置了主应用的 host,初始化 iframe 的时候需要等待 iframe 的 location.orign 从 'about:blank' 初始化为主应用的 host,这个采用的计时器去等待的不是很悠亚。
底层原理 使用 shadowDom 隔离 css,js 使用空的 iframe 隔离,通讯使用的是 proxy
# 原理
无界的原理就是使用开启影子 dom 来进行样式的隔离,通过使用 attachShadow 方法开启影子 dom(继承 HTMLElement),获得的 shadow 影子 dom 添加 template(无界的初始模板)为子元素,这样就进行了样式隔离,最后通过 window.customElement.define 注册 dom 元素 wu-jie,使用标签即可。
index.js:
window.onload = () => { | |
class WuJie extends HTMLElement { | |
constructor() { | |
super() | |
this.init() | |
this.getAttr('url') | |
} | |
init() { | |
const shadow = this.attachShadow({ mode: "open" }) // 开启影子 dom 也就是样式隔离 | |
const template = document.querySelector('#wu-jie') as HTMLTemplateElement | |
console.log(template); | |
shadow.appendChild(template.content.cloneNode(true)) | |
} | |
getAttr (str:string) { | |
console.log('获取参数',this.getAttribute(str)); | |
} | |
// 生命周期自动触发有东西插入 | |
connectedCallback () { | |
console.log('类似于vue 的mounted'); | |
} | |
// 生命周期卸载 | |
disconnectedCallback () { | |
console.log('类似于vue 的destory'); | |
} | |
// 跟 watch 类似 | |
attributeChangedCallback (name:any, oldVal:any, newVal:any) { | |
console.log('跟vue 的watch 类似 有属性发生变化自动触发'); | |
} | |
} | |
window.customElements.define('wu-jie', WuJie) | |
} |
index.html:
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Document</title> | |
<script src="./index.js"></script> | |
</head> | |
<body> | |
<!-- 外层写一个 div 测试隔离 --> | |
<div>我是div</div> | |
<wu-jie url="xxxxxx"></wu-jie> | |
<template id="wu-jie"> | |
<!--div 的样式是作用于全局的 --> | |
<style> | |
div { | |
background: red; | |
} | |
</style> | |
<div> | |
样式隔离的 | |
</div> | |
</template> | |
</body> | |
</html> |
# 预加载
使用:
需要从 wujie 的实例导出 preloadApp
, 参数跟 startApp
一致,预加载必须开启 exec 选项
preloadApp({ name: "vue3", url: "http://127.0.0.1:5174/", exec: true }) | |
preloadApp({ name: "react", url: "http://127.0.0.1:5175/", exec: true }) |
- exec 代表是否进行预加载,默认值是 true
- js 的执行模式,由于子应用的执行会阻塞主应用的渲染线程,当设置为 true 时 js 采取类似于 react fiber 的模式方式间断执行,每个 js 文件的执行都包裹在
requestidlecallback
中,每执行一个 js 文件后就可以返回去响应外部的输入,不会造成浏览器渲染和加载之间的冲突从而造成卡顿,但是这个颗粒度是 js 文件,所以应该保证 js 文件不应过大。
浏览器一帧之内要做的事情:
- 处理用于输入(事件)
- 执行定时任务
- 执行 requestAnimationFrame
- 执行 dom 的回流和重绘
- 计算更新涂层的更新指令
- 绘制指令合并主线程,如果有空余时间执行 requestidlecallbask
react 也有该机制 但是 react 并没有用 requestidlecallback
,说是这个东西经过测试可能会超过 16ms,超过 16ms 绘制就会看起来很卡 所以 react16 是用的 requestAnimationFrame + postMessage
实现的那为什么不用 setTimeOut
setTimeOut 及时为 0 也会有一个最小毫秒延迟 4ms,所以是用了 postMessage,react18 又换成了 MessageChannel
实现了队列方式去执行任务。
# micro-app
micro-app 是基于 webcomponent + qiankun sandbox 的微前端方案。
特点
- 使用 webcomponet 加载子应用相比 single-spa 这种注册监听方案更加优雅;
- 复用经过大量项目验证过 qiankun 的沙箱机制也使得框架更加可靠;
- 组件式的 api 更加符合使用习惯,支持子应用保活;
- 降低子应用改造的成本,提供静态资源预加载能力;
不足
- css 沙箱依然无法绝对的隔离,js 沙箱做全局变量查找缓存,性能有所优化;
- 支持 vite 运行,但必须使用 plugin 改造子应用,且 js 代码没办法做沙箱隔离;
- 对于不支持 webcompnent 的浏览器没有做降级处理;
底层原理 js 隔离跟 qiankun 类似也是使用 proxy + with,css 隔离自定义前缀类似于 scoped
# 模块联邦
模块联邦是跟 webpack5
强耦合的,是基于 webpack5 内置插件的 无须安装
它相当于是一个去中心化技术,它可以让多个独立构建的应用之间,动态的调用彼此的模块。这种运行机制,可以让我们轻松的拆分应用,真正做到跨应用的模块共享。
配置过程就是在 webpack.config.js 中配置 ModuleFederationPlugin 插件,打完包之后观察会发现其实就是 import 函数动态加载,使用模块联邦的好处就在于之前当多个项目共有的模块改变时,每一个项目都需要重新 install 一下,而使用模块联邦之后,就相当于动态导入,免去了重新 install 的过程(由项目的克隆转为类似软链接的过程)。