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

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

新特性

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

配置精简

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

export default {  qiankun: {    master: {      apps: [        {          name: 'microApp1',          entry: '//test.com/app1',-         base: '/app1',-         mountElementId: 'app1-root'        }      ]    }  }}

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

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

容器 加载/卸载 时序问题

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

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

base 配置的问题

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

{  name: 'microApp1',  entry: '//test.com/app1',+ base: '/app1'}

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

// config.js{+ base: '/app1',  plugins: [...],}

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

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

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

全新的微应用接入方式

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

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

A. 路由绑定式

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

export default {  routes: [    { path: '/login', component: 'login'},    {      path: '/',      component: '@/layouts/index',      routes: [        { path: '/list', component: 'list' },        { path: '/admin', component: 'admin' },      ],    },   ]}

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

export default {  routes: [    {      path: '/',      component: '@/layouts/index',      routes: [        { path: '/list', component: 'list' },        {           path: '/admin',          component: 'admin',+         routes: [+           {+             path: '/admin/:operation', microApp: 'app2',+           }+         ]        },      ],    },+   { path: '/users', microApp: 'app1'},  ]}

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

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

B. MicroApp 组件式

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

import { MicroApp } from 'umi';function MyPage(props) {  const { loading } = props;    if (loading) {    return <Spin />;  }    return (    <div>      <MicroApp name="microApp1"/>    </div>  )}

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

全新的应用通信模式

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

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

主应用数据下发

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

MicroApp 组件式

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

function MyPage() {  const [name, setName] = useState(null);  return <MicroApp name={name} onNameChange={newName => setName(newName)} />}
路由绑定式

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

// src/app.tsexport function useQiankunStateForSlave() {  const [globalState, setGlobalState] = useState({});    return {    globalState,    setGlobalState,  }}

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

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

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

微应用消费数据

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

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

升级指南

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

  • 移除无必要的应用配置
export default { qiankun: {   master: {     apps: [       {         name: 'microApp',         entry: '//umi.dev.cnd/entry.html',-         base: '/microApp',-         mountElementId: 'microApp',-         history: 'browser',       }     ]   } }}
  • 移除无必要的全局配置
export default { qiankun: {   master: {     apps: [],-     defer: true,   } }}
  • 移除不必要的挂载容器
-export default MyContainer() {-  return (-    <div>-      <div id="root-subapp"></div>-    </div>-  )-}
  • 关联微应用

    比如我们之前配置了微应用名为 microApp 的 base 为 /microApp ,mountElementId 为 subapp-container, 那么我们只需要:

    a. 增加 /microApp 的路由

    export default {  routes: [    ...,    { path: '/microApp', microApp: 'microApp' }  ]}

    b. 在 /microApp 路由对应的组件里使用 MicroApp

    export default {  routes: [    ...,    { path: '/microApp', component: 'MyPage' }  ]}
    import { MicroApp } from 'umi';export default MyPage() {  return (    <div>          <MicroApp name="microApp" />      </div>  )}
  • 移除一些无效配置,如 手动添加子应用路由配置

Roadmap

  • [ ] 动态 history type 支持(即将到来 ????)

    通过运行时设置微应用 props 的方式,修改微应用 history 相关配置,从而解耦微应用配置,如:

    // HistoryOptions 配置见 https://github.com/ReactTraining/history/blob/master/docs/api-reference.mdtype HistoryProp = { type: 'browser' | 'memory' | 'hash' } & HistoryOptions;<MicroApp history={{ type: 'browser', basename: '/microApp' }} />
  • [ ] 运行时统一,针对多层嵌套微应用场景
  • [ ] 微应用自动 mountElementId,避免多个 umi 子应用 mountElementId 冲突
  • [ ] 自动 loading
  • [ ] 本地集成开发支持

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