两个月前,我们正式发布了 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 上的一致。比如主应用这边在注册微应用时配置的是:
{ name: 'microApp1', entry: '//test.com/app1', + base: '/app1' }
那么 microApp1
这个应用也必须使用同样的 base 配置,如:
// config.js { + base: '/app1', plugins: [...], }
否则可能会出现 base 配置不一致导致 url 无法被微应用识别,从而无法正常加载微应用的问题。
同时在一些更复杂的场景,比如我希望在 [/users/:userId, /members/:mid]
这样一组动态的 url 路径下加载某一个微应用,处理起来就会非常麻烦,甚至可能无法支持。
而全新的微应用接入方式,会完美的解决这样一些问题。
👆上面提到的配置只是声明了一组微应用,何时绑定渲染微应用还需要进一步配置。
新的插件提供两种微应用绑定方式:
假设我们有这样一组路由:
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
信息后会在运行时自动更新路由设置(需要微应用也使用最新版本插件)。
在一些更复杂的场景,我们可能希望自己能控制微应用的渲染,这个时候可用直接使用我们提供 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 组件模式消费微应用,那么数据传递的方式就跟普通的 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> ) }
[ ] 动态 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' }} />
最后感谢 2.3.0 版本开发中参与贡献的同学们 @天一(troy.lty) @尽龙(brickspert.fjl) @早弦(tianyi.mty) @宜鑫(chaolin.jcl)