微前端进阶(二)
子应用开发规范、SSO 统一认证、RBAC 权限模型、公共依赖共享与通信规范
目录
- 子应用开发规范
- 子应用脚手架与模板
- 统一登录认证(SSO)
- [RBAC 权限模型跨子应用同步](#RBAC 权限模型跨子应用同步)
- 公共依赖与组件库共享
- 子应用通信规范
- 数据持久化与跨应用存储
- [子应用接入 Checklist](#子应用接入 Checklist)
一、子应用开发规范
1.1 规范总览
┌─────────────────────────────────────────────────────────┐
│ 子应用开发规范体系 │
├─────────────────────────────────────────────────────────┤
│ 1. 项目结构规范 │
│ - 统一的目录结构约定 │
│ - 命名规范(文件、组件、变量) │
│ 2. 生命周期规范 │
│ - 必须暴露 bootstrap / mount / unmount │
│ - unmount 必须彻底清理 │
│ 3. 路由规范 │
│ - 统一使用 hash 模式 │
│ - 路由前缀与基座约定一致 │
│ 4. 构建规范 │
│ - UMD 输出格式 │
│ - CORS 头配置 │
│ - externals 声明公共依赖 │
│ 5. 通信规范 │
│ - 使用基座提供的全局状态 API │
│ - 自定义事件必须加命名空间前缀 │
│ 6. 样式规范 │
│ - 使用 CSS Module / Scoped CSS │
│ - 不使用全局样式覆盖 │
│ 7. 资源规范 │
│ - 静态资源使用相对路径 │
│ - 动态 publicPath 适配 │
│ 8. 安全规范 │
│ - 不操作基座 DOM │
│ - 不污染全局变量 │
│ - 不修改基座路由 │
└─────────────────────────────────────────────────────────┘
1.2 项目结构规范
sub-app-{name}/
├── src/
│ ├── main.ts # 入口:暴露生命周期
│ ├── public-path.ts # 运行时 publicPath
│ ├── App.vue # 根组件
│ ├── router/
│ │ └── index.ts # 路由(hash 模式 + 前缀)
│ ├── stores/
│ │ └── index.ts # 状态管理
│ ├── pages/ # 页面组件
│ ├── components/ # 业务组件
│ ├── composables/ # 组合式函数
│ ├── services/ # API 服务
│ ├── utils/ # 工具函数
│ └── types/ # 类型定义
├── public/
├── package.json
├── tsconfig.json
├── vite.config.ts # UMD 输出 + CORS 头
└── .eslintrc.cjs
1.3 生命周期规范
typescript
// sub-app/src/main.ts
import './public-path';
import { createApp, App as VueApp } from 'vue';
import { createRouter, createWebHashHistory } from 'vue-router';
import { createPinia } from 'pinia';
import App from './App.vue';
import { routes } from './router';
import { setupMicroApp } from './micro-setup';
let app: VueApp | null = null;
let router: ReturnType<typeof createRouter> | null = null;
let pinia: ReturnType<typeof createPinia> | null = null;
// 独立运行时的渲染
function render(props: any = {}) {
const { container } = props;
app = createApp(App);
router = createRouter({
history: createWebHashHistory(
window.__POWERED_BY_QIANKUN__ ? '/vue/' : '/'
),
routes
});
pinia = createPinia();
app.use(router);
app.use(pinia);
// 接入基座提供的全局能力
setupMicroApp(app, props);
const rootEl = container
? container.querySelector('#app')
: document.getElementById('app');
app.mount(rootEl);
}
// 独立运行检测
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
// qiankun 生命周期
export async function bootstrap() {
console.log('[SubApp-Vue] bootstrap');
}
export async function mount(props: any) {
console.log('[SubApp-Vue] mount', props);
render(props);
}
export async function unmount() {
console.log('[SubApp-Vue] unmount');
// 彻底清理:销毁 Vue 实例
app?.unmount();
app = null;
router = null;
pinia = null;
// 清理子应用创建的 DOM 容器
const container = document.getElementById('app');
if (container) container.innerHTML = '';
}
1.4 公共路径适配
typescript
// sub-app/src/public-path.ts
// eslint-disable-next-line no-undef
if ((window as any).__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = (window as any).__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
typescript
// Vite 子应用的 public-path 适配
// sub-app/vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
base: process.env.NODE_ENV === 'production'
? '/sub-app/vue/'
: '/',
build: {
// 输出 UMD 格式给 qiankun 加载
lib: {
entry: 'src/main.ts',
name: 'SubAppVue',
formats: ['umd']
},
rollupOptions: {
external: ['vue', 'vue-router', 'pinia'],
output: {
globals: {
vue: 'Vue',
'vue-router': 'VueRouter',
pinia: 'Pinia'
}
}
}
},
server: {
port: 3002,
headers: {
'Access-Control-Allow-Origin': '*'
},
origin: 'http://localhost:3002'
}
});
1.5 路由规范
typescript
// sub-app/src/router/index.ts
import { createRouter, createWebHashHistory } from 'vue-router';
// 获取路由前缀(由基座传递)
function getBaseRoute(): string {
if ((window as any).__POWERED_BY_QIANKUN__) {
return (window as any).__MICRO_APP_BASE_ROUTE__ || '/';
}
return '/';
}
export const routes = [
{
path: '/',
redirect: '/dashboard'
},
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/pages/Dashboard.vue'),
meta: { title: '概览' }
},
{
path: '/list',
name: 'List',
component: () => import('@/pages/List.vue'),
meta: { title: '列表页' }
},
{
path: '/detail/:id',
name: 'Detail',
component: () => import('@/pages/Detail.vue'),
meta: { title: '详情页' }
}
];
const router = createRouter({
history: createWebHashHistory(getBaseRoute()),
routes
});
export default router;
二、子应用脚手架与模板
2.1 脚手架工具
typescript
// scripts/create-micro-app.ts
// 用于快速创建符合规范的新子应用
import { execSync } from 'child_process';
import { copyFileSync, mkdirSync, writeFileSync, existsSync } from 'fs';
import { join, resolve } from 'path';
interface AppConfig {
name: string; // 子应用名称,如 app-report
framework: 'vue' | 'react' | 'angular';
port: number;
baseRoute: string; // 路由前缀,如 /report
}
function createMicroApp(config: AppConfig) {
const { name, framework, port, baseRoute } = config;
const targetDir = resolve(process.cwd(), `sub-apps/${name}`);
if (existsSync(targetDir)) {
console.error(`子应用 ${name} 已存在!`);
process.exit(1);
}
console.log(`创建子应用: ${name}`);
console.log(`框架: ${framework}, 端口: ${port}, 路由: ${baseRoute}`);
// 1. 克隆模板
execSync(`git clone git@company.com:micro-app-templates/${framework}.git ${targetDir}`);
// 2. 替换模板变量
const pkgPath = join(targetDir, 'package.json');
const pkg = require(pkgPath);
pkg.name = name;
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
// 3. 生成子应用配置
const configContent = `
export default {
name: '${name}',
framework: '${framework}',
port: ${port},
baseRoute: '${baseRoute}',
entry: ${framework === 'vue' ? '' : ''}
};
`;
writeFileSync(join(targetDir, 'micro-app.config.ts'), configContent);
// 4. 替换占位路由前缀
const routerPath = join(targetDir, 'src/router/index.ts');
// 自动替换逻辑...
// 5. 安装依赖
execSync(`cd ${targetDir} && pnpm install`);
console.log(`子应用 ${name} 创建成功!`);
console.log(`启动: cd ${targetDir} && pnpm dev`);
}
2.2 模板示例
json
// sub-app-templates/vue/package.json
{
"name": "__SUB_APP_NAME__",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "vite --port __PORT__",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview --port __PORT__",
"lint": "eslint src --ext .ts,.vue",
"typecheck": "vue-tsc --noEmit"
},
"dependencies": {
"vue": "^3.4.0",
"vue-router": "^4.2.0",
"pinia": "^2.1.0",
"element-plus": "^2.5.0",
"@company/ui": "^1.0.0",
"@company/utils": "^1.0.0"
},
"devDependencies": {
"vite": "^5.0.0",
"vue-tsc": "^2.0.0",
"typescript": "^5.3.0",
"eslint": "^8.0.0",
"sass": "^1.70.0"
}
}
yaml
# sub-app-templates/vue/.gitlab-ci.yml
stages:
- lint
- test
- build
- deploy
variables:
APP_NAME: __SUB_APP_NAME__
lint:
stage: lint
script:
- pnpm lint
- pnpm typecheck
test:
stage: test
script:
- pnpm test:unit
build:
stage: build
script:
- pnpm build
artifacts:
paths:
- dist/
deploy:
stage: deploy
script:
- cp -r dist/ /data/www/micro/$APP_NAME/
only:
- main
三、统一登录认证(SSO)
3.1 认证流程设计
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ 浏览器 │ │ 基座应用 │ │ SSO 服务 │ │ 子应用 │
└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │ │
│ 访问 /dashboard │ │ │
│─────────────────>│ │ │
│ │ 检测无 token │ │
│ │────────────────>│ │
│ │ 返回 SSO 登录页 │ │
│<─────────────────│ │ │
│ │ │ │
│ 用户输入凭证 │ │ │
│─────────────────>│ │ │
│ │ 验证凭证 │ │
│ │────────────────>│ │
│ │ 返回 code │ │
│<─────────────────│ │ │
│ │ │ │
│ 回调携带 code │ │ │
│─────────────────>│ │ │
│ │ code 换 token │ │
│ │────────────────>│ │
│ │ 返回 token │ │
│ │<────────────────│ │
│ │ │ │
│ 存储 token, │ │ │
│ 跳转目标页 │ │ │
│<─────────────────│ │ │
│ │ │ │
│ 切换子应用 │ │ │
│──────────────────────────────────────────────────────>│
│ │ │ │
│ │ 注入 token 到 │ │
│ │ 子应用 props │ │
│ │──────────────────────────────────>│
│ │ │ │
│ │ 子应用使用 token │ │
│ │ 请求 API │ │
│ │ │ │
3.2 基座认证服务
typescript
// src/services/auth.ts
import { request } from '@/utils/request';
import { storage } from '@/utils/storage';
interface LoginParams {
username: string;
password: string;
captcha?: string;
}
interface LoginResult {
accessToken: string;
refreshToken: string;
expiresIn: number;
tokenType: string;
}
interface UserInfo {
id: string;
name: string;
email: string;
avatar?: string;
roles: string[];
permissions: string[];
}
export const authService = {
// 登录
async login(params: LoginParams): Promise<LoginResult> {
const result = await request.post<LoginResult>('/auth/login', params);
storage.setToken(result.accessToken);
storage.setRefreshToken(result.refreshToken);
return result;
},
// SSO 登录(通过 code)
async ssoLogin(code: string): Promise<LoginResult> {
const result = await request.post<LoginResult>('/auth/sso', { code });
storage.setToken(result.accessToken);
storage.setRefreshToken(result.refreshToken);
return result;
},
// 刷新 Token
async refreshToken(): Promise<LoginResult> {
const refreshToken = storage.getRefreshToken();
if (!refreshToken) throw new Error('No refresh token');
const result = await request.post<LoginResult>('/auth/refresh', {
refreshToken
});
storage.setToken(result.accessToken);
storage.setRefreshToken(result.refreshToken);
return result;
},
// 获取用户信息
async fetchUserInfo(): Promise<UserInfo> {
return request.get<UserInfo>('/auth/user-info');
},
// 登出
async logout(): Promise<void> {
try {
await request.post('/auth/logout');
} finally {
storage.clearToken();
storage.clearRefreshToken();
}
},
// Token 刷新拦截器(配合 Axios)
setupTokenRefreshInterceptor() {
let isRefreshing = false;
let pendingRequests: Array<{
resolve: (token: string) => void;
reject: (err: any) => void;
}> = [];
return {
onResponseError: async (error: any) => {
const originalRequest = error.config;
if (error.response?.status !== 401 || originalRequest._retry) {
return Promise.reject(error);
}
if (isRefreshing) {
return new Promise((resolve, reject) => {
pendingRequests.push({ resolve, reject });
}).then((token) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
return request(originalRequest);
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
const { accessToken } = await authService.refreshToken();
pendingRequests.forEach(({ resolve }) => resolve(accessToken));
pendingRequests = [];
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return request(originalRequest);
} catch (refreshError) {
pendingRequests.forEach(({ reject }) => reject(refreshError));
pendingRequests = [];
storage.clearToken();
window.location.href = '/login';
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
};
}
};
3.3 向子应用注入认证信息
typescript
// src/micro/auth-injector.ts
import { useUserStore } from '@/stores/user';
import type { AppGlobalState, MicroProps } from '@/types/micro';
// 在子应用加载时注入认证信息
export function injectAuthToProps(appName: string): Partial<MicroProps> {
const userStore = useUserStore();
return {
// 当前 Token
token: userStore.token,
// 用户信息
user: {
id: userStore.userInfo?.id,
name: userStore.userInfo?.name,
roles: userStore.userInfo?.roles,
permissions: userStore.userInfo?.permissions
},
// 认证 API
auth: {
// 子应用可以调用刷新 Token
refreshToken: async () => {
const { accessToken } = await authService.refreshToken();
return accessToken;
},
// 子应用可以获取最新 Token
getToken: () => userStore.token,
// 监听 Token 变化
onTokenChange: (callback: (token: string) => void) => {
return userStore.$subscribe((mutation, state) => {
callback(state.token);
});
}
},
// 登出回调
onLogout: async () => {
await authService.logout();
window.location.href = '/login';
}
};
}
// 子应用端的使用示例
// 在子应用的 mount 生命周期中:
// export async function mount(props) {
// const { token, user, auth } = props;
// // 设置 axios 的 Authorization header
// axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
// // 存储用户信息
// userStore.setUserInfo(user);
// }
3.4 Token 刷新机制
typescript
// src/micro/token-sync.ts
import { useUserStore } from '@/stores/user';
// Token 即将过期时主动通知子应用
export function setupTokenSync() {
const userStore = useUserStore();
// 监听 Token 刷新
let refreshTimer: ReturnType<typeof setInterval> | null = null;
function startTokenRefreshCheck() {
refreshTimer = setInterval(() => {
const expiresAt = userStore.tokenExpiresAt;
if (!expiresAt) return;
// 提前 5 分钟检查
const timeLeft = expiresAt - Date.now();
if (timeLeft > 0 && timeLeft < 5 * 60 * 1000) {
// 静默刷新 Token
authService.refreshToken().then(() => {
// 通知所有子应用更新 Token
window.dispatchEvent(new CustomEvent('micro:token-refreshed', {
detail: { token: userStore.token }
}));
});
}
}, 60 * 1000); // 每分钟检查一次
}
function stopTokenRefreshCheck() {
if (refreshTimer) {
clearInterval(refreshTimer);
refreshTimer = null;
}
}
return { startTokenRefreshCheck, stopTokenRefreshCheck };
}
// 子应用监听 Token 刷新
// window.addEventListener('micro:token-refreshed', (e: CustomEvent) => {
// const { token } = e.detail;
// axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
// });
四、RBAC 权限模型跨子应用同步
4.1 权限模型设计
┌─────────────────────────────────────────────────────────────┐
│ RBAC 权限模型 │
│ │
│ 用户 (User) ◄──── 角色 (Role) ────► 权限 (Permission) │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ user_A │ │ admin │ │ dashboard:view │ │
│ │ user_B │ │ editor │ │ report:export │ │
│ │ user_C │ │ viewer │ │ user:manage │ │
│ └──────────┘ └──────────┘ │ system:config │ │
│ │ micro-app:react │ │
│ │ micro-app:vue │ │
│ └──────────────────┘ │
│ │
│ 权限格式: {resource}:{action} │
│ 示例: report:export │
│ user:manage │
│ micro-app:react │
└─────────────────────────────────────────────────────────────┘
4.2 权限 Store
typescript
// src/stores/permission.ts
import { defineStore } from 'pinia';
import { request } from '@/utils/request';
interface MenuItem {
path: string;
title: string;
icon?: string;
children?: MenuItem[];
microApp?: string; // 如果是微前端子应用
permissions?: string[]; // 所需的权限列表
}
interface PermissionState {
menus: MenuItem[];
permissions: string[];
roles: string[];
loaded: boolean;
}
export const usePermissionStore = defineStore('permission', {
state: (): PermissionState => ({
menus: [],
permissions: [],
roles: [],
loaded: false
}),
getters: {
// 检查是否有某个权限
hasPermission: (state) => {
return (permission: string): boolean => {
if (state.roles.includes('super_admin')) return true;
return state.permissions.includes(permission);
};
},
// 检查是否有多个权限(AND)
hasAllPermissions: (state) => {
return (permissions: string[]): boolean => {
if (state.roles.includes('super_admin')) return true;
return permissions.every(p => state.permissions.includes(p));
};
},
// 检查是否有任一权限(OR)
hasAnyPermission: (state) => {
return (permissions: string[]): boolean => {
if (state.roles.includes('super_admin')) return true;
return permissions.some(p => state.permissions.includes(p));
};
},
// 获取当前用户有权限的菜单
accessibleMenus: (state) => {
function filterMenus(menus: MenuItem[]): MenuItem[] {
return menus.filter(menu => {
if (menu.permissions?.length) {
const hasAccess = menu.permissions.some(
p => state.permissions.includes(p)
|| state.roles.includes('super_admin')
);
if (!hasAccess) return false;
}
if (menu.children) {
menu.children = filterMenus(menu.children);
}
return true;
});
}
return filterMenus(state.menus);
}
},
actions: {
// 获取权限数据
async fetchPermissions() {
try {
const userStore = useUserStore();
const userInfo = userStore.userInfo;
this.roles = userInfo?.roles || [];
this.permissions = userInfo?.permissions || [];
this.loaded = true;
} catch (err) {
console.error('[Permission] 获取权限失败:', err);
this.permissions = [];
this.loaded = true;
}
},
// 获取菜单(根据权限过滤)
async fetchMenus() {
try {
const allMenus = await request.get<MenuItem[]>('/api/menus');
this.menus = allMenus;
} catch (err) {
console.error('[Permission] 获取菜单失败:', err);
this.menus = [];
}
},
// 重置权限状态
reset() {
this.menus = [];
this.permissions = [];
this.roles = [];
this.loaded = false;
}
}
});
4.3 路由权限守卫
typescript
// src/router/guards.ts(增强版)
import type { Router } from 'vue-router';
import { storage } from '@/utils/storage';
import { usePermissionStore } from '@/stores/permission';
import { ElMessage } from 'element-plus';
export function setupRouterGuards(router: Router) {
const WHITE_LIST = ['/login'];
router.beforeEach(async (to, from, next) => {
const title = to.meta?.title;
if (title) {
document.title = `${title} - ${import.meta.env.VITE_APP_TITLE}`;
}
if (to.meta?.noAuth) {
next();
return;
}
const token = storage.getToken();
if (!token) {
ElMessage.warning('请先登录');
next({ path: '/login', query: { redirect: to.fullPath } });
return;
}
if (to.path === '/login') {
next('/');
return;
}
// 权限校验
const permissionStore = usePermissionStore();
// 等待权限加载
if (!permissionStore.loaded) {
await permissionStore.fetchPermissions();
}
// 检查路由所需的权限
const requiredPermissions = to.meta?.permissions as string[] | undefined;
if (requiredPermissions?.length) {
const hasAccess = permissionStore.hasAllPermissions(requiredPermissions);
if (!hasAccess) {
ElMessage.error('无权限访问此页面');
next('/403');
return;
}
}
// 检查微应用访问权限
const microApp = to.meta?.microApp as string | undefined;
if (microApp) {
const microPermission = `micro-app:${microApp}`;
if (!permissionStore.hasPermission(microPermission)) {
ElMessage.error('无权限访问此应用');
next('/403');
return;
}
}
next();
});
}
// 权限指令(组件内使用)
// v-permission="'report:export'"
// v-permission="['report:export', 'report:view']"
// v-permission:and="['report:export', 'report:view']"
// v-permission:or="['report:export', 'report:view']"
import { Directive } from 'vue';
export const vPermission: Directive = {
mounted(el, binding) {
const permissionStore = usePermissionStore();
const { value, arg } = binding;
let hasPermission = false;
const permissions = Array.isArray(value) ? value : [value];
if (arg === 'or') {
hasPermission = permissionStore.hasAnyPermission(permissions);
} else {
hasPermission = permissionStore.hasAllPermissions(permissions);
}
if (!hasPermission) {
el.parentNode?.removeChild(el);
}
}
};
4.4 权限跨子应用同步
typescript
// src/micro/permission-sync.ts
import { usePermissionStore } from '@/stores/permission';
// 向子应用注入权限信息
export function injectPermissionsToProps(appName: string) {
const permissionStore = usePermissionStore();
return {
permissions: permissionStore.permissions,
roles: permissionStore.roles,
// 权限校验 API
permission: {
has: (perm: string) => permissionStore.hasPermission(perm),
hasAll: (perms: string[]) => permissionStore.hasAllPermissions(perms),
hasAny: (perms: string[]) => permissionStore.hasAnyPermission(perms)
}
};
}
// 权限变更时通知子应用
export function setupPermissionSync() {
const permissionStore = usePermissionStore();
permissionStore.$subscribe(() => {
window.dispatchEvent(new CustomEvent('micro:permission-change', {
detail: {
permissions: permissionStore.permissions,
roles: permissionStore.roles
}
}));
});
}
// 子应用端监听权限变化
// window.addEventListener('micro:permission-change', (e: CustomEvent) => {
// const { permissions, roles } = e.detail;
// permissionStore.setPermissions(permissions);
// permissionStore.setRoles(roles);
// });
五、公共依赖与组件库共享
5.1 依赖共享策略
┌─────────────────────────────────────────────────────────────┐
│ 公共依赖共享策略 │
├─────────────────────────────────────────────────────────────┤
│ 方案一:CDN externals(qiankun 推荐) │
│ 基座通过 CDN 加载 Vue/React 等框架 │
│ 子应用通过 externals 排除公共依赖 │
├─────────────────────────────────────────────────────────────┤
│ 方案二:Module Federation 共享 │
│ 子应用将公共依赖声明为 shared │
│ Webpack 自动去重,版本冲突时使用更高版本 │
├─────────────────────────────────────────────────────────────┤
│ 方案三:基座注入依赖 │
│ 基座在子应用加载前,将依赖挂载到 window 上 │
│ 子应用通过 window 获取公共依赖 │
├─────────────────────────────────────────────────────────────┤
│ 方案四:import-map(ESM 方案) │
│ 通过 import-map 映射公共依赖的 CDN 地址 │
│ 子应用使用 ESM import 引入 │
└─────────────────────────────────────────────────────────────┘
5.2 基座注入公共依赖
typescript
// src/micro/dependencies.ts
import { loadScript, loadStylesheet } from '@/utils/helpers';
// 公共依赖清单
export const SHARED_DEPS: Record<string, {
global: string; // 全局变量名
scripts: string[]; // JS 脚本
styles?: string[]; // CSS 样式
version: string;
priority: number; // 加载优先级
}> = {
vue: {
global: 'Vue',
scripts: [
'/cdn/vue@3.4.0/vue.global.prod.js'
],
version: '3.4.0',
priority: 1
},
'vue-router': {
global: 'VueRouter',
scripts: [
'/cdn/vue-router@4.2.0/vue-router.global.prod.js'
],
version: '4.2.0',
priority: 2
},
pinia: {
global: 'Pinia',
scripts: [
'/cdn/pinia@2.1.0/pinia.global.prod.js'
],
version: '2.1.0',
priority: 3
},
react: {
global: 'React',
scripts: [
'/cdn/react@18.2.0/umd/react.production.min.js'
],
version: '18.2.0',
priority: 1
},
'react-dom': {
global: 'ReactDOM',
scripts: [
'/cdn/react-dom@18.2.0/umd/react-dom.production.min.js'
],
version: '18.2.0',
priority: 2
},
'element-plus': {
global: 'ElementPlus',
scripts: [
'/cdn/element-plus@2.5.0/index.full.min.js'
],
styles: [
'/cdn/element-plus@2.5.0/index.css'
],
version: '2.5.0',
priority: 4
},
'ant-design': {
global: 'antd',
scripts: [
'/cdn/antd@5.12.0/antd.min.js'
],
styles: [
'/cdn/antd@5.12.0/reset.css'
],
version: '5.12.0',
priority: 4
}
};
// 按优先级顺序加载公共依赖
export async function loadSharedDependencies(
depNames: string[]
): Promise<void> {
const toLoad = depNames
.map(name => SHARED_DEPS[name])
.filter(Boolean)
.sort((a, b) => a.priority - b.priority);
for (const dep of toLoad) {
// 跳过已加载的依赖
if ((window as any)[dep.global]) continue;
console.log(`[Deps] 加载共享依赖: ${dep.global}@${dep.version}`);
// 加载样式
if (dep.styles) {
for (const style of dep.styles) {
await loadStylesheet(style);
}
}
// 加载脚本
for (const script of dep.scripts) {
await loadScript(script);
}
console.log(`[Deps] 共享依赖加载完成: ${dep.global}`);
}
}
5.3 子应用构建配置
javascript
// 子应用 externals 配置(React)
// webpack.config.js
module.exports = {
externals: {
react: 'React',
'react-dom': 'ReactDOM',
'react-router-dom': 'ReactRouterDOM',
'antd': 'antd',
'@ant-design/icons': 'icons'
},
output: {
library: `${packageName}-[name]`,
libraryTarget: 'umd',
chunkLoadingGlobal: `webpackJsonp_${packageName}`
}
};
javascript
// 子应用 externals 配置(Vite)
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
build: {
rollupOptions: {
external: ['vue', 'vue-router', 'pinia', 'element-plus'],
output: {
globals: {
vue: 'Vue',
'vue-router': 'VueRouter',
pinia: 'Pinia',
'element-plus': 'ElementPlus'
}
}
}
}
});
5.4 共享组件库
typescript
// @company/ui -> 企业级共享组件库
// 共享组件库的模块联邦配置
// webpack.config.js(组件库端)
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'companyUI',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/components/Button',
'./Table': './src/components/Table',
'./Form': './src/components/Form',
'./Modal': './src/components/Modal',
'./Layout': './src/components/Layout',
'./hooks/useAuth': './src/hooks/useAuth',
'./hooks/usePermission': './src/hooks/usePermission'
},
shared: {
vue: { singleton: true },
'element-plus': { singleton: true }
}
})
]
};
typescript
// 子应用中消费共享组件
// 通过 Module Federation 动态加载
import { defineAsyncComponent } from 'vue';
const CompanyButton = defineAsyncComponent(() =>
import('companyUI/Button')
);
const CompanyTable = defineAsyncComponent(() =>
import('companyUI/Table')
);
// 或通过 npm 包引入(不需要 MF 的场景)
// import { Button } from '@company/ui';
六、子应用通信规范
6.1 通信方式总览
┌─────────────────────────────────────────────────────────────┐
│ 微前端通信方式速查表 │
├──────────┬────────────┬──────────┬───────────┬──────────────┤
│ 通信方式 │ 方向 │ 数据量 │ 实时性 │ 推荐场景 │
├──────────┼────────────┼──────────┼───────────┼──────────────┤
│ Props │ 基座→子应用 │ 小 │ 高 │ 初始化配置 │
│ GlobalState│ 双向 │ 中 │ 高 │ 用户/主题/权限 │
│ EventBus │ 任意↔任意 │ 小 │ 高 │ 事件通知 │
│ URL 参数 │ 任意→子应用│ 小 │ 中 │ 导航传参 │
│ Storage │ 任意↔任意 │ 大 │ 低 │ 持久化数据 │
│ 共享模块 │ 任意↔任意 │ 大 │ 高 │ 组件/工具共享 │
└──────────┴────────────┴──────────┴───────────┴──────────────┘
6.2 通信规范约定
typescript
// src/micro/communication-spec.ts
// 通信规范:命名空间 + 数据格式
/**
* 通信事件命名规范:
* micro:{domain}:{action}
*
* domain 取值:
* auth - 认证相关
* theme - 主题相关
* i18n - 国际化
* user - 用户信息
* nav - 导航
* notify - 通知
* app - 应用生命周期
*/
// 事件定义表
export const MICRO_EVENTS = {
// 认证事件
AUTH_TOKEN_REFRESHED: 'micro:auth:token-refreshed',
AUTH_LOGOUT: 'micro:auth:logout',
AUTH_EXPIRED: 'micro:auth:expired',
// 主题事件
THEME_CHANGE: 'micro:theme:change',
THEME_COLOR_CHANGE: 'micro:theme:color-change',
// 国际化事件
LANG_CHANGE: 'micro:i18n:lang-change',
// 用户事件
USER_INFO_UPDATE: 'micro:user:info-update',
USER_PERMISSION_CHANGE: 'micro:user:permission-change',
// 导航事件
NAV_TO: 'micro:nav:to',
NAV_BACK: 'micro:nav:back',
// 通知事件
NOTIFY_SHOW: 'micro:notify:show',
NOTIFY_COUNT_UPDATE: 'micro:notify:count-update',
// 应用生命周期
APP_MOUNTED: 'micro:app:mounted',
APP_UNMOUNTED: 'micro:app:unmounted',
APP_ERROR: 'micro:app:error'
} as const;
// 通用事件数据格式
interface MicroEventPayload<T = any> {
source: string; // 来源应用名
target?: string; // 目标应用(空则广播)
timestamp: number;
data: T;
id: string; // 事件唯一 ID(用于去重)
}
// 创建标准事件
export function createMicroEvent<T>(
type: string,
data: T,
options?: { target?: string }
): CustomEvent<MicroEventPayload<T>> {
return new CustomEvent(type, {
detail: {
source: window.__POWERED_BY_QIANKUN__
? (window as any).__MICRO_APP_NAME__
: 'portal',
target: options?.target,
timestamp: Date.now(),
data,
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
}
});
}
6.3 封装通信 API
typescript
// src/micro/communication.ts
import { createMicroEvent, MICRO_EVENTS } from './communication-spec';
export class MicroCommunication {
private listeners: Map<string, Set<(payload: any) => void>> = new Map();
private eventLog: Array<{ type: string; payload: any; time: number }> = [];
private maxLogSize = 100;
// 发送事件
emit<T = any>(type: string, data: T, target?: string) {
const event = createMicroEvent(type, data, { target });
window.dispatchEvent(event);
this.logEvent(type, data);
}
// 监听事件
on<T = any>(type: string, callback: (data: T, source: string) => void) {
if (!this.listeners.has(type)) {
this.listeners.set(type, new Set());
}
this.listeners.get(type)!.add(callback);
const handler = (e: Event) => {
const customEvent = e as CustomEvent<{
source: string;
data: T;
target?: string;
timestamp: number;
id: string;
}>;
// 检查是否是发给自己的
if (customEvent.detail.target) {
const appName = (window as any).__MICRO_APP_NAME__ || 'portal';
if (customEvent.detail.target !== appName) return;
}
callback(customEvent.detail.data, customEvent.detail.source);
};
window.addEventListener(type, handler);
// 返回取消订阅函数
return () => {
this.listeners.get(type)?.delete(callback);
window.removeEventListener(type, handler);
};
}
// 一次性监听
once<T = any>(type: string): Promise<T> {
return new Promise((resolve) => {
const unsubscribe = this.on<T>(type, (data) => {
unsubscribe?.();
resolve(data);
});
});
}
// 发送给特定子应用
sendTo<T = any>(appName: string, type: string, data: T) {
this.emit(type, data, appName);
}
// 回复发送者
reply<T = any>(originalSource: string, type: string, data: T) {
this.sendTo(originalSource, type, data);
}
// 获取事件日志(调试用)
getEventLog() {
return [...this.eventLog];
}
private logEvent(type: string, data: any) {
this.eventLog.push({ type, payload: data, time: Date.now() });
if (this.eventLog.length > this.maxLogSize) {
this.eventLog.shift();
}
}
// 清理
destroy() {
this.listeners.clear();
this.eventLog = [];
}
}
// 单例导出
export const microComm = new MicroCommunication();
6.4 子应用端通信适配
typescript
// sub-app/src/composables/useMicroCommunication.ts
import { onMounted, onUnmounted } from 'vue';
export function useMicroCommunication() {
const appName = 'app-vue';
function emit(type: string, data: any) {
window.dispatchEvent(new CustomEvent(type, {
detail: {
source: appName,
timestamp: Date.now(),
data
}
}));
}
function on(type: string, callback: (data: any, source: string) => void) {
const handler = (e: Event) => {
const detail = (e as CustomEvent).detail;
callback(detail.data, detail.source);
};
window.addEventListener(type, handler);
return () => window.removeEventListener(type, handler);
}
// 监听基座推送的主题变化
function onThemeChange(callback: (theme: { mode: string; color: string }) => void) {
return on('micro:theme:change', (data) => callback(data));
}
// 监听基座推送的语言变化
function onLangChange(callback: (lang: string) => void) {
return on('micro:i18n:lang-change', (data) => callback(data.lang));
}
// 监听基座推送的权限变化
function onPermissionChange(callback: (perms: string[]) => void) {
return on('micro:user:permission-change', (data) => callback(data.permissions));
}
// 通知基座子应用已就绪
function notifyReady() {
emit('micro:app:mounted', { appName });
}
return {
emit,
on,
onThemeChange,
onLangChange,
onPermissionChange,
notifyReady
};
}
七、数据持久化与跨应用存储
7.1 存储规范
typescript
// src/utils/storage.ts
const STORAGE_PREFIX = 'portal_';
export const storage = {
get<T = any>(key: string): T | null {
try {
const raw = localStorage.getItem(STORAGE_PREFIX + key);
if (raw === null) return null;
return JSON.parse(raw) as T;
} catch {
return null;
}
},
set(key: string, value: any): void {
try {
localStorage.setItem(STORAGE_PREFIX + key, JSON.stringify(value));
} catch (e) {
console.warn('[Storage] 写入失败:', e);
}
},
remove(key: string): void {
localStorage.removeItem(STORAGE_PREFIX + key);
},
clear(): void {
const keys = Object.keys(localStorage).filter(k =>
k.startsWith(STORAGE_PREFIX)
);
keys.forEach(k => localStorage.removeItem(k));
},
// Token 专用
getToken(): string | null {
return this.get<string>('token');
},
setToken(token: string): void {
this.set('token', token);
},
clearToken(): void {
this.remove('token');
},
getRefreshToken(): string | null {
return this.get<string>('refresh_token');
},
setRefreshToken(token: string): void {
this.set('refresh_token', token);
},
clearRefreshToken(): void {
this.remove('refresh_token');
},
// 子应用存储命名空间
getSubAppStorage(appName: string) {
const namespace = `sub_${appName}`;
return {
get: <T = any>(key: string) => this.get<T>(`${namespace}_${key}`),
set: (key: string, value: any) => this.set(`${namespace}_${key}`, value),
remove: (key: string) => this.remove(`${namespace}_${key}`),
clear: () => {
const prefix = STORAGE_PREFIX + namespace;
const keys = Object.keys(localStorage).filter(k => k.startsWith(prefix));
keys.forEach(k => localStorage.removeItem(k));
}
};
}
};
7.2 子应用存储规范约定
typescript
// 子应用存储规范
// 子应用不应直接操作 localStorage,应通过基座提供的 props 中的存储 API
// 基座注入子应用的存储 API
export function injectStorageToProps(appName: string) {
const subStorage = storage.getSubAppStorage(appName);
return {
storage: {
get: subStorage.get,
set: subStorage.set,
remove: subStorage.remove,
clear: subStorage.clear
}
};
}
// 子应用端使用
// export async function mount(props) {
// const { storage } = props;
// const preferences = storage.get('user-preferences');
// // ...
// }
八、子应用接入 Checklist
8.1 接入检查清单
□ 1. 项目结构
□ 目录结构遵循规范
□ 命名规范合规
□ 类型定义完整
□ 2. 生命周期
□ 暴露 bootstrap / mount / unmount
□ unmount 已清理所有资源(定时器、事件、DOM)
□ 独立运行与微前端模式均正常
□ 3. 路由
□ 使用 hash 模式
□ 路由前缀与基座约定一致
□ 路由懒加载
□ 4. 构建
□ UMD 输出格式
□ CORS 头配置(Access-Control-Allow-Origin: *)
□ externals 声明公共依赖
□ publicPath 运行时适配
□ 5. 样式
□ 使用 Scoped CSS / CSS Module
□ 无全局样式污染
□ 主题变量使用基座提供的 CSS 变量
□ 6. 通信
□ 使用基座提供的全局状态 API
□ 自定义事件加 micro: 前缀
□ 通信数据格式遵循规范
□ 7. 权限
□ 页面级权限受控
□ 按钮级权限使用指令控制
□ 接口请求携带正确 Token
□ 8. 性能
□ 路由懒加载
□ 图片懒加载
□ 组件按需加载
□ 无内存泄漏
□ 9. 安全
□ 不操作基座 DOM
□ 不污染全局变量
□ 不修改基座路由
□ 不存储敏感信息到 localStorage
□ 10. 部署
□ Dockerfile 配置正确
□ Nginx 配置正确
□ CI/CD 流水线就绪
□ 健康检查接口可用
8.2 自动化接入检测
typescript
// scripts/validate-micro-app.ts
// 用于 CI/CD 中自动检查子应用是否符合接入规范
interface ValidationResult {
passed: boolean;
checks: Array<{
name: string;
status: 'pass' | 'fail' | 'warn';
message: string;
}>;
}
async function validateMicroApp(appPath: string): Promise<ValidationResult> {
const checks = [];
let allPassed = true;
// 1. 检查生命周期导出
const mainContent = fs.readFileSync(`${appPath}/src/main.ts`, 'utf-8');
checks.push({
name: '生命周期导出',
status: mainContent.includes('export async function mount') ? 'pass' : 'fail',
message: mainContent.includes('export async function mount')
? 'mount 函数已导出'
: '缺少 mount 函数导出'
});
if (!mainContent.includes('export async function mount')) allPassed = false;
// 2. 检查 publicPath
const hasPublicPath = fs.existsSync(`${appPath}/src/public-path.ts`);
checks.push({
name: 'publicPath',
status: hasPublicPath ? 'pass' : 'warn',
message: hasPublicPath
? 'public-path.ts 已配置'
: '未找到 public-path.ts,Vite 项目可能需要特殊处理'
});
// 3. 检查 CORS 配置
const viteConfig = fs.readFileSync(`${appPath}/vite.config.ts`, 'utf-8');
checks.push({
name: 'CORS 头',
status: viteConfig.includes('Access-Control-Allow-Origin') ? 'pass' : 'fail',
message: viteConfig.includes('Access-Control-Allow-Origin')
? 'CORS 已配置'
: '缺少 Access-Control-Allow-Origin 头'
});
if (!viteConfig.includes('Access-Control-Allow-Origin')) allPassed = false;
// 4. 检查 externals
checks.push({
name: '公共依赖 external',
status: viteConfig.includes('external:') ? 'pass' : 'warn',
message: viteConfig.includes('external:')
? '公共依赖已 external'
: '未配置 externals,可能重复加载公共依赖'
});
// 5. 检查 Unmount 清理
checks.push({
name: 'Unmount 清理',
status: mainContent.includes('app?.unmount()') ||
mainContent.includes('app.unmount()') ? 'pass' : 'fail',
message: mainContent.includes('unmount')
? 'unmount 已包含清理逻辑'
: 'unmount 中可能缺少清理逻辑'
});
if (!mainContent.includes('unmount')) allPassed = false;
// 6. 检查路由模式
const routerContent = fs.readFileSync(`${appPath}/src/router/index.ts`, 'utf-8');
checks.push({
name: '路由模式',
status: routerContent.includes('createWebHashHistory') ? 'pass' : 'fail',
message: routerContent.includes('createWebHashHistory')
? '使用 hash 模式'
: '推荐使用 hash 模式'
});
return { passed: allPassed, checks };
}
通过本文,你已经掌握了企业级微前端平台子应用生态的完整规范:
- 子应用开发规范体系
- 脚手架与模板快速创建
- SSO 统一登录认证集成
- RBAC 权限模型跨应用同步
- 公共依赖与组件库共享
- 标准化通信机制
- 数据持久化规范
- 接入检查清单