两个月前,我们正式发布了 qiankun2.0,在经历了 15+ beta 版本及大量的内部打磨之后,今天我们将正式发布基于 qiankun2.0 的全新的 @umijs/plugin-qiankun

本次升级在插件层完全兼容 @umijs/plugin-qiankun 之前的版本,所以只是做了 minor 版本的更新。

新特性

2.3.0 版本在将底层完全迁移至 qiankun2.0 之后,不仅修复了之前 qiankun plugin 的若干问题,同时也带来了一些激动人心的新特性。

配置精简

配置微应用时,不再需要手动配置 base 和 mountElementId。

  1. export default {
  2. qiankun: {
  3. master: {
  4. apps: [
  5. {
  6. name: 'microApp1',
  7. entry: '//test.com/app1',
  8. - base: '/app1',
  9. - mountElementId: 'app1-root'
  10. }
  11. ]
  12. }
  13. }
  14. }

在此前的模式下,我们需要在主应用中给每个微应用提前准备一个可挂载的节点 mountElementId,以及一个双方提前约定好的路由 /base 才能完成一次微应用接入。

但这种方式会碰到一些麻烦的问题:

容器 加载/卸载 时序问题

比如我们的主应用的渲染可能是 异步/时序不确定 的,那么我们必须保证微应用在渲染前,其预备的 mountElementId 容器是已经就绪的状态,否则就会出现子应用 mount 时抛出 Target is not container 之类的异常。此前我们为解决这类问题提供了一个 defer: boolean 的配置,通过开启此配置 + 手动调用 qiankunStart() 的方式完成 qiankun 框架的懒初始化。但这个方式并没有从根本上解决问题,在更复杂的场景(比如每一个微应用的挂载点都可能是异步渲染出来的)下,这个方案还是会有问题。

同样的,在微应用卸载时,也可能由于主应用中别的逻辑的影响(如路由切换),导致 mountElementId 容器被其他应用逻辑给提前移除了,最终导致微应用卸载时也会抛出 Target container is not a dom element 类似的异常。

base 配置的问题

此前我们的主应用想正确渲染出一个微应用,需要两边保持路由 base 上的一致。比如主应用这边在注册微应用时配置的是:

  1. {
  2. name: 'microApp1',
  3. entry: '//test.com/app1',
  4. + base: '/app1'
  5. }

那么 microApp1 这个应用也必须使用同样的 base 配置,如:

  1. // config.js
  2. {
  3. + base: '/app1',
  4. plugins: [...],
  5. }

否则可能会出现 base 配置不一致导致 url 无法被微应用识别,从而无法正常加载微应用的问题。

同时在一些更复杂的场景,比如我希望在 [/users/:userId, /members/:mid] 这样一组动态的 url 路径下加载某一个微应用,处理起来就会非常麻烦,甚至可能无法支持。

而全新的微应用接入方式,会完美的解决这样一些问题。

全新的微应用接入方式

👆上面提到的配置只是声明了一组微应用,何时绑定渲染微应用还需要进一步配置。

新的插件提供两种微应用绑定方式:

A. 路由绑定式

假设我们有这样一组路由:

  1. export default {
  2. routes: [
  3. { path: '/login', component: 'login'},
  4. {
  5. path: '/',
  6. component: '@/layouts/index',
  7. routes: [
  8. { path: '/list', component: 'list' },
  9. { path: '/admin', component: 'admin' },
  10. ],
  11. },
  12. ]
  13. }

假设我们希望在 /users/admin/:operation 这样两个 url 下分别加载微应用 app1 和 微应用 app2,那么我们需要做的是在路由配置里加这样几行代码:

  1. export default {
  2. routes: [
  3. {
  4. path: '/',
  5. component: '@/layouts/index',
  6. routes: [
  7. { path: '/list', component: 'list' },
  8. {
  9. path: '/admin',
  10. component: 'admin',
  11. + routes: [
  12. + {
  13. + path: '/admin/:operation', microApp: 'app2',
  14. + }
  15. + ]
  16. },
  17. ],
  18. },
  19. + { path: '/users', microApp: 'app1'},
  20. ]
  21. }

这样在 react-router 匹配到 /users/admin/:operation 规则的 url 时,就会自动渲染其关联的微应用了。

在路由绑定的模式下,qiankun plugin 会自动给匹配的微应用注入 base 信息,微应用在读到 base 信息后会在运行时自动更新路由设置(需要微应用也使用最新版本插件)。

B. MicroApp 组件式

在一些更复杂的场景,我们可能希望自己能控制微应用的渲染,这个时候可用直接使用我们提供 React 组件的方式,如:

  1. import { MicroApp } from 'umi';
  2. function MyPage(props) {
  3. const { loading } = props;
  4. if (loading) {
  5. return <Spin />;
  6. }
  7. return (
  8. <div>
  9. <MicroApp name="microApp1"/>
  10. </div>
  11. )
  12. }

这样在 loading 为 false 时,MyPage 组件就会渲染出我们之前声明的 microApp1 了。

全新的应用通信模式

2.3.0 版本之前,主应用与微应用之间的通信方式有两种:基于 props基于 Hooks 的方式。但这两种方式都存在一个问题就是,不够开箱即用,比如我想实现主应用更新下发的 props 后,微应用使用了 props 的组件自动触发 rerender 这个能力,两个方式实现起来都会比较别扭。

在 umi@3 的加持下,我们基于 model 插件,提供了一个更友好、更强大的应用间通信的机制。

主应用数据下发

不同的微应用使用模式,通信的方式不太一样。

MicroApp 组件式

如果你用的 MicroApp 组件模式消费微应用,那么数据传递的方式就跟普通的 react 组件通信是一样的,直接通过 props 传递即可:

  1. function MyPage() {
  2. const [name, setName] = useState(null);
  3. return <MicroApp name={name} onNameChange={newName => setName(newName)} />
  4. }

路由绑定式

如果你用的 路由绑定式 消费微应用,那么你需要在 src/app.ts 里导出一个 qiankunGlobalState 函数,函数的返回值将作为 props 传递给微应用,如:

  1. // src/app.ts
  2. export function useQiankunStateForSlave() {
  3. const [globalState, setGlobalState] = useState({});
  4. return {
  5. globalState,
  6. setGlobalState,
  7. }
  8. }

主应用需要变更 globalState 并自动触发子应用更新时,只需要:

  1. import { useModel } from 'umi';
  2. function MyPage() {
  3. const { setGlobalState } = useModel('@@qiankunStateForSlave');
  4. return <button onClick={() => setGlobalState({})}>修改主应用全局状态</button>
  5. }

注意,由于更新的是全局 state,所以变更后可能会导致当前挂载的所有微应用都触发更新。如果需要精确更新某一个微应用,请使用 MicroApp 组件模式。

微应用消费数据

微应用中直接通过 useModel('@@qiankunStateFromMaster') 即可获取到主应用下发的状态数据。

  1. import { useModel } from 'umi';
  2. function MyPage() {
  3. const masterState = useModel('@@qiankunStateFromMaster');
  4. return <div>{ masterState.userName }</div>
  5. }

升级指南

v2.3.0 完全兼容 v2 之前的版本,但我们还是建议您能升级到最新版本已获得更好的开发体验。

  1. 移除无必要的应用配置```diff export default { qiankun: { master: { apps: [
    1. {
    2. name: 'microApp',
    3. entry: '//umi.dev.cnd/entry.html',
  • base: ‘/microApp’,
  • mountElementId: ‘microApp’,
  • history: ‘browser’, } ] } } } ```
  1. 移除无必要的全局配置```diff export default { qiankun: { master: { apps: [],
  • defer: true, } } } ```
  1. 关联微应用
    比如我们之前配置了微应用名为 microApp 的 base 为 /microApp ,mountElementId 为 subapp-container, 那么我们只需要:
    a. 增加 /microApp 的路由```jsx export default { routes: [ …, { path: ‘/microApp’, microApp: ‘microApp’ } ] }

    1. <br />b. 在 `/microApp` 路由对应的组件里使用 `MicroApp````jsx
    2. export default {
    3. routes: [
    4. ...,
    5. { path: '/microApp', component: 'MyPage' }
    6. ]
    7. }
    1. import { MicroApp } from 'umi';
    2. export default MyPage() {
    3. return (
    4. <div>
    5. <MicroApp name="microApp" />
    6. </div>
    7. )
    8. }
  2. 移除一些无效配置,如 手动添加子应用路由配置

Roadmap

  • 动态 history type 支持(即将到来 🎉)
    通过运行时设置微应用 props 的方式,修改微应用 history 相关配置,从而解耦微应用配置,如:```tsx // HistoryOptions 配置见 https://github.com/ReactTraining/history/blob/master/docs/api-reference.md type HistoryProp = { type: ‘browser’ | ‘memory’ | ‘hash’ } & HistoryOptions;

```

  • 运行时统一,针对多层嵌套微应用场景
  • 微应用自动 mountElementId,避免多个 umi 子应用 mountElementId 冲突
  • 自动 loading
  • 本地集成开发支持

最后感谢 2.3.0 版本开发中参与贡献的同学们 @天一(troy.lty) @尽龙(brickspert.fjl) @早弦(tianyi.mty) @宜鑫(chaolin.jcl)