Qiankun-主应用 Angular 子应用 Umi

428 阅读5分钟

环境

qiankun:2.10.10

主应用:angular 7

子应用:umi 3

主应用配置

子应用有两种加载方式,一种是根据页面路由自动加载,一种则为手动加载,自动加载的方式,在切换页面(或后台系统切换 tab )路由变化时会自动卸载掉子应用,不方便后续的状态缓存,所以选择手动加载子应用。

// 在入口文件
this.loadApp = loadMicroApp({
  name: "micro", // 子应用名字
  entry: "//localhost:8000", // 子应用的入口,若是部署上线子应用访问有前缀,入口配置也要加上对应前缀
  container: "#subapp-viewport", // 挂载子应用时的id选择器出口
  props: { brand: "qiankun", actions: {} },
});

start();

// html 容器
<div id="subapp-viewport" ></div>

在主应用 angular 新建一个空白组件,把打开到子应用的所有路由配置该组件

// /portal/list/xx 按照这样的格式命令路由
const routes: Routes = [
  {
    path: "portal",
    data: { pageTitle: "微应用入口", keep: true },
    children: [
      {
        path: 'list/:id',
        component: PortalComponent,
      },
      {
        path: 'detail/:id',
        component: PortalComponent
      },
    ]
  },
];

子应用配置

在配置文件 umi.js 开启子应用配置

export default defineConfig({
  qiankun: {
    slave: {},
  },
});

运行配置文件 app.js 写入生命周期方法

export const qiankun = {
  // 应用加载之前
  async bootstrap(props) {
    console.log('app bootstrap', props);
  },
  // 应用 render 之前触发
  async mount(props) {
    console.log('app mount', props);
    await storeChange(props);
  },
  // 应用卸载之后触发
  async unmount(props) {
    console.log('app unmount', props);
    props.offGlobalStateChange && props.offGlobalStateChange();
  },
};

至此,在打开主应用时,子应用就会加载上并成功执行相应的生命周期方法,因为这里的加载并没有根据路由来,可以根据路由判断手动加载的时机,并在关闭子应用的时候卸载掉,但是这样会有点卡顿。若是只有一个子应用的话,内存占用不是很大,可以按照上面的方法加载,在主应用的销毁方法里进行卸载。

子应用路由配置

若是进入到主应用对应的页面,子应用未加载出来,页面空白且没有其他报错,有可能是子应用没有加载到对应的页面,所以子应用的路由要配置的和主应用的一致。

// 如
主应用打开路由:/main/portal/list/page1
子应用要有相应配置路由:{ path: 'main/portal/list/page1', component: '@/pages/Table' }

路由要一致,子应用页面才会加载出来,动态包含其他变量的路由子路由要加上对应配置,这样才能匹配上。

组件通信

主应用和子应用通信 通过 qiankun 内置方法 initGlobalState 来实现,通过调用 initGlobalState 初始化一个对象,调用该对象的方法 setGlobalState 来进行传值,onGlobalStateChange 方法则进行监听。

import { initGlobalState } from 'qiankun';

// 这里可以写初始化数据
const initialState = {}

// 初始化需要传递的对象
export const actions = (props) => initGlobalState(Object.assign(initialState, props));

// state: 变更后的状态; prev 变更前的状态
actions().onGlobalStateChange((state, prev) => {
  console.log('state', state);
  console.log('prev', prev);
});

在注册子应用的时候把这个对象传到子应用

this.parentState = actions({ _ngTabPageService: this.ngTabPageService });
this.loadApp = loadMicroApp({
  name: "micro", // 子应用名字
  entry: "//localhost:8000", // 子应用的入口
  container: "#subapp-viewport", // 挂载子应用时的id选择器出口
  props: { brand: "qiankun", actions: this.parentState },
});

子应用在注册时就可以获取到该对象,也能进行调用 setGlobalState 和主应用通信(主应用要写相应的监听方法)

image.png

状态缓存

状态缓存多用于后台管理的多 tab 场景,在切换到其他 tab 页面,再切换回来时,子应用操作未完成的页面继续保持。状态缓存有两种方案,一是 umi-plugin-keep-alive,以及 @alitajs/keep-alive,两者的核心内容都是基于 react-activation,只是使用的方式不一样。

umi-plugin-keep-alive 的使用需要在组件页面通过 umi 引用 KeepAlive 组件,并用它来包裹住需要缓存的组件,再添加组件属性 name 来做唯一标识,在关闭的时候通过调用释放缓存方法 dropScope 或者其他方法来释放缓存,具体使用方式可以查看文档。@alitajs/keep-alive 的使用方式个人看来则比较简单,只需要安装好相关依赖,在 umi.js 配置 文件的 plugins 添加上,再开启 keepalive,之后在组件入口,比如 layout 模块进行配置就可以,具体使用查看文档,释放缓存则和 umi-plugin-keep-alive 有所不同,也是通过 umi.js 暴露出的方法 dropByCacheKey 来释放缓存,但是它是通过路由来做唯一标识的,所以 dropByCacheKey 的参数应当是要关闭的页面路由,如果发现关掉页面再次打开缓存还在,可以断点调试看看路由是否一致。

@alitajs/keep-alive 配置方法

// umi.js
import microRoutes from './src/router';

export default defineConfig({
  nodeModulesTransform: {
    type: 'none',
  },
  publicPath: '/micro/',
  base: '/micro/',
  plugins: [
    '@alitajs/keep-alive'
  ],
  qiankun: {
    slave: {},
  },
  routes: [
    {
      path: 'main',
      component: '@/layouts/index',
      routes: [
        ...microRoutes
      ],
    },
  ],
  keepalive: [...microRoutes.map(i => `/main/${i.path}`)],
});
// app.js

// 主应用创建共享状态:
const storeChange = async (props) => {
  // 将主应用传递参数存储
  actions.setActions(props);
  // 微应用通过 props 获取共享状态并监听
  props.onGlobalStateChange &&
    (await props.onGlobalStateChange(async (values) => {
      // 关掉的 tab,解除状态保持
      const { delMicroUrls } = values;
      // 遍历解除关闭页面的缓存
      delMicroUrls.map(i => {
        dropByCacheKey(i)
      })
      // dva 状态管理
      const timer = setTimeout(() => {
        getDvaApp()._store.dispatch({
          type: 'global/updateMainParams',
          payload: values,
        });
        clearTimeout(timer);
      }, 10);
    }, true));
};

export const qiankun = {
  // 应用加载之前
  async bootstrap(props) {
    console.log('app bootstrap', props);
  },
  // 应用 render 之前触发
  async mount(props) {
    console.log('app mount', props);
    await storeChange(props);
  },
  // 应用卸载之后触发
  async unmount(props) {
    console.log('app unmount', props);
    props.offGlobalStateChange && props.offGlobalStateChange();
  },
};
// layout

// import React, { useEffect } from 'react';
import {
  connect,
  KeepAliveLayout,
} from 'umi';
import { Layout, ConfigProvider } from 'antd';
import themeProviderHoc from '@/common/hocs/themeProviderHoc/index';

const { Content } = Layout;

const BasicLayout = (props) => {

  const { children = <></>, ...rest } = props;
  return (
    <>
      <ConfigProvider>
        <KeepAliveLayout  {...props}>
        <Content>{React.cloneElement(children, rest)}</Content>
        </KeepAliveLayout>
      </ConfigProvider>
    </>
  );
};

export default connect((state) => {
  const global = state.global;
  return {
    global,
  };
})(
  themeProviderHoc(BasicLayout, {
    components: {
      Menu: {
        colorPrimary: '#BC261A',
      },
    },
    token: {
      colorPrimary: '#BC261A',
    },
  }),
);

本地代理

子应用在本地开发的时候,代理写在主应用里面这样才能生效,或者子应用的请求路径写成完整的绝对路径,例:127.0.0.1/api

部署

建议部署的主应用和子应用的路由形式保持一致,hash 对hash,history 对 history,这样从主应用进入到子应用页面路由可以保持一致。若是子应用部署前面带有路径 127.0.0.1/micro/,则需要在 umi.js 文件里加上对应的 base 以及 publicPath。