随心而记
1675 字
8 分钟
使用乾坤微前端嵌入早期不同技术栈的项目
2024-07-02
发布时间
分类
标签

最终效果#

最近公司有个新的需求,类似于低代码平台的一个需求,前端主要做的是一个拖拽生成大屏的平台,但是这个平台完全从零开发需要消耗非常多的工时,因此需要从 Github 上找类似的开源的可以满足需求的组件或者项目。

最终选定了 AJ-Report 这个项目,由于这个需求一期可能要在其他系统上开发(React 技术栈),二期可能要改造形成自己的产品,而这个项目使用的技术栈是 Vue2,不管是对于一期还是二期形成自己的产品它的技术栈都不符合要求,但是通过调研,市面上也没有其他更适合的开源项目了,因此我想要使用微前端的架构将它嵌入到主应用中。

一期需求#

对于一期来说,仅仅将它改造成为子系统并嵌入原有的系统中即可,具体改造配置请见二期需求。

二期需求#

对于二期来说,至少需要三个工程,分别是主应用、开源的可拖拽大屏平台子应用、业务代码子应用。

由于我们团队技术栈都为 React 因此主应用和子应用的首选技术栈为 React。

主应用#

  1. 通过 Creact React App 脚手架创建。
npx create-react-app cra-framework --template typescript
  1. 安装乾坤
npm i qiankun -S
  1. 安装路由并配置路由
npm install react-router-dom

路由及布局如下图

Untitled

  1. 注册并启动子应用
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import RouterGurad from './router/RouterGurad';
import reportWebVitals from './reportWebVitals';
import { registerMicroApps, start, initGlobalState, MicroAppStateActions } from 'qiankun';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <BrowserRouter>
      <RouterGurad />
    </BrowserRouter>
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

registerMicroApps([
  {
    name: 'app-vue',
    entry: '//localhost:9528',
    container: '#root',
    activeRule: '/app-vue',
  },
  {
    name: 'cra-sub-web',
    entry: '//localhost:3002',
    container: '#main',
    activeRule: '/cra-sub-web',
  },
]);
// 启动 qiankun
start();

可拖拽大屏平台子应用#

  1. 在 src 目录新增 public-path.js
if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
  1. 入口文件 main.js 修改,为了避免根 id #app 与其他的 DOM 冲突,需要限制查找范围。

相关代码主要集中在最后导出bootstrap、mount、unmount函数以及挂载 Vue(有很多 原本存在的与改造成子系统无关的代码)

import Vue from 'vue'
import './public-path';
import VueRouter from 'vue-router';

// element-ui
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import zhLocale from 'element-ui/lib/locale/lang/zh-CN'
import 'normalize.css/normalize.css'// A modern alternative to CSS resets
import '@/assets/styles/common.css'
import '@/assets/styles/index.scss'// custome global css

// app router vuex filter mixins
import App from './App'
import router from './router'
import store from './store'
import * as filter from './filter'
import mixins from '@/mixins'
import echarts from 'echarts';
// 全局定义echarts
import ECharts from 'vue-echarts'
import 'echarts/lib/chart/bar'
import 'echarts/lib/component/tooltip'
//import 'echarts-liquidfill'
// import 'echarts-gl'
Vue.component('v-chart', ECharts)
// 全局引入datav
import dataV from '@jiaminghi/data-view'
Vue.use(dataV)
// anji component
import anjiCrud from '@/components/AnjiPlus/anji-crud/anji-crud'
import anjiSelect from '@/components/AnjiPlus/anji-select'
import anjiUpload from '@/components/AnjiPlus/anji-upload'
Vue.component('anji-upload', anjiUpload)
Vue.component('anji-crud', anjiCrud)
Vue.component('anji-select', anjiSelect)

// permission control
import '@/permission'
// 按钮权限的指令
import permission from '@/components/Permission/index'
Vue.use(permission)

import Avue from '@smallwei/avue';
import '@smallwei/avue/lib/index.css';
Vue.use(Avue);

// enable element zh-cn
Vue.use(ElementUI, { zhLocale })

// register global filter.
Object.keys(filter).forEach(key => {
  Vue.filter(key, filter[key])
})

// register global mixins.
Vue.mixin(mixins)

// 分页的全局size配置;
Vue.prototype.$pageSizeAll = [10, 50, 100, 200, 500]

// create the app instance.
// new Vue({ el: '#app', router, store, render: h => h(App) })

Vue.config.productionTip = false;

let instance = null;
function render(props = {}) {
  const { container } = props;
  router.base = window.__POWERED_BY_QIANKUN__ ? '/app-vue/' : '/';

  instance = new Vue({
    el: container ? container.querySelector('#app') : '#app',
    router,
    store,
    render: (h) => h(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] props from main framework', props);
  render(props);
}
export async function unmount() {
  console.log('[vue] vue app unmount');
  instance.$destroy();
  instance.$el.innerHTML = '';
  instance = null;
  // router = null;
}
  1. 打包配置修改(vue.config.js):

由于这个 vue 项目使用的脚手架版本的问题并没有 vue.config.js文件,因此直接修改 webpack 配置

const devWebpackConfig = merge(baseWebpackConfig, {
  output: {
    path: config.build.assetsRoot,
    filename: '[name].js',
    publicPath: config.dev.assetsPublicPath,
    library: 'app1',
    libraryTarget: 'umd', // 把微应用打包成 umd 库格式
    jsonpFunction: `webpackJsonp_${name}`, // webpack 5 需要把 jsonpFunction 替换成 chunkLoadingGlobal
  },
   devServer: {
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
   }
});

配置完成后访问 http://localhost:3001/app-vue#/bigscreen/designer?reportCode=test_001 如下图

Untitled

业务代码子应用#

  1. 通过 Creact React App 脚手架创建。
npx create-react-app cra-sub-web --template typescript
  1. 在 src 目录新增 public-path.js
if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
  1. 设置 history 模式路由的 base
<BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/app-react' : '/'}>
  1. 入口文件 index.js 修改,为了避免根 id #root 与其他的 DOM 冲突,需要限制查找范围。
import React from 'react';
import './public-path';
import ReactDOM from 'react-dom/client';
import routes from './router';
import { BrowserRouter, useRoutes } from 'react-router-dom';
import './index.css';

let root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);

function App () {
  return useRoutes(routes)
}
// @ts-ignore
console.log(window.__POWERED_BY_QIANKUN__);

// @ts-ignore
if (!window.__POWERED_BY_QIANKUN__) {
  root.render(
    <React.StrictMode>
      {/* @ts-ignore */}
      <BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/cra-sub-web' : '/'}>
        <App />
      </BrowserRouter>
    </React.StrictMode>
  );
}

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

export async function mount(props: any) {
  console.log('[react16] props from main framework', props);
  const { container } = props;
  root = ReactDOM.createRoot(container ? container.querySelector('#root') as HTMLElement : document.getElementById('root') as HTMLElement);
  console.log(container ? container.querySelector('#root') as HTMLElement : document.getElementById('root') as HTMLElement);
  
  root.render(
    <React.StrictMode>
      {/* @ts-ignore */}
      <BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/cra-sub-web' : '/'}>
        <App />
      </BrowserRouter>
    </React.StrictMode>
  );
}

export async function unmount(props: any) {
  // 卸载
  root.unmount();
}
  1. 修改 webpack 配置

我使用的是 react-app-rewired 库,安装 react-app-rewired

npm install react-app-rewired --save-dev

修改 package.json

{
  "scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test",
    "eject": "react-scripts eject"
  }
}

创建 config-overrides.js 文件

const { override, addWebpackAlias } = require('customize-cra');
const path = require('path');
const { name } = require('./package');

module.exports = {
  webpack: override(
    (config) => {
      config.output.library = `${name}-[name]`;
      config.output.libraryTarget = 'umd';
      // Webpack 5 需要把 jsonpFunction 替换成 chunkLoadingGlobal
      config.output.chunkLoadingGlobal = `webpackJsonp_${name}`;
      config.output.globalObject = 'window';
      return config;
    },
    addWebpackAlias({
      '@': path.resolve(__dirname, 'src'),
    })
  ),

  devServer: (configFunction) => {
    const config = configFunction;

    config.headers = {
      'Access-Control-Allow-Origin': '*',
    };
    config.historyApiFallback = true;
    config.hot = false;
    config.watchContentBase = false;
    config.liveReload = false;

    return config;
  },
};

重新启动服务 然后访问 http://localhost:3001/cra-sub-web/home 如下图

Untitled

应用间通信#

根据文档使用**GlobalState** , 使用方式类似于 bus。

首先在主应用中定义 state

import { initGlobalState, MicroAppStateActions } from 'qiankun';

const state = {
  status: 1,
};

const actions: MicroAppStateActions = initGlobalState(state);

actions.onGlobalStateChange((state, prev) => {
  // state: 变更后的状态; prev 变更前的状态
  console.log(state, prev);
  if (state.status === 0) {
    alert('token 失效,跳转登录页');
    window.location.href = '/home';
  }
});

子应用从 mount 函数中消费即可,这里将函数保存到 Vue 示例上。

export async function mount(props) {
  console.log('[vue] props from main framework', props);
  Vue.prototype.$onGlobalStateChange = props.onGlobalStateChange;
  Vue.prototype.$setGlobalState = props.setGlobalState;
  render(props);
}
// 使用
Vue.prototype.$setGlobalState({
  status: 2
});

但是使用时控制台警告提醒该 api 将要在 qiankun3.0 版本移除,不推荐使用

Untitled

根据 Github Issue 中看到开发者建议将变量挂载到 window对象中,最终修改如下

const state = {
  status: 1,
};
// window 添加__qiankun__.$state全局变量
window.__qiankun__ = {};
window.__qiankun__.$state = state;

// 监听 window.__qiankun__.$state 变化
window.__qiankun__.$state = new Proxy(state, {
  set: function (target, key, value) {
    console.log('set', key, value);
    if (key === 'status' && value === 0) {
      alert('token 失效,跳转登录页');
      window.location.href = '/home';
    }
    return Reflect.set(target, key, value);
  },
});

进入子系统 console**.log**(window**.qiankun.**$state); 控制台成功打印

Untitled

将子系统中的window.qiankun.$state.status设置为 0 成功被主应用监听到。

Untitled

使用乾坤微前端嵌入早期不同技术栈的项目
https://www.promises.top/posts/embedding-legacy-projects-with-different-tech-stacks-using-qiankun-micro-frontend/
作者
发布于
2024-07-02
许可协议
CC BY-NC-SA 4.0