微前端最近是个很火的概念,社区其实也有不少成熟的方案去实现。作为一个umi的重度使用者,就必须来体验一下阿里基于single-spa开发的qiankun了,而umi跟qiankun的最佳结合方案,无疑是qiankun为umi量身订造的插件@umijg/plugin-qiankun。那我们就跟着官网的文档开造吧。
Demo实现
创建项目
npx @umijs/create-umi-appnpm installnpm install @umijs/plugin-qiankun --savenpm start
共创建3个项目,一个作为主应用,2个作为子应用
配置主应用
- 注册子应用。在主应用的.umirc.ts中,开启qiankun。这是umi3插件的一种开启方式,只要插件的key(这里的plugin-qiankun的key就是qiankun)有对应的配置就会开启qiankun。
export default {
qiankun: {
master: {
// 注册子应用信息
apps: [
{
name: 'app1', // 唯一 id,需要与微应用中的package.json中的name一致
entry: '//localhost:8001', // html entry
},
{
name: 'app2', // 唯一 id,需要与微应用中的package.json中的name一致
entry: '//localhost:8002', // html entry
}
],
},
},
};
- 装载子应用。在需要加载子应用的路由上,配置microApp属性为子应用的name,即可与路由绑定
export default {
routes: [
{
path: '/',
component: '../layouts/index.js',
routes: [
{
path: '/app1',
microApp: 'app1',
// 配置Loading属性
microAppProps: {
autoSetLoading: true,
className: 'myContainer',
wrapperClassName: 'myWrapper',
}
},
{
// app2
// ...
}
],
},
],
}
配置子应用
- 插件注册。在
package.json中配置"name": "app1"。同时也需要配置.umirc.ts文件开启qiankun。
export default {
qiankun: {
slave: {}
}
}
- 环境变量配置。在
.env中:
PORT=8001
HMR=none
如果遇到了因为dev scripts端口号等问题产生冲突以致于报错,可以先关闭HMR试试
- 如果子应用需要在生命周期中加一些自定义逻辑,则需要在
src/app.ts中导出一个对象qiankun
export const qiankun = {
// 应用加载之前
async bootstrap(props) {
console.log('app1 bootstrap', props);
},
// 应用 render 之前触发
async mount(props) {
console.log('app1 mount', props);
},
// 应用卸载之后触发
async unmount(props) {
console.log('app1 unmount', props);
},
};
现在运行3个项目,就能通过主应用来访问子应用了,例如访问http://localhost:8000/app1显示的是子应用的页面。这里需要注意的是,子应用的端口号是_8001_,那么原来的子应用发出的处于同一域名下的后端请求也是在_8001_端口上,但通过主应用来访问,后端请求就会变为_8000_端口,需要做一个代理转发,可以在.umirc.ts中配置proxy。
这时候应用之间是基本独立的,我们还需要加入供应用间通讯的全局状态
应用间通讯
这里主要使用@umijs/plugin-model的useModel来将数据全局化。umi应该已经内置了@umijs/preset-react插件集,其中就包括了@umijs/plugin-model
主应用生产数据
在src/app.ts中导出一个函数useQiankunStateForSlave,这个函数返回的内容会注入到props中传递给子应用。这里有个坑,useState返回的setXXX方法不能命名为setGlobalState,不然会在mount阶段被qiankun的另一个同名函数覆盖。
// src/app.ts
export function useQiankunStateForSlave() {
const [globalState, setQiankunGlobalState] = useState({ str: 'aaa'})
return {
globalState,
setQiankunGlobalState
};
}
如果主应用也需要用到这个globalState的话,也可以在组件中借助useModel访问@@qiankunStateForSlave:
const { globalState } = useModel('@@qiankunStateForSlave');
子应用消费数据
子应用通过useModel访问@@qiankunStateFromMaster这个model以获取主应用传过来的props
export default () => {
const masterProps = useModel('@@qiankunStateFromMaster');
useEffect(() => {
masterProps.setQiankunGlobalState({ str: 'bbb' })
}, [])
return (
<p>{JSON.stringify(masterProps.globalState)}</p>
);
}
这样,主应用负责管理globalState,子应用也能取到globalState并调用masterProps上的方法来通知主应用修改globalState
Demo 演示
子应用app2将主应用的globalState从aaa改成bbb
一些思考
如何管理应用权限
每个中后台系统,基本都是需要获取用户权限来限制某些页面的访问权。那么在微前端这种使用场景,最好是由主应用来统一管理每个子应用的用户权限等信息。最好还能不改变原来单独访问子应用时的逻辑。
假设token放在localStorage中,这里想到了2个方案:
- 【统一处理】一旦进入任一子应用,主应用取出token发出一个checkLogin请求,后端检查2个子应用的登录态,只要任一没登录就触发login逻辑,如果都登录了就返回2个子应用的userInfo,主应用再分别传递userInfo给子应用。(需要后端加接口支持)
- 【分开处理】一旦进入任一子应用,主应用取出2个token发出2个checkLogin请求,经转发至子应用的域名后,子应用的2个后端各自检查登录态,有则返回userInfo。主应用只要没有拿到2份userInfo,就触发login逻辑,再分别传递userInfo给子应用。
子应用通过当前域名识别出是否位于主应用,是则不走自己的登录校验逻辑。
能支持Umi2吗?
本人也尝试接入umi2版本的项目,即按照对应版本的qiankun插件
npm install @umijs/plugin-qiankun@umi2 --save
也能运行起来
但是这个版本的qiankun插件,子应用并不能使用@@qiankunStateForSlave来获取masterProps,这个版本是使用const masterProps = useRootExports()来获取masterProps。显然这样的话主应用还得额外处理供umi2子应用使用的全局state,感觉不是很方便。