文章目录
微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。
核心价值:
技术栈无关:主框架不限制接入应用的技术栈,微应用具备完全自主权
独立开发、独立部署:微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
增量升级:在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略
独立运行时:每个微应用之间状态隔离,运行时状态不共享
qiankun
qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。
- 基于 single-spa 封装,提供了更加开箱即用的 API。
- 技术栈无关,任意技术栈的应用均可 使用/接
- HTML Entry 接入方式,让你接入微应用像使用 iframe 一样简单。
- 样式隔离,确保微应用之间样式互相不干扰。
- JS 沙箱,确保微应用之间 全局变量/事件 不冲突。
- 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
- umi 插件,提供了 @umijs/plugin-qiankun 供 umi 应用一键切换成微前端架构系统
主应用
① 安装 qiankun
npm i qiankun -S
或者 yarn add qiankun
② 在主应用中注册微应用
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'vue-app',
entry: '//localhost:8080',
container: '#container',
activeRule: '/activeRule',
},
]);
start()
当微应用信息注册完之后,一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑,所有 activeRule 规则匹配上的微应用就会被插入到指定的 container 中,同时依次调用微应用暴露出的生命周期钩子。
微应用
微应用不需要额外安装任何其他依赖即可接入 qiankun 主应用。
① 导出相应的生命周期钩子
微应用需要在自己的入口 js (通常就是你配置的 webpack 的 entry js) 导出 bootstrap、mount、unmount 三个生命周期钩子,以供主应用在适当的时机调用。
/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。*/
export async function bootstrap() {
console.log('app bootstraped');
}
/**应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法*/
export async function mount(props) {
// 渲染方法等
}
/** 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例*/
export async function unmount(props) {}
/** 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效*/
export async function update(props) {
console.log('update props', props);
}
② 配置微应用的打包工具
除了代码中暴露出相应的生命周期钩子之外,为了让主应用能正确识别微应用暴露出来的一些信息,微应用的打包工具需要增加如下配置:
const packageName = require('./package.json').name;
module.exports = {
output: {
library: `${packageName}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${packageName},
}
独立调试子项目
在单独运行和调试每个子项目时,可以避免为前端架构带来的复杂性;
单独启动子项目:在开发环境中,可以通过独立启动子项目来进行调试。通常在子项目的package.json中配置
独立的开发服务器:为每个子项目配置单独的开发服务器端口,便于在独立环境下调试;
在主应用中调试子应用
主项目配置:确保主项目的配置允许加载和热更新子项目。在主项目的qiankun
配置中定义子项目的入口。
import { registerMicroApps , start } from 'qiankun';
registerMicroApps([
{
name:'sub-app-1',
entry:'//localhost:3001',
container:'#container',
activeRule:'/app1',
}
]);
start();
通信
子应用和主应用之间的通信可以通过多种方式实现,包括全局状态管理、props传递、路由参数共享等。以下是详细介绍:
全局状态管理
qiankun提供了全局状态管理功能,通过actions
进行通信。
- 初始化全局状态:在主应用中,使用
initGlobalState
方法创建全局状态,并返回一个MicroAppStateActions
实例。 - 设置全局状态:子应用可以通过
setGlobalState
方法修改全局状态。 - 监听全局状态变化:子应用可以使用
onGlobalStateChange
方法监听全局状态的变化。
通过props传递数据
在注册子应用时,可以通过props
属性将数据传递给子应用。
- 主应用传递数据:在注册子应用时,通过
props
属性传递数据。 - 子应用接收数据:子应用在挂载时接收这些
props
,并在组件中使用。
路由参数共享
通过在URL中添加参数,主应用和子应用可以共享数据。
- 父传子:主应用在跳转到子应用时,可以通过路由参数传递数据。
- 子传父:子应用在内部路由跳转时,可以通过URL参数将数据传递给主应用。
使用localStorage/sessionStorage
主应用和子应用可以通过浏览器的localStorage
或sessionStorage
进行数据共享。
- 主应用设置数据:主应用可以使用
localStorage.setItem
方法设置数据。 - 子应用获取数据:子应用可以使用
localStorage.getItem
方法获取数据。
事件发布/订阅
qiankun提供了一个全局的事件总线,可以通过事件发布/订阅机制进行通信。
- 发布事件:主应用可以使用
window.dispatchEvent
方法发布事件。 - 订阅事件:子应用可以使用
document.addEventListener
方法订阅事件。
通过这些通信方式,qiankun子应用和主应用之间可以实现高效、灵活的数据传递和状态管理,从而提升微前端应用的性能和用户体验。
状态隔离
乾坤(Qiankun)通过沙箱机制来隔离各个微应用,确保它们在同一个页面中不会相互干扰。
乾坤通过以下技术实现了微应用的沙箱隔离:
全局变量隔离:使用 Proxy 对象拦截和管理全局变量的读写操作。
样式隔离:使用 Shadow DOM 或 scoped CSS 防止样式冲突。
事件隔离:拦截和管理全局事件,确保事件不会跨微应用传播。
生命周期管理:定义详细的生命周期钩子,确保微应用在不同阶段的行为可控。
通过这些机制,乾坤能够有效地隔离各个微应用,确保它们在同一个页面中稳定运行。
以下是乾坤实现沙箱的主要技术和步骤:
一、 沙箱实现原理
- 全局变量隔离:
- 乾坤通过代理(Proxy)对象来拦截和管理全局变量(如 window 对象)的读写操作,从而实现全局变量的隔离。
- 当微应用尝试访问或修改全局变量时,沙箱会捕获这些操作并进行处理,确保不会影响其他微应用。
- 样式隔离:
- 乾坤使用 Shadow DOM 或 scoped CSS 来隔离微应用的样式,防止样式冲突。
- 对于不支持 Shadow DOM 的浏览器,乾坤会通过 CSS 前缀或其他方式来实现样式隔离。
- 事件隔离:
- 乾坤会拦截和管理全局事件(如 click、resize 等),确保事件不会跨微应用传播。
- 通过事件代理和事件委托,实现事件的精确控制和隔离。
- 生命周期管理:
- 乾坤为每个微应用定义了详细的生命周期钩子,包括 bootstrap、mount 和 unmount,确保微应用在不同阶段的行为可控。
- 在 unmount 阶段,乾坤会清理微应用的全局变量、事件监听器等,确保微应用卸载后不会留下残留。
沙箱机制代码实现示例
以下是一个简单的示例,展示了乾坤如何通过 Proxy 对象实现全局变量隔离:
// 沙箱类
class Sandbox {
constructor() {
this.originalWindow = window; // 保存原始的 window 对象
this.proxyWindow = new Proxy(window, {
get: (target, key) => {
// 检查是否已经存在隔离的变量
if (this[key] !== undefined) {
return this[key];
}
return target[key];
},
set: (target, key, value) => {
// 检查是否已经存在隔离的变量
if (this[key] !== undefined) {
this[key] = value;
return true;
}
target[key] = value;
return true;
}
});
}
activate() {
// 激活沙箱,将 window 替换为 proxyWindow
window = this.proxyWindow;
}
deactivate() {
// 恢复原始的 window 对象
window = this.originalWindow;
}
clear() {
// 清理沙箱中的所有变量
for (const key in this) {
if (this.hasOwnProperty(key) && key !== 'originalWindow' && key !== 'proxyWindow') {
delete this[key];
}
}
}
}
// 使用沙箱
const sandbox = new Sandbox();
// 激活沙箱
sandbox.activate();
// 模拟微应用的全局变量操作
window.myVar = 'Hello, Qiankun!';
// 检查沙箱中的全局变量
console.log(sandbox.myVar); // 输出: Hello, Qiankun!
// 恢复原始的 window 对象
sandbox.deactivate();
// 清理沙箱
sandbox.clear();
// 检查原始的 window 对象
console.log(window.myVar); // 输出: undefined
代码详细解释
-
构造函数:
constructor 中保存了原始的 window 对象,并创建了一个 Proxy 对象 proxyWindow,用于拦截对 window 的访问。 -
拦截读取操作:
get 方法拦截对 window 对象属性的读取操作。如果沙箱中已经存在该属性,则返回沙箱中的值;否则返回原始 window 对象中的值。 -
拦截写入操作:
set 方法拦截对 window 对象属性的写入操作。如果沙箱中已经存在该属性,则更新沙箱中的值;否则更新原始 window 对象中的值。 -
激活和恢复:
activate 方法将 window 替换为 proxyWindow,激活沙箱。
deactivate 方法将 window 恢复为原始的 window 对象,退出沙箱。 -
清理:
clear 方法清理沙箱中的所有变量,确保微应用卸载后不会留下残留。
优势
- 隔离性:通过 Proxy 拦截,确保微应用对全局变量的读写操作不会影响其他微应用。
- 灵活性:可以在 get 和 set 方法中添加更多的逻辑,例如日志记录、权限检查等。
- 透明性:对微应用来说,使用 window 对象的体验与未使用沙箱时相同,无需修改微应用的代码。
通过这种方式,乾坤等微前端框架能够有效地隔离各个微应用的全局变量,确保它们在同一个页面中稳定运行。
使用 Proxy 对象拦截和管理全局变量的读写操作
使用 Proxy 对象拦截和管理全局变量的读写操作是实现沙箱机制的一种常见方法。Proxy 是 JavaScript 提供的一个内置对象,用于定义自定义行为(也称为陷阱,traps)来拦截并控制对目标对象的操作。在微前端框架中,Proxy 可以用来拦截对 window 对象的访问,从而实现全局变量的隔离。
详细步骤
-
创建 Proxy 对象:
使用 new Proxy(target, handler) 创建一个 Proxy 对象,其中 target 是要拦截的目标对象(通常是 window),handler 是一个对象,定义了各种拦截操作的自定义行为。 -
定义拦截行为:
handler 对象中可以定义多种拦截操作,例如 get、set、apply、construct 等。这里主要关注 get 和 set 方法,用于拦截对全局变量的读取和写入操作。 -
激活和恢复 Proxy:
在微应用启动时激活 Proxy,在微应用卸载时恢复原始的 window 对象。
二,Shadow DOM
Shadow DOM 是一种 Web 技术,允许你在文档中创建独立的 DOM 树,并将其附加到一个元素上。这些独立的 DOM 树与主文档的其余部分隔离,因此可以避免样式和脚本的冲突。
实现步骤
- 创建 Shadow Root:
为每个微应用的根元素创建一个 Shadow Root。 - 插入样式:
将微应用的样式插入到 Shadow Root 中,而不是主文档的 中。 - 插入内容:
将微应用的内容插入到 Shadow Root 中。
Shadow Dom示例代码
!-- HTML 结构 -->
<div id="app-root"></div>
<script>
// 获取微应用的根元素
const rootElement = document.getElementById('micri-app-root');
// 创建 Shadow Root
const shadowRoot = rootElement.attachShadow({ mode: 'open' });
// 插入样式
const style = document.createElement('style');
style.textContent = `
.app-header {
background-color: blue;
color: white;
}
`;
shadowRoot.appendChild(style);
// 插入内容
const content = document.createElement('div');
content.className = 'app-header';
content.textContent = 'Hello, Qiankun!';
shadowRoot.appendChild(content);
</script>
三,Scoped CSS
Scoped CSS 是一种在 HTML 中为特定组件或部分定义样式的机制。通过在<style>
标签中使用 scoped 属性,可以确保样式仅应用于当前元素及其子元素。
Scoped CSS实现步骤
- 创建带有 scoped 属性的
<style>
标签:
在微应用的根元素内部创建一个带有 scoped 属性的<style>
标签。 - 插入样式:
将微应用的样式插入到带有 scoped 属性的<style>
标签中。 - 插入内容:
将微应用的内容插入到根元素中。
Scoped CSS示例代码
<!-- HTML 结构 -->
<div id="micro-app-root">
<style scoped>
.app-header {
background-color: blue;
color: white;
}
</style>
<div class="app-header">Hello, Qiankun!</div>
</div>
scoped CSS
原理
scoped CSS
是一种在组件中实现样式隔离的方法,其原理主要是通过为组件内的元素添加特定的属性选择器,确保样式仅作用于当前组件的元素,而不影响其他组件或全局样式。
以下是 scoped CSS
实现样式隔离的主要原理:
-
生成唯一属性:当在 Vue 单文件组件(.vue 文件)中使用
<style scoped>
标签时,Vue 会为组件内的每个元素自动生成一个唯一的属性(例如data-v-f3f3eg9
),这个属性是动态生成的,确保每个组件的属性都是唯一的。 -
修改 CSS 选择器:Vue 会遍历所有的 CSS 选择器,并为它们添加上这个唯一属性作为伪类(例如
div[data-v-f3f3eg9]
)。这样,原本普通的 CSS 选择器就被转换成了属性选择器,确保样式仅作用于当前组件的元素。 -
应用样式:在编译完成后,生成的 CSS 代码会包含这些属性选择器,浏览器在应用样式时会根据这些属性来选择元素,从而实现样式隔离。
以下是一个简单的例子来说明 scoped CSS
的工作原理:
<template>
<div class="scoped-style">
This is a component with scoped CSS.
</div>
</template>
<script>
export default {
name: 'ScopedStyleComponent'
}
</script>
<style scoped>
.scoped-style {
color: red;
}
</style>
在这个例子中,Vue 会为 <div class="scoped-style">
元素生成一个唯一的属性(例如 data-v-f3f3eg9
),并将 CSS 选择器 .scoped-style
转换为 div[data-v-f3f3eg9].scoped-style
。这样,即使其他组件或全局样式中也使用了 .scoped-style
类名,它们也不会影响到这个组件内的元素。
需要注意的是,scoped CSS
并不是完美的解决方案,因为它可能会导致一些副作用,例如:
- 深层嵌套元素:如果组件内部有深层嵌套的元素,那么这些元素的样式可能会受到父组件样式的影响。
- 动态生成的内容:如果组件内部有动态生成的内容(例如通过 JavaScript 插入的元素),这些元素的样式可能不会被正确应用。
- 伪类和媒体查询:
scoped CSS
可能会导致一些复杂的 CSS 规则(如伪类和媒体查询)无法正常工作。
尽管如此,scoped CSS
仍然是实现组件样式隔离的一种有效方法,特别是在使用 Vue.js 等现代前端框架时。
Shadow DOM 和 scoped CS
通过使用 Shadow DOM 和 scoped CSS,乾坤能够有效地隔离微应用的样式,防止样式冲突。这两种方法各有优缺点:
Shadow DOM:
优点:完全隔离,不会受到外部样式的影响。
缺点:浏览器兼容性稍差,某些旧浏览器不支持。
Scoped CSS:
优点:兼容性好,大多数现代浏览器都支持。
缺点:样式隔离不如 Shadow DOM 完全,可能会受到一些外部样式的影响。
根据具体需求和项目环境,可以选择适合的样式隔离方式。
微前端提升子应用加载速度
微前端架构通过将前端应用拆分为多个独立的子应用,每个子应用可以独立开发、部署和运行,从而提升了开发效率和团队协作。这种架构模式对子应用的加载速度有显著提升作用,主要得益于以下几个方面:
微前端提升子应用加载速度的原理
- 依赖共享:微前端允许公共依赖只加载一次,而不是每个子应用都单独加载。这样可以减少重复加载,节省带宽和资源,从而提升加载速度。
- 按需加载:微前端可以实现按需加载,即只有当用户需要访问某个子应用时,才加载该子应用的相关资源。这避免了不必要的资源加载,提高了加载效率。
- 代码分割和懒加载:通过代码分割和懒加载技术,可以将子应用的代码分割成多个小块,并在需要时才加载这些代码块。这样可以减少初始加载时间,提升用户体验。
判断微前端是否提升了子应用加载速度的方法
- 性能测试:使用工具如Lighthouse、WebPageTest等对微前端架构前后的子应用进行性能测试。通过比较加载时间、首次内容绘制(FCP)、首次有意义绘制(FMP)等指标,可以直观地看到加载速度的提升
- 用户反馈:收集用户反馈,了解用户在实际使用中的体验。如果用户反馈加载速度有明显提升,那么可以认为微前端架构确实提升了子应用的加载速度。
- 监控和分析:通过监控工具如Google Analytics、Sentry等,收集和分析子应用的加载数据。通过对比微前端架构实施前后的数据,可以量化加载速度的提升
通过上述方法,可以有效地判断微前端架构是否提升了子应用的加载速度,并验证其性能优化效果。