乐趣区

发布-umijspluginqiankun-230-全面拥抱-qiankun2

两个月前,我们正式发布了 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.ts
export 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.md
    type 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)

退出移动版