Qiankun 生命周期与数据通信实战

18 阅读8分钟

微前端是一种前端架构模式 ,它将一个大型的前端应用拆分成多个独立、可独立部署的子应用 ,然后通过一个主应用(或基座应用)将这些子应用集成在一起。

项目支持作为子应用集成到 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 生命周期,卸载子应用