微前端进阶(二)

微前端进阶(二)

子应用开发规范、SSO 统一认证、RBAC 权限模型、公共依赖共享与通信规范


目录

  1. 子应用开发规范
  2. 子应用脚手架与模板
  3. 统一登录认证(SSO)
  4. [RBAC 权限模型跨子应用同步](#RBAC 权限模型跨子应用同步)
  5. 公共依赖与组件库共享
  6. 子应用通信规范
  7. 数据持久化与跨应用存储
  8. [子应用接入 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 权限模型跨应用同步
  • 公共依赖与组件库共享
  • 标准化通信机制
  • 数据持久化规范
  • 接入检查清单
相关推荐
罗超驿1 小时前
9.零基础学CSS:元素属性设置(字体、颜色、对齐等)全解析
前端·css
云水一下2 小时前
JavaScript 从零基础到精通系列:流程控制、函数与作用域
前端·javascript
柚子科技2 小时前
Vue3 响应式原理:我被 ref 和 reactive 坑了3次后终于搞懂了
前端·javascript·vue.js
大鱼前端2 小时前
Veaury:让Vue和React组件在同一应用中共存的神器
前端·vue.js·react.js
scan7242 小时前
大模型只是知道要调用工具,本身不
前端·javascript·html
云水一下3 小时前
CSS3从零基础到精通(一):前世今生与基础入门
前端·css3
顾凌陵3 小时前
CSRF&SSRF漏洞攻击的溯源分析与实战
前端·csrf
月月大王的3D日记3 小时前
Three.js 材质篇(中):从兰伯特到PBR,一篇文章看懂五种光照材质
前端·javascript