qiankun+umi4+react+antd 微前端构建指南

3,235 阅读4分钟

前言

随时umi4的到来, 我把qiankun学习计划也提上了日程, 在系统的学习一周后, 下面通过umi4+qiankun+react 展开说说我的学习成果吧~

说明

鉴于版面关系, 代码量较多, 只展示关键代码, 建议直接阅读DEMO项目更佳, 请移步至DMEO仓库

后续有个延伸阅读, <<Qiankun避坑指南>>, 本文反响不错的话, 将后续奉上 !!!

项目结构

  • 主应用(基于umi4构建)
  • 微应用(基于umi3构建)
  • 微应用2(基于react-create-app构建)
  • 微应用3(react-create-app + router构建)

项目构建请参考官网提供的示例(很全面), 这里不做展示, 链接放在底部

主要完成以下功能:

挂载

  • 基于qiankun提供的loadMicroApp方法
/* main/src/pages/load.tsx 关键代码 */
let microApp: any = null;
const LoadMicroApp: React.FC = () => {
    const containerRef = useRef(null)
    useEffect(() => {
        if (containerRef.current) {
            microApp = loadMicroApp({
                name: 'sub-app-3',
                entry: '//localhost:5003',
                container: containerRef.current,
                props: {
                    base: '/',
                    message: 'hello sub-app-3, load as loadMicroApp function',
                    callback: (message: string) => callback(message, 3)
                },
            }, {
                // @ts-ignore
                fetch: customFetch
            });
        }
        return () => {
            microApp?.unmount()
            microApp = null
        }
    }, [containerRef.current])

    return (
       <div style={{border: 'solid 1px #cecece'}} ref={containerRef}/>
    );
};
  • 基于umi-plugin-qiankun提供的MicroApp , MicroAppWithMemoHistory 组件
/* main/src/pages/load/index.tsx 关键代码 */
<MicroApp name="sub-app-2" />
/* sub-app-1/src/pages/two/index.tsx 关键代码 */
<MicroAppWithMemoHistory 
    name="sub-app-3"
    base={window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ ? '/sub-app-1' : '/'}/>
  • 基于umi-plugin-qiankun提供的路由声明形式绑定挂载
/* .umirc.ts 关键代码 */
routes: [
...
{
    name: 'react-router-micro-app',
    path: '/sub-app-3',
    microApp: 'sub-app-3',
    icon: 'SmileOutlined',
    routes: [
        {
            name: '嵌套路由1',
            path: '/sub-app-3/one',
        },
        {
            name: '嵌套路由2',
            path: '/sub-app-3/three',
        },
        {
            path: '/sub-app-3',
            redirect: '/sub-app-3/one',
        },
    ]
},
]
  • 微应用嵌套微应用挂载
/* sub-app-1/src/app.ts 关键代码 */
export qiankun = {
    master: {
      // 微应用运行时需要再注册一次, .umirc.ts声明也需要, 否则出错
      apps: [
        {
          name: 'sub-app-2',
          entry: '//localhost:5002',
        },
        {
          name: 'sub-app-3',
          entry: '//localhost:5003',
        },
      ],
      prefetch: false,
},

// 应用加载之前
async bootstrap(props: any) {
  setCreateHistoryOptions({basename: props?.base || '/'})
},
}
/* .umirc.ts 关键代码 */
routes: [
...
{
    name: 'umi3-micro-app',
    path: '/sub-app-1',
    layout: true,
    microApp: 'sub-app-1',
    icon: 'SmileOutlined',
    routes: [
        {
            name: '应用间通信',
            path: '/sub-app-1/one',
        },
        {
            name: '应用间嵌套',
            path: '/sub-app-1/two',
        },
        {
            name: '应用间通信',
            path: '/sub-app-1/sub-app-3',
            routes: [
                {
                    name: '嵌套路由1',
                    path: '/sub-app-1/sub-app-3/one',
                },
                {
                    name: '嵌套路由2',
                    path: '/sub-app-1/sub-app-3/three',
                },
            ]
        }
    ]
},
]

qiankun: {
  master: {
    // 注册子应用信息
    apps: [
      {
        name: 'sub-app-2',
        entry: '//localhost:5002',
      },
      {
        name: 'sub-app-3',
        entry: '//localhost:5003',
      },
    ],
  },
  slave: {},  //微应用必须配置
},

通信

  • 基于qiankun-apps注册时的props属性透传
/* main/src/app.tsx 关键代码 */
export const qiankun: any = {
    apps: [
        {
            name: 'sub-app-1',
            entry: '//localhost:5001',
            activeRule: '/sub-app-1',
            container: '#micro-app-1',
            props: {
                autoCaptureError: true,
                base: '/sub-app-1',
                defaultProps: {
                    slogan: 'Hello MicroFrontend from qiankun-apps-props',
                    callback
                }
            },
        },
        ...
    ]
  • 基于qiankun提供的全局状态-initGlobalState
主应用
/* main/src/app.tsx 关键代码 */
const state = {
    slogan: 'Hello MicroFrontend from qiankun-initGlobalState',
    callback
}

// 初始化 state
const actions: MicroAppStateActions = initGlobalState(state);

//  监听数据变化
actions.onGlobalStateChange((state, prev) => {
    // update
    //actions.setGlobalState(state);
});

//  取消监听
// actions.offGlobalStateChange();
子应用
/* sub-app-1/src/app.ts 关键代码 */
const qiankun = {
  ...
  // 应用 render 之前触发
  async mount(props: any) {
    //  监听qiankun initState
    props.onGlobalStateChange((state: any, prev: any) => {
      qiankun_state = state
      //  简单实现个订阅
      listener?.(state, prev)
    })
    qiankun_props = props
  },
}
  • 基于umi-plugin-qiankun提供的hooks-useQiankunStateForSlave
    • umi微应用使用hooks-useModel, hoc-connectMaster方式获取内容
主应用
/* main/src/app.tsx 关键代码 */
export function useQiankunStateForSlave() {
    return {
        slogan: 'Hello MicroFrontend from umi-useQiankunStateForSlave',
        callback
    };
}
子应用
/* sub-app-1/src/pages/one.index.tsx 关键代码 */
import {connectMaster, useModel} from "umi";
const Sub = connectMaster(Record)
export default function One(props: any) {
  const globalProps = useModel('@@qiankunStateFromMaster');
  ...
}

样式隔离

  • 基于qiankun提供的sandbox-experimentalStyleIsolation实现样式隔离
/* main/src/pages/theme/index.ts 关键代码 */

const defaultParams = {
    base: '/',
    url: '/theme',
    settings: {
        sandbox: {
            experimentalStyleIsolation: true
        }
    }
}
...

export default function Theme() {

    return (
        <PageContainer
            ghost
            title={"基于sandbox: {experimentalStyleIsolation: true}样式隔离"}
        >
            <pre style={{background: '#f0f0f0', padding: '0 24px'}}>{text}</pre>
            <Divider/>

            <h3>主应用(main)</h3>
            <div style={{background: '#f0f0f0', padding: 24, display: "flex", flexDirection: "column"}}>
                <h4>antd-组件库样式</h4>
                <Space>
                    <Button type={"primary"}>主应用按钮1</Button>
                    <Button>主应用按钮2</Button>
                </Space>
                <Space style={{marginTop: 16}} size={24}>
                    <div>
                        <h4>CSS Modules</h4>
                        <a className={styles.link}>我是Link</a>
                    </div>
                    <div>
                        <h4>内联样式</h4>
                        <a style={{color: '#2572E6'}}>我是Link</a>
                    </div>
                    <div>
                        <h4>外联样式</h4>
                        <a className={"link"}>我是Link</a>
                    </div>
                    <div>
                        <h4>默认</h4>
                        <a>我是Link</a>
                    </div>
                </Space>
            </div>
            <Divider/>
            <h3>微应用(sub-app-1)</h3>
            <MicroApp name="sub-app-1" {...defaultParams} />
            <Divider/>
            <h3>微应用(sub-app-3)</h3>
            <MicroApp name="sub-app-3" {...defaultParams}  />

        </PageContainer>
    )
}
  • 基于css-loader添加前缀
主应用
/* main/.umirc.ts 关键代码 */
export default defineConfig({
    antd: {
        configProvider: {
            prefixCls: 'mainAnt',
        },
    },
    lessLoader: {
        modifyVars: {
            '@ant-prefix': 'mainAnt',
            "primary-color": "#004FD9"
        },
        javascriptEnabled: true,
    },
    mfsu: false
});
子应用
/* sub-app-3/craco.config.js 关键代码 */

const {name} = require('./package.json');
const CracoAntDesignPlugin = require("craco-antd");

module.exports = {
    plugins: [{
        plugin: CracoAntDesignPlugin,
        options: {
            customizeTheme: {
                "@primary-color": "#1DA57A",
            },
        }
    }],
    ...
}

错误处理

  • 基于qiankun提供的autoCaptureError属性开启组件异常捕获
/* main/src/pages/theme/index.tsx */
const defaultParams = {
    base: '/',
    url: '/theme',
    settings: {
        sandbox: {
            experimentalStyleIsolation: true
        }
    },
    // 自动捕获错误, 吊起ant <Result /> (这里并没有触发, 有BUG)
    autoCaptureError: true
}
  • 基于qiankun提供的errorBoundary属性实现自定义异常页面
路由形式
/* main/src/app.tsx 关键代码 */
export function patchClientRoutes({routes}: any) {
    routes[0].children.forEach((item: any, index: number) => {
        if (item.microApp) {
            console.log("item", item)
            routes[0].children[index].element = getMicroAppRouteComponent({
                appName: item.microApp,
                base: item.microAppProps?.base || '/',
                routePath: item.path,
                masterHistoryType: item.microAppProps?.hisory || 'browser',
                routeProps: {
                    errorBoundary: (error: any) => <CustomErrorBoundary error={error}/>
                }
            })()
        }
    })
}
组件形式
/* main/src/pages/theme/index.tsx */
const defaultParams = {
    base: '/',
    url: '/theme',
    settings: {
        sandbox: {
            experimentalStyleIsolation: true
        }
    },
    //  自定义异常页面
    errorBoundary: (error: any) => <CustomErrorBoundary error={error}/>
}
  • 基于qiankun提供的addGlobalUncaughtErrorHandler全局异常捕获
/* main/src/app.tsx 关键代码 */
//  捕获全局微应用错误
addGlobalUncaughtErrorHandler((event) => {
    //  这里会频繁触发, 注意使用
})

环境准备

clone

git clone https://github.com/xoptimal/qiankun-demo.git

项目使用pnpm管理依赖, 请务先安装pnpm

npm install -g pnpm

安装依赖

pnpm i

启动项目

pnpm dev

http://localhost:5000

预览

主应用 微应用页面形式挂载 微应用路由形式挂载 应用间通信 微应用嵌套微应用挂载 微应用嵌套微应用路由形式挂载 样式隔离 微应用路由形式加载自定义错误页面 微应用组件形式加载自定义错误页面

参考链接

最后

欢迎交流, 如果本项目对你有帮助的话, 欢迎━(`∀´)ノ亻! ⭐⭐⭐ ~