原文:https://zhuanlan.zhihu.com/p/...
两个月前,我们正式发布了 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)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。