react为基座,集成react/vue子应用

284 阅读2分钟

参考文档

qiankun.umijs.org/zh

相关库的版本

  1. 主应用技术栈(create-react-app构建):
  • react@18.2.0
  • react-router-dom@6.10.0
  • react-redux@8.0.5
  • qiankun@2.10.11
  • "@craco/craco
  1. 子应用1技术栈(create-react-app构建)
  • react@18.2.0
  • react-router-dom@6.10.0
  • react-redux@8.0.5
  • "@craco/craco
  1. 子应用2技术栈(vue-cli构建的):
  • vue@2.6.10
  • vue-router@3.1.3
  • vuex@3.0.1

主应用的配置

  1. 下载依赖 yarn add qiankun/npm i qiankun,
  2. 主应用路由配置 我的子应用容器是QiankunPage组件
import React, { lazy } from 'react'
import { Navigate } from 'react-router-dom'
import NotExistPage from '@/pages/404'
import QiankunPage from '@/pages/QiankunPage'
import AuthRouter from '@/components/AuthRouter'

const Layout = lazy(() => import("../pages/Layout"))
const Mainpage = lazy(() => import("../pages/MainPage"))
const routes = [

    {
        path: '/',
        element: <Layout />,
        children: [
            {
                path: 'mainpage',
                element: <Mainpage />
            },
            {
                path: '/*',
                element: <QiankunPage />
            },
        ]
    },
    {
        path: '/404',
        element: <NotExistPage />
    },
]

export default routes
  1. QiankunPage组件相关代码如下:
import { useEffect } from 'react'
import { Spin } from 'antd'
import { registerApps } from '../../qiankun'
import { connect } from 'react-redux'
import globalAction from "@/qiankun/globalAction"
import { setGlobalState } from '@/qiankun/reducer/actions'

const QiankunPage = (props) => {

    useEffect(() => {
        //注册子应用
        registerApps()
        //监听由乾坤管理的状态
        globalAction.onGlobalStateChange((state, prev) => {
            props.dispatch(setGlobalState(state)) //乾坤的管理的state存到react-redux的store方便使用
        })

    }, [])

    return (<Spin spinning={props.loading}>
        //  俺的子应用容器节点
        <div id="qiankun_container" style={{ flex: 1, height: '100%', }} />
    </Spin>)
}

export default connect(({ qiankunGlobalState }) => {
    return {
        loading: qiankunGlobalState.loading[qiankunGlobalState.activeAppName]
    }
})(QiankunPage)
  1. 注册子应用
//子应用配置文件:subAppConfig.js文件
import globalAction from "@/qiankun/globalAction"

const apps = [
    {
        name: 'sub_react', //微应用的名称
        entry: 'http://localhost:5000/', //微应用的入口
        container: '#qiankun_container', //微应用的容器节点的选择器或者 Element 实例
        activeRule: ({ href }) => {
            return isActiveCurrentApp("sub_react", href, "/sub_react/")
        }, //微应用的激活规则--当浏览器 url 发生变化时,会自动检查每一个微应用注册的 activeRule 规则,符合规则的应用将会被自动激活。
        props: {  //props属性用来给子应用传递下发消息
            //下发给子应用的全局状态
            getGlobalState: globalAction.getGlobalState
        }
    },
    {
        name: 'sub_vue', //微应用的名称
        entry: 'http://localhost:8002/', //微应用的入口
        container: '#qiankun_container', //微应用的容器节点的选择器或者 Element 实例
        activeRule: ({ href }) => {
            return isActiveCurrentApp("sub_vue", href, "/sub_vue/")
        }, //微应用的激活规则--当浏览器 url 发生变化时,会自动检查每一个微应用注册的 activeRule 规则,符合规则的应用将会被自动激活。
        props: {
            getGlobalState: globalAction.getGlobalState
        }
    }
]
//判断是否激活当前应用
const isActiveCurrentApp = (appName, href, urlShort) => {
    const flag = href.includes(urlShort)
    if (flag) {
        //此处把当前激活的app存起来,方便在app资源下载阶段开启加载动画,避免白屏时间过长
        globalAction.setGlobalState({ activeAppName: appName })
    }
    return flag
}
export default apps
import { registerMicroApps, start, addGlobalUncaughtErrorHandler } from 'qiankun'
import subApps from './subAppConfig'
import globalAction from "@/qiankun/globalAction"
/* 调用registerMicroApps注册子应用 */
const registerApps = () => {
    let apps = subApps
    registerMicroApps(apps, {
        beforeLoad: [
            app => {
                console.log(`${app.name}load前完成了 %c%s`, 'color: green;', app.name)
            }
        ],
        beforeMount: [
            app => {

                console.log(`${app.name}挂载前完成了 %c%s`, 'color: green;', app.name)
            }
        ],
        afterMount: [
            app => {
                const loading = { ...globalAction.getGlobalState().loading, [app.name]: false }
                globalAction.setGlobalState({ loading })
                console.log(`${app.name}应用挂载完成了 %c%s`, 'color: green;', app.name)
            }
        ],
        afterUnmount: [
            app => {
                console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name)
            }
        ]
    })

    // 启动qiankun
    start({
        prefetch: true,  //并开启预加载
        sandbox: {
            strictStyleIsolation: true, // 开启样式隔离
        },
    })
}
addGlobalUncaughtErrorHandler(event => {
    const { message: msg } = event
    if (msg && msg.includes('died in status LOADING_SOURCE_CODE')) {
        console.error('子应用加载失败,请检查应用是否可运行')
    }
})
export { registerApps }
  1. 创建qiankun状态管理页面(为了完成应用之间的通信)
import { initGlobalState } from 'qiankun'
import store from "@/store"
import { setGlobalState } from "./reducer/actions"

const initialState = {
    loading: {
        'sub_react': true,// 加载动画初始化为true
        "sub_vue": true
    },
    activeAppName: '',
    message: '全局消息'
}

store.dispatch(setGlobalState(initialState))
const globalAction = initGlobalState(initialState)

// 获取state的方法
globalAction.getGlobalState = key => {
    return key ? initialState[key] : initialState
}

export default globalAction

到此主应用里面的工作已经完成了

子应用配置

集成react子应用
  1. 在src底下创建public-path.js文件夹 写入如下代码
if (window.__POWERED_BY_QIANKUN__) {
    // eslint-disable-next-line no-undef
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
  1. craco.config.js文件配置
const path = require('path')
const { name } = require('./package')

module.exports = {
    webpack: {
        // 路径别名
        alias: {
            '@': path.resolve(__dirname, 'src')
        },
        configure: (webpackConfig, { env, paths }) => {
            webpackConfig.output.library = `${name}-[name]`
            webpackConfig.output.libraryTarget = 'umd'
            webpackConfig.output.globalObject = 'window'
            // webpackConfig.output.jsonpFunction = `webpackJsonp_${name}`;
            return webpackConfig
        }
    },
    // babel 配置
    babel: {},
    plugins: [],
    devServer: {
        port: 5000,
        headers: {
            'Access-Control-Allow-Origin': '*'
        }
    }
}

  1. 在index.js入口文件中做如下工作
import React from 'react'
import ReactDOM from 'react-dom/client'
import { RouterProvider } from 'react-router-dom'
import { Provider } from 'react-redux'
import store from '@/store'
import { ConfigProvider } from 'antd'
import zhCN from 'antd/es/locale/zh_CN'
import './index.css'
import routes from './router'
import { StyleProvider, legacyLogicalPropertiesTransformer } from '@ant-design/cssinjs'
import '@/utils/importDayjsPlugin'
import './public-path'; // 引入qiankun配置文件
import globalAction from "@/utils/globalAction"
let root = null

//获取ReactDOMRoot实例
const getReactDOMRoot = (props) => {
    if (window.__POWERED_BY_QIANKUN__) {
        root = ReactDOM.createRoot(props.container.querySelector('#root'))
        globalAction.setActions(props)
    } else {
        root = ReactDOM.createRoot(document.querySelector('#root'))
    }
    return root
}

function render (props) {
    getReactDOMRoot(props).render(
        <ConfigProvider locale={zhCN}>
            <StyleProvider hashPriority="high" transformers={[legacyLogicalPropertiesTransformer]}>
                <Provider store={store}>
                    <RouterProvider router={routes} />
                </Provider>
            </StyleProvider>
        </ConfigProvider>
    )
}

if (!window.__POWERED_BY_QIANKUN__) {
    render({})
}
/**
 * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
 * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
 */
export async function bootstrap (props) {
    console.log('%c[react18] app bootstraped', 'background:#364a93;color:#fff', props)
}
/**
 * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
 */
export async function mount (props) {
    console.log('%c[react18] props from11111 main framework', 'background:#364a93;color:#fff', props)
    render(props)
}
/**
 * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
 */
export async function unmount (props) {
    console.log('%c[react18] app unmount', 'background:#364a93;color:#fff', props)
    root.unmount()
}
  1. 路由配置(我用的hash路由,用history也是可以的,但是主子应用的路由模式得一制)
import React from 'react'也是可以的,但是主子应用的路由模式得一制
import { createHashRouter } from 'react-router-dom'
import Layout from '../pages/Layout'
import NotExistPage from '@/pages/404'
import AuthRouter from '@/components/AuthRouter'
import Qiankun from "../pages/Qiankun"
import Page1 from "@/pages/Page1"
import Page2 from "@/pages/Page2"
const childRoutes = [
    {
        path: '/sub_react/page1',
        element: (
            <AuthRouter>
                <Page1 />
            </AuthRouter>
        )
    },
    {
        path: '/sub_react/page2',
        element: (
            <AuthRouter>
                <Page2 />
            </AuthRouter>
        )
    }
]

let routes = [
    {
        path: '/',
        //我的子应用也需要单独做为项目使用所以这里做了区分,Qiankun组件做了一下主要用于监听主应用的消息并映射到store里面
        element: !window.__POWERED_BY_QIANKUN__ ? (
            <AuthRouter>
                <Layout />
            </AuthRouter>
        ) : <Qiankun />,
        children: childRoutes
    },
    {
        path: '/*',
        element: <NotExistPage />
    }
]

export default createHashRouter(routes)
  1. Qiankun.jsx 代码. 如下:
import React, { useEffect } from 'react'
import { connect } from 'react-redux'
import { Outlet } from 'react-router-dom'
import globalAction from "@/utils/globalAction"
import { setGlobalState } from "./globalReducer/actions"
import "./index.less"

const Qiankun = props => {

    useEffect(() => {
        globalAction.onGlobalStateChange((state) => {
            props.dispatch(setGlobalState(state))
        }, true)
    }, [])

    return <div className="sub_qiankun_container"> <Outlet /></div>
}

export default connect(() => ({}))(Qiankun)
  1. globalAction.js代码如下:
class globalAction {
    actions = {
        onGlobalStateChange: () => { },
        setGlobalState: () => { }
    };

    globalState = {}

    // 设置actions
    setActions (actions) {
        this.actions = actions;
    }

    // 映射  onGlobalStateChange
    onGlobalStateChange (...args) {
        return this.actions.onGlobalStateChange && this.actions.onGlobalStateChange(...args);
    }
    //获取主应用管理的所有状态
    getGlobalState (...args) {
        return this.actions.getGlobalState && this.actions.getGlobalState(...args);
    }
    // 映射  setGlobalState
    setGlobalState (...args) {
        return this.actions.setGlobalState(...args);
    }
}

export default new globalAction()

#####集成vue子应用

  1. 在src底下创建public-path.js文件夹 写入如下代码
if (window.__POWERED_BY_QIANKUN__) {
    // eslint-disable-next-line no-undef
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
  1. vue.config.js必要的配置
 devServer: {
        hot: true,
        disableHostCheck: true,
        port,
        overlay: {
            warnings: false,
            errors: true,
        },
        headers: {
            'Access-Control-Allow-Origin': '*',
        },
    },
configureWebpack: {
        resolve: {
            alias: {
                '@': resolve('src'),
            },
        },
        output: {
            // 把子应用打包成 umd 库格式
            library: `${name}-[name]`,
            libraryTarget: 'umd',
            jsonpFunction: `webpackJsonp_${name}`,
        },
    },
  1. 在 main.js入口文件的改造
import './public-path';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import Vue from 'vue';
import VueRouter from 'vue-router';
import App from './App.vue';
import routes from './router';
import store from './store';
import "@/assets/fonts/iconfont.css"
import "@/assets/css/global.css"

Vue.config.productionTip = false;
Vue.use(ElementUI);
let router = null;
let instance = null;
function render (props = {}) {
    const { container } = props;
    router = new VueRouter({
        base: "",
        mode: 'hash',
        routes,
    });

    instance = new Vue({
        router,
        store,
        render: h => h(App),
    }).$mount(container ? container.querySelector('#app') : '#app');
}

if (!window.__POWERED_BY_QIANKUN__) {
    render();
}

export async function bootstrap () {
    console.log('[vue] vue app bootstraped');
}

export async function mount (props) {
    console.log('[vue] mount props from main framework', props);
    render(props);
}

export async function unmount () {
    instance.$destroy();
    instance.$el.innerHTML = '';
    instance = null;
    router = null;
}
  1. 路由配置(我用的hash路由,用history也是可以的,但是主子应用的路由模式得一制)
import Vue from 'vue';
import VueRouter from 'vue-router';

Vue.use(VueRouter);

const routes = [
    {
        path: "/",
        redirect: "/sub_vue/1111"
    },
    {
        path: '/sub_vue/page1', //
        name: 'sub_vue_1',
        component: () => import(/* webpackChunkName: "page1" */ '../views/demo1/index.vue'),
    },
    {
        path: '/sub_vue/page2', //我的日程
        name: 'sub_vue_2',
        component: () => import(/* webpackChunkName: "page2" */ '../views/demo/index.vue'),
    },
];
export default routes;