vue基座的阿里 qiankun主、微应用的demo配置

418 阅读2分钟

文前备注:

  • 本文阿里 qiankun版本为2.x

  • 本文vue项目皆以vue-cli 4.x 脚手架搭建。

  • 本文react项目皆以create-react-app 5.x 脚手架搭建

  • 本文仅为个人测试demo代码,非最优解

一、主应用配置

主应用qiankun改造需要修改的文件.png

1、安装 qiankun

npm install -s qiankun

该命令为npm环境命令,其他环境自行变更

2、修改/src/main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from '@/store'

Vue.config.productionTip = false

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#mainApp')


// 乾坤改造相关,为了便于观察,相关 import 单独写在相应代码附近,实际项目请按规范放置顶部
import {registerMicroApps,addGlobalUncaughtErrorHandler,initGlobalState} from "qiankun";

// 注册微应用
const config= [
    //微应用1
    {
        name:"vue2-app1",
        entry:"http://192.168.1.111:8181/", // 微应用访问地址
        container:"#childAppContainer",  // 主应用中的微应用容器
        activeRule:"/childApp/vue2-app1", // 微应用在主应用路由中的前置路径,乾坤监听主应用中路由变化,匹配到对应路径,会请求>加载>渲染相应的微应用
        props:{}, // 传递给微应用的参数
    },
    // 微应用2
    {
        name:"react-app1",
        ......
    },
    ......
]
registerMicroApps(config);

// 添加全局的未捕获异常处理器
addGlobalUncaughtErrorHandler(e => {
    const message = e.message||"";
    const appName = message.replace(/[\w\s:]*?'([\d\w-_]*app[\d\w-_]*)'[\w\s_:]*/ig,`$1`);
    let errorType = 'unknown';
    if(/Failed to fetch/ig.test(message)){
        setTimeout(()=>{
          console.log('qk全局捕获异常处理器',`加载微应用【${errorData.appName}】失败,重定向404`);
          router.replace({path:"/404"});
        },100);
    }
});

// 全局状态(主/微应用共享数据)
const state = {
  token: store.state.token,
  lang: store.state.lang,
};
// 初始化
const actions = initGlobalState(state);
// 主应用监听
actions.onGlobalStateChange((state, prev) => {
  // state: 变更后的状态; prev 变更前的状态
  console.log("onGlobalStateChange", state, prev);
  const { token } = state;
  const changeToken = token !== store.state.token;
  if (changeToken) {
      console.log('检测到token变更,执行响应操作')
  }
});

3、修改 /src/router/index.js

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/childApp/*', // 通配符方便匹配微应用路径
    name: 'childApp',
    component: () => import( '../views/ChildApp.vue')
  },
  {
    path: '/404',
    name: 'page404',
    component: () => import( '../views/System/404.vue')
  },
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
});

const childrenPath = ["/childApp/vue2-app1"];// 微应用注册配置中相应的activeRule,实际项目中可以使用单独文件导出共享该配置参数
router.beforeEach((to, from, next) => {
  console.log('router.beforeEach', `【${from.path}】 => 【${to.path}】`);
  if(to.name){
    if(to.name !== 'app') return next();
    if(childrenPath.some(item => to.path.includes(item)))  return next();
  }
  // 重定向404
  return next({path:"/404"});
});
router.afterEach((to, from) => {
  console.log('router.afterEach', `【${from.path}】 => 【${to.path}】`);
});

export default router

4、新增 /src/views/ChildApp.vue

<template>
  <!--微应用容器-->
  <div id="childAppContainer"></div>
</template>

<script>
  import Vue from 'vue'
  import { start } from 'qiankun'

  export default {
    mounted() {
      // 启动乾坤
      if (!window.qiankunStarted) {
        window.qiankunStarted = true;
        start();
      }
    }
  }
</script>

二、微应用 vue2.x + vue-router3.x 配置

vue微应用qiankun改造需要修改的文件.png

1、新增 /src/public-path.js

if (window.__POWERED_BY_QIANKUN__) {
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

2、修改 /src/router/index.js

import Vue from 'vue'
// import VueRouterfrom 'vue-router'
import Home from '../views/Home.vue'

Vue.use(VueRouter)

export const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: () => import('../views/About.vue')
  },
]

// const router = new VueRouter({
//   mode: 'history',
//   base: process.env.BASE_URL,
//   routes
// })

// export default router

3、修改 /src/main.js

import Vue from 'vue'
import App from './App.vue'
//import router from './router'
import store from './store'

Vue.config.productionTip = false

// new Vue({
//   router,
//   store,
//   render: h => h(App)
// }).$mount('#app')


import {routes} from './router'
import VueRouter from  'vue-router'

const isQiankun = window.__POWERED_BY_QIANKUN__;
let router = null;
let instance = null;
function render(props) {
  const {container} = props;
  const base = isQiankun ? '/childApp/vue2-app1/' : process.env.BASE_URL;
  router = new VueRouter({
    mode: 'history',
    base: base,
    routes
  });
  router.beforeEach((to, from, next) => {
    console.log("router.beforeEach",`【${from.path}】 => 【${to.path}】`);
    next();
  });
  router.afterEach((to, from) => {
    console.log("router.afterEach",`【${from.path}】 => 【${to.path}】`);
  });
  instance = new Vue({
    router,
    store,
    render: h => h(App)
  }).$mount(container ? container.querySelector('#app') : '#app');
}

if(!isQiankun) {
  render();
}

export async function bootstrap() {
  console.log("qk.bootstrap");
}

export async function mount(props:ObjectType) {
  console.log("qk.mount",props);
  props.onGlobalStateChange((state,prev)=>{
    console.log("onGlobalStateChange",state,prev);
    const { token } = state;
    if(token !== store.state.token) {
        console.log("检测到token变更,执行后续操作")
    }
  },true);
  render(props);
}

export async function unmount() {
  console.log("qk.unmount");
  if(instance) instance.$destroy();
  if(instance) instance.$el.innerHTML = '';
  instance = null;
  router = null;
}

4、在/src同级目录下新增 vue.config.js

const { name } = require('./package');
module.exports = {
    devServer: {
        headers: {
            'Access-Control-Allow-Origin': '*',
        },
    },
    configureWebpack: {
        output: {
            library: `${name}-[name]`,
            libraryTarget: 'umd', // 把微应用打包成 umd 库格式
            jsonpFunction: `webpackJsonp_${name}`,
        },
    },
};

三、微应用react 17.x + react-router-dom 6.x 配置

react微应用qiankun改造需要修改的文件.png

1、新增 /src/public-path.js

if (window.__POWERED_BY_QIANKUN__) {
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

2、修改 /src/index.js

import React from 'react';
// import {render} from 'react-dom';
import './index.css';

import {BrowserRouter,Route,Routes} from "react-router-dom";

import App from './App';
import Expenses from './view/expenses'
import Invoices from './view/invoices'
import InvoiceDetails from './view/invoiceDetails'

// const rootElement = document.getElementById("root");

// render(
//     <BrowserRouter>
//         <Routes>
//             <Route path="/" element={<App/>}>
//                 <Route path="/expenses" element={<Expenses/>}/>
//                 <Route path="/invoices" element={<Invoices/>}>
//                     <Route path=":invoiceId" element={<InvoiceDetails/>}/>
//                 </Route>
//                 <Route path="*" element={
//                     <p>There's nothing here!</p>
//                 }/>
//             </Route>
//         </Routes>
//     </BrowserRouter>,
//     rootElement
// );


import {render as ReactRender,unmountComponentAtNode} from 'react-dom';

function getRootElement(container) {
    return container ? container.querySelector('#root') : document.getElementById("root")
}
function render(props) {
    const {container} = props;
    const rootElement = getRootElement(container);

    ReactRender(
        <BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/childApp/react17-app2' : '/'}>
            <Routes>
                <Route path="/" element={<App/>}>
                    <Route path="/expenses" element={<Expenses/>}/>
                    <Route path="/invoices" element={<Invoices/>}>
                        <Route path=":invoiceId" element={<InvoiceDetails/>}/>
                    </Route>
                    <Route path="*" element={
                        <p>There's nothing here!</p>
                    }/>
                </Route>
            </Routes>
        </BrowserRouter>,
        rootElement
    );
}

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

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

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

export async function unmount(props) {
    const {container} = props;
    const rootElement = getRootElement(container);
    unmountComponentAtNode(rootElement);
}

3、修改webpack打包配置、开发环境devServer配置

通过create-react-app 5.x搭建的create项目,你会发现没有对外开放webpakc的配置文件。

当需要自定义webapck配置时,有两个方案:

  1. 通过create-react-app 5.x自身携带的eject命令转成自定义webpack工程,create-react-app会将webpack的配置拓印一个镜像到复制到项目中,并且所有的package.json命令都会被指向镜像的配置文件。

    因为eject命令执行的操作不可逆,正常情况不推荐不熟悉webpack配置的开发者使用该方案。

  2. 通过插件@rescripts/cli来实现create-react-app项目的webpack自定义配置。

下面,详细介绍这两种方案的具体实现

3.1、 通过 create-react-app 5.xeject命令自定义webpack配置
  1. 执行create-react-app 5.xeject命令

    npm run eject
    

    该命令为npm环境命令,其他环境自行变更

    命令执行完后,项目中可见/config/scripts文件夹,这两个文件夹中就是webpack的相关配置文件

    webpack的相关配置文件.png

  2. 新建/config/qiankun.js文件(不建也行,建哪里都行)

const { name } = require('../package');

module.exports = {
    devServer: {
        port: 3001,
    },
    output: {
        library: `${name}-[name]`,
        libraryTarget: 'umd', // 把微应用打包成 umd 库格式
        // jsonpFunction: `webpackJsonp_${name}`, // webpack 4
        chunkLoadingGlobal: `webpackJsonp_${name}`, // webpack 5
        globalObject: `window`,
    }
}
  1. 修改/config/webpack.config.js文件,新增两行代码

    const { output } = require('./qiankun'); // 新增代码
    
    module.exports = function (webpackEnv) {
        return {
            output:{
                ...output, //  新增代码
            }
        }
    }
    

    create-react-app版本不同,代码可能稍有偏差,但只要在关键的 module.exports.outpu 出口配置处,新增对应的打包配置即可。

  2. 修改/script/start.js文件

    const { devServer } = require('../config/qiankun');
    // const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000;
    const DEFAULT_PORT = parseInt(process.env.PORT, 10) || devServer.port || 3000;
    
3.2、 通过 @rescripts/cli插件自定义webpack配置

@rescripts/cli插件自定义webpack配置需要修改的文件.png

  1. 安装 @rescripts/cli

    npm install -d @rescripts/cli
    

    该命令为npm环境命令,其他环境自行变更

  2. 修改 package.json

    {
        "scripts": {
            // "start": "react-scripts start",
            // "build": "react-scripts build",
            // "test": "react-scripts test",
            // "eject": "react-scripts eject",
            "start": "rescripts start",
            "build": "rescripts build",
            "test": "rescripts test"
        }
    }
    
  3. 新增 .rescriptsrc.js

    const { name } = require('./package');
    
    module.exports = {
        webpack: (config) => {
            config.output.library = `${name}-[name]`;
            config.output.libraryTarget = 'umd';
            // config.output.jsonpFunction = `webpackJsonp_${name}`; // webpack 4
            config.output.chunkLoadingGlobal = `webpackJsonp_${name}`;// webpack 5
            config.output.globalObject = 'window';
            return config;
        },
    
        devServer: (_) => {
            const config = _;
            // config.port = 3002; // create-react-app 5.x无效,请使用.env.POST
            config.headers = {
                'Access-Control-Allow-Origin': '*',
            };
            config.historyApiFallback = true;
            config.hot = false;
            // config.watchContentBase = false; // webpack 4
            config.static = { watch:false }; // webpack 5
            config.liveReload = false;
            return config;
        },
    };
    
  4. 新增 .env.development

    PORT=3002