微前端是一种前端架构模式 ,它将一个大型的前端应用拆分成多个独立、可独立部署的子应用 ,然后通过一个主应用(或基座应用)将这些子应用集成在一起。
项目支持作为子应用集成到 qiankun 微前端框架中,qiankun 是一个基于 single-spa 的微前端框架,主要特点包括:基于路由的自动激活、样式隔离、JS 沙箱、预加载、全局状态管理。
主应用代码分析
主应用与子应用交互流程如下:
1)注册阶段 :主应用调用 regMicroAllApp(list) 批量注册子应用
2)路由生成 :调用 getMicroAllRouter(microAll) 生成微应用路由
3)启动框架 :调用 loadingMicro() 启动 qiankun 框架
4)路由匹配 :当 URL 匹配到子应用的 activeRule 时
5)子应用挂载 :触发子应用的 mount 生命周期,调用 render(props) 渲染
6)状态同步 :通过 actions 同步主应用与子应用的状态
7)子应用卸载 :当离开子应用路由时,触发 unmount 生命周期
相关逻辑封装到 main\spa\node_modules@pangu\mixmicro\dist\index.esm.js 里
子应用注册机制
微前端路由采用双层路由体系:主应用配置子应用的入口路由和激活规则 ,控制子应用是否加载;子应用配置内部页面的具体路由 ,控制内部页面显示
regMicroAllApp(list) - 批量注册子应用
框架会记录每个子应用的 name 、 entry 、 activeRule 等信息,监听 URL 变化,当 URL 匹配到某个子应用的 activeRule 时,就会准备加载该子应用
-
参数 list:包含所有子应用配置的数组
-
entry:子应用入口,支持自定义或默认路径( origin/pathname/name/index.html )
-
container:子应用挂载容器,固定为 #subapp-viewport
-
activeRule:子应用激活规则,支持多种 URL 格式
-
props:传递给子应用的数据,包括上层产品名、百度地图 API、侧边栏和主应用路由
const regMicroAllApp = (list) => {
if (regMicroAllApp.length === 0)
return;
const microAllApp = [];
list.map((item) => {
microAllApp.push({
name: item.name,
entry: item.entry ? item.entry : window.location.origin + window.location.pathname + item.name + "/index.html",
container: "#subapp-viewport",
activeRule: ["/#/" + item.name, "/index.html#/" + item.name, window.location.pathname + "#/" + item.name, window.location.pathname + "index.html#/" + item.name],
props: {
"upper-name": item["upper-name"],
"BMap": window.BMap,
"Sidebar": window.Sidebar,
"mainRouter": Router
}
});
});
registerMicroApps(microAllApp);
};
getMicroAllRouter(microAll) - 生成微应用路由
为每个子应用生成两个动态路由,使用 Vue Router 的 /:pathMatch(.) 和 /:pathMatch(.)* 匹配所有子应用路由,路由组件统一使用 @pangu/layout/index.vue
const getMicroAllRouter = (microAll) => {
const microAllRouter = [];
microAll.map((item) => {
microAllRouter.push({
name: item.name,
path: "/" + item.name + "/:pathMatch(.*)",
meta: { parameter: true },
component: () => import("@pangu/layout/index.vue")
});
microAllRouter.push({
name: item.name,
path: "/" + item.name + "/:pathMatch(.*)*",
meta: { parameter: true },
component: () => import("@pangu/layout/index.vue")
});
});
return microAllRouter;
};
当 URL 匹配到子应用的 activeRule 时,框架会:
1)加载子应用资源 :通过 entry 地址获取子应用的 HTML、CSS 和 JavaScript
2)创建挂载容器 :在主应用的 #subapp-viewport 容器中为子应用创建 DOM 节点
3)执行子应用代码 :调用子应用的 mount 生命周期函数
4)渲染子应用 :子应用在自己的容器中渲染内容
子应用生命周期管理
-
bootstrap:子应用初始化,目前为空实现
-
mount:子应用挂载,保存 props 到全局 window 对象,设置应用为嵌入模式,配置路由前置守卫,同步路由元信息到全局状态,并调用 render 函数挂载子应用
-
unmount :子应用卸载,检查是否需要清理数据(通过 microNeedClean 标识),若需要清理,调用 setting/exit 退出并卸载实例,否则仅卸载实例
-
update :子应用更新,目前为空实现
function life(render, instance = null) {
const obj = {};
obj.bootstrap = async () => {
};
obj.update = async () => {
};
obj.mount = async (props) => {
window.props = props;
console.log("++++微应用渲染++++");
Store.commit("setting/SET_EMBEDDING", true);
if (Router) {
Router.beforeEach((to, from, next) => {
props.setGlobalState && props.setGlobalState({
meta: to.meta,
cleanData: false
});
next();
});
}
instance = render(props);
};
obj.unmount = async (props) => {
const microNeedClean = window.localStorage.getItem("microNeedClean") === "true";
if (microNeedClean) {
localStorage.removeItem("microNeedClean");
Store.dispatch("setting/exit");
instance.unmount();
} else {
instance.unmount();
}
};
window.qiankunLifecycle = {
bootstrap: obj.bootstrap,
mount: obj.mount,
unmount: obj.unmount,
update: obj.update
};
}
数据通信
props 传递
在注册子应用时,通过 props 传递静态数据:
// 主应用注册时传递
props: {
"upper-name": item["upper-name"], // 上层应用名
"BMap": window.BMap, // 百度地图API
"Sidebar": window.Sidebar, // 侧边栏组件
"mainRouter": Router // 主应用路由实例
}
// 子应用中使用
console.log(window.props["upper-name"]); // 输出: "mainapp"
全局状态管理
通过 actions 对象实现主应用和子应用之间的动态数据共享:
-
初始化全局状态 initialState ,包含元信息、皮肤、语言等
-
actions.onGlobalStateChange:监听全局状态变化,同步到 Vuex 的 micro 模块
-
actions.setGlobalStatePg:封装了全局状态设置方法
const initialState = {
meta: null,
cleanData: false,
skin: "",
lang: "",
mobileCloudData: null,
expandData: null,
thcloud: null
};
const actions = initGlobalState(initialState);
actions.onGlobalStateChange((state, prev) => {
if (state.meta) {
state.meta.parent_id = getParentId();
}
Store.commit("micro/SET_PARAMETER", state);
});
actions.setGlobalStatePg = (val) => {
actions.setGlobalState({ ...initialState, ...val });
};
微前端框架启动
使用 window.qiankunStarted 避免重复启动,调用 qiankun 的 start 方法启动框架,配置沙箱模式: strictStyleIsolation: false (不严格隔离样式)
function loadingMicro() {
if (!window.qiankunStarted) {
window.qiankunStarted = true;
console.log("++++非容器内启动乾坤,加载路由会自动加载微应用");
setTimeout(() => {
start({
sandbox: { strictStyleIsolation: false }
});
}, 0);
}
}
子应用代码分析
代码目录
- .vscode/ # VS Code配置文件
- build/ # 构建脚本
- dev/ # 开发环境配置
- http-server/ # HTTP服务器配置(Go语言实现)
- public/ # 静态资源目录
- src/ # 源代码目录
- app/ # 应用入口和全局配置
- router/ # 路由配置
- access.ts # 路由权限控制
- index.ts # 路由实例
- routes-stack.ts # 路由栈管理
- routes.ts # 路由定义
- store/ # Vuex状态管理
- index.ts # Store实例
- App.vue # 根组件
- index.ts # 应用初始化
- assets/ # 静态资源(图片、图标、样式等)
- common/ # 公共组件、工具和服务
- modules/ # 业务功能模块
- main.ts # 应用入口文件
- app-config.d.ts # 应用配置类型定义
- .browserslistrc # 浏览器兼容性配置
- .cz.yaml # Commitizen配置
- .editorconfig # 编辑器配置
- .env # 环境变量配置
- .eslintignore # ESLint忽略配置
- .eslintrc.js # ESLint配置
- .gitignore # Git忽略配置
- .npmrc # npm配置
- .prettierrc # Prettier配置
- Dockerfile # Docker配置
- README.md # 项目说明文档
- babel.config.js # Babel配置
- config.js # 项目配置
- package.json # 项目依赖和脚本
入口文件 main.ts
作为应用程序的入口文件,负责微前端环境检测、生命周期函数定义和全局配置
环境检测
检测当前是否运行在 qiankun 微前端环境中,如果是微前端环境,动态设置 webpack 的 publicPath,确保资源正确加载
-
POWERED_BY_QIANKUN:qiankun 注入的全局变量,用于标识微前端环境
-
INJECTED_PUBLIC_PATH_BY_QIANKUN:qiankun 注入的资源基础路径,解决子应用资源加载路径问题
if ((window as any).POWERED_BY_QIANKUN) {
webpack_public_path = (window as any).INJECTED_PUBLIC_PATH_BY_QIANKUN;
}
生命周期函数
-
bootstrap:初始化函数,只会在子应用第一次启动时调用一次
-
mount:挂载函数,当子应用被挂载到主应用时调用
-
unmount:卸载函数,当子应用从主应用中卸载时调用,负责清理资源
-
props:主应用传递给子应用的数据和方法
export async function bootstrap() {
//console.log('VueMicroApp bootstraped');
}export async function mount(props: any) {
(window as any).QIANKUN_PROPS = props;
storeTest(props);
render(props);
}export async function unmount() {
instance.destroy(); instance.el.innerHTML = '';
instance = null;
router = null;
}
全局状态变化
通过 onGlobalStateChange 监听主应用传递的全局状态变化,实现主应用与子应用之间的状态同步,如语言切换、主题切换等
-
参数: state 是当前全局状态, prev 是上一次的状态
function storeTest(props: any) {
props.onGlobalStateChange &&
props.onGlobalStateChange((state: any, prev: any) => {
console.log([onGlobalStateChange - ${props.name}]:, state);state?.lang && switchLang(state.lang); state?.skin && setTheme(state.skin); }, true);}
应用路径工具 getAppPath.ts
处理应用程序在不同环境(独立运行/微前端)下的路径逻辑,确保 API 路由和资源定位的正确性
-
微前端环境逻辑:优先从 qiankun props 中获取 upper-name 属性;如果没有,从 URL 路径中提取第一个路径段
-
独立运行环境 :使用环境变量 VUE_APP_PRODUCT 的值
export function getAppPath(): string {
let pathName;
if ((window as any).POWERED_BY_QIANKUN) {
if ('props' in window && (window as any).QIANKUN_PROPS['upper-name']) {
pathName = (window as any).QIANKUN_PROPS['upper-name'];
} else {
pathName = window.location.pathname.split('/')[1];
}
} else {
pathName = process.env.VUE_APP_PRODUCT;
}
global.AppPath_PRODUCT = pathName;
return pathName;
}
应用启动文件 index.ts
实现应用程序的启动和模块加载逻辑,动态导入子模块并集成路由和状态管理
动态导入子模块
从环境变量 VUE_APP_CONFIG 中解析模块配置,使用 webpack 的动态导入功能按需加载模块:
-
如果模块包含路由配置,调用 addRoute 添加到路由系统
-
如果模块包含状态管理,调用 addModule 添加到 Vuex store
async function loadModules() {
const config = process.env.VUE_APP_CONFIG;
if (!config) {
throw Error('No config found');
}
const conf = JSON.parse(JSON.parse(config)) as AppConfig;
const modules = conf.modules;const modulesPromise = modules.map( (name) => new Promise((resolve) => { import( /* webpackInclude: INCLUDED_MODULES, webpackMode: 'lazy-once', webpackChunkName: 'modules', */ '@/modules/' + name + '/module.ts' ).then((result) => { const mod = result.default as AppModule; if (mod.router) { const r = mod.router; addRoute(r.name, r.path, r.routes); } if (mod.store) { const s = mod.store; addModule(s.name, s.module); } resolve(mod); }); }), ); await Promise.all(modulesPromise); return conf;}
路由、状态管理
-
路由守卫 :添加全局路由守卫,实现权限控制和状态同步
-
状态同步 :通过 props.setGlobalState 将子应用状态同步给主应用
-
Vue 实例创建 :根据是否有容器参数决定挂载方式,支持独立运行和微前端挂载
export const start = async (selector: string, props: any) => {
const [, conf] = await Promise.all([loadData(), loadModules()]);
addRouteGuard();// 路由守卫,与主应用通信 if (router) { router.beforeEach((to: any, from: any, next: any) => { if (props.setGlobalState) { props.setGlobalState({ ignore: props.name, user: { name: props.name, }, meta: to.meta, }); } // 权限验证 const token = sessionStorage.getItem('Authorization') || sessionStorage.getItem('ueba-Authorization'); if (!token) { window.location.href = '#/404'; } else { next(); } }); } // 初始化Vue实例 const instance = new Vue({ router, store, i18n, render: (h) => h(App, { props: { appConfig: conf } }), }).$mount(props.container ? props.container.querySelector(selector) : selector); return { instance, router };};
开发环境代理配置 proxy.js
该文件是开发环境的 API 代理配置 ,用于在独立开发子应用时,将 API 请求转发到对应的后端服务
为不同的 API 路径配置不同的代理目标,自动添加认证信息和用户身份头,处理跨域和 SSL 证书问题
const generateProxyConfig = (config) => {
return {
target: config.target, // 代理目标地址
pathRewrite: config.pathRewrite || {}, // 路径重写规则
auth: 'admin:secret+3s', // 代理认证信息
changeOrigin: true, // 启用跨域
ssl: {
rejectUnauthorized: false, // 不验证SSL证书
requestCert: false,
},
onProxyReq: (proxyReq) => {
// 添加请求头,用于模拟用户身份
proxyReq.setHeader('userId', '101');
proxyReq.setHeader('userName', '666666666');
// ...其他请求头
},
onProxyRes: (proxyRes, req, res) => {
// 记录代理响应日志
res.on('finish', () => {
const code = res.statusCode;
// 根据状态码打印不同颜色的日志
// ...日志逻辑
});
},
};
};
const proxyConfigs = {
'/m1/1961322-1395971-default': generateProxyConfig({ target: 'http://127.0.0.1:4523' }),
'/ueba/api': generateProxyConfig({ target: 'http://192.168.77.17:8087' }),
'/model-manager/api': generateProxyConfig({ target: 'http://192.168.77.4:8088' }),
'/csa': generateProxyConfig({ target: 'https://192.168.97.206:8443' }),
};
完整互动流程示例
场景 :用户登录主应用后,点击"数据分析"菜单进入数据子应用,查看图表后返回主应用。
1)注册阶段 :
主应用调用 regMicroAllApp 注册包括 "data-analysis" 在内的所有子应用,同时调用 getMicroAllRouter 生成子应用路由
2)用户访问主应用 :
主应用正常渲染,用户登录成功后,主应用通过 actions.setGlobalStatePg 设置全局用户信息
3)用户点击"数据分析"菜单 :
主应用路由跳转到 /data-analysis,框架检测到 URL 匹配 data-analysis 子应用的 activeRule
4)子应用加载 :
框架通过子应用的 entry 地址加载 HTML、CSS 和 JS,调用子应用的 bootstrap、mount 生命周期,并传递 props,子应用渲染数据分析界面
5)子应用内部操作 :
用户在子应用中切换图表类型,子应用通过 actions.setGlobalState 更新全局状态,主应用通过 onGlobalStateChange 监听到状态变化
6)返回主应用 :
用户点击主应用的"首页"菜单,主应用路由跳转到 /home,框架检测到 URL 不再匹配子应用的 activeRule,调用子应用的 unmount 生命周期,卸载子应用