本篇博客主要介绍在使用微前端架构时最常见的污染问题及处理方法。
微前端架构
在微前端架构中,如何解决多个子应用见的样式污染与全局变量冲突
-
样式污染:
原因是因为在子应用中使用了相同的类名,或者直接修改了全局标签的样式
- 采用
qiankun自带的sandbox配置开启隔离。
- 严格沙箱:利用
shadow Dom将子应用整个包裹,这是最彻底的隔离,因为shadow Dom内部的样式不会溢出道外部。 - 实验性沙箱:
quankun会动态地为子应用的所有css选择器前缀增加一个特殊的属性选择器,
CSS Modules/CSS-in-JS
从开发规范上解决问题,在打包子应用时,利用
webpack的css-loader开启modules模式,将类名混淆为唯一的哈希值-
BEM命名规范+样式前缀
在css预处理器(Sass/Less)中,为每个子应用配置一个唯一的命名空间前缀
- 采用
-
变量隔离
当多个应用都想在window对象挂载localStorage、 vue 实例和全局配置时,冲突就会发生
-
采用qiankun的JS沙箱机制:
qiankun会默认开启JS沙箱,它主要是分为3种
- SnaoshotSandbox(快照沙箱):适用于不支持Proxy的旧浏览器,子应用挂载前记录window状态,卸载时对比并恢复
- LegencySandbox(单实例沙箱):记录快照,但是在操作上更高效
- ProxySandbox(多实例沙箱):最核心、强大的机制。它为每个子应用都创建了一个“伪window”,通过proxy拦截子应用对window的所有操作
- 读取时:先看伪window有没有,没有再去全局真window找
- 写入时,只写入在伪window中
-
避免显示的window挂载
在微前端环境下,尽量使用props传递数据,而不是只写写死在window.xx上。
- 统一封装基于
initGlobalStated的通信机制或者自定义customEvent进行跨应用状态同步
- 统一封装基于
封装GlobalState代码实现:
思路:
- 在基座中:初始化并封装一套“状态分发器”
- 子应用端:封装一个高阶组件或者hook,自动监听与销毁
- 基座端的封装:在基座中定义状态池和修改方法
// main-app/src/utils/actions.js
import { initGlobalState } from 'qiankun';const initialState = {user: { name: 'Alice', role: 'Admin' }, // 初始状态 [cite: 1]theme: 'light',auth: []
};const actions = initGlobalState(initialState);actions.onGlobalStateChange((state, prev) => {// 可以在这里统一处理日志打印或权限校验console.log('[Main App State Change]:', state, prev);
});// 增加自定义的 get 方法(原生不支持,需自行记录最后一次 state)
let currentState = initialState;
actions.onGlobalStateChange((state) => {currentState = state;
});export const getGlobalState = () => currentState;
export default actions;
- 在子应用端封装Hook
// micro-app/src/hooks/useGlobalState.js
import { useState, useEffect } from 'react';export function useGlobalState(props) {const [globalState, setGlobalState] = useState({});useEffect(() => {// 监听全局状态变化 props.onGlobalStateChange((state, prev) => {setGlobalState(state);}, true); // true 表示立即触发一次return () => {// 这里的销毁逻辑由 qiankun 在子应用卸载时统一处理,// 但在组件级别,我们需要确保引用不产生副作用。};}, [props]);const updateState = (newData) => {props.setGlobalState({ ...globalState, ...newData });};return { globalState, updateState };
}
如果用户在多个子应用中频繁的修改同一个状态,导致页面出现卡顿,如何处理?
这里我们需要思考:是否需要按需订阅,也就是说在封装时增加selector函数,只有子应用关心特定字段变化时,才会出发setState,避免变量全局污染;其次需要考虑状态合并问题,防止高频修改阻塞主线程。
优化后的代码:
基座端:增加防抖函数,多次改变合并为一次,减少通信频次
import { initGlobalState } from 'qiankun';
import { debounce } from 'lodash';const initialState = {dataList: [], // 假设这是高频更新的大数据config: {}
};const actions = initGlobalState(initialState);// 原始更新方法
const rawSetState = actions.setGlobalState;// 封装防抖更新:在 16ms(约一帧)内多次调用只会触发一次
actions.setGlobalState = debounce((state) => {console.log('--- 批量更新执行 ---');rawSetState(state);
}, 16);export default actions;
子应用端按需更新Hook
import { useState, useEffect, useRef } from 'react';
import { isEqual } from 'lodash'; // 用于深比较export function useSelectGlobalState(props, selector) {// selector 定义:(state) => state.someFieldconst [selectedState, setSelectedState] = useState(() => selector(window?.currentGlobalState || {}));const lastStateRef = useRef(selectedState);useEffect(() => {const unread = props.onGlobalStateChange((state) => {const nextState = selector(state);// 核心优化:只有当 selector 选中的数据发生变化时,才触发 React 更新if (!isEqual(lastStateRef.current, nextState)) {lastStateRef.current = nextState;setSelectedState(nextState);}}, true);return () => unread();}, [props, selector]);return selectedState;
}
