最终效果
最近公司有个新的需求,类似于低代码平台的一个需求,前端主要做的是一个拖拽生成大屏的平台,但是这个平台完全从零开发需要消耗非常多的工时,因此需要从 Github 上找类似的开源的可以满足需求的组件或者项目。
最终选定了 AJ-Report 这个项目,由于这个需求一期可能要在其他系统上开发(React 技术栈),二期可能要改造形成自己的产品,而这个项目使用的技术栈是 Vue2,不管是对于一期还是二期形成自己的产品它的技术栈都不符合要求,但是通过调研,市面上也没有其他更适合的开源项目了,因此我想要使用微前端的架构将它嵌入到主应用中。
一期需求
对于一期来说,仅仅将它改造成为子系统并嵌入原有的系统中即可,具体改造配置请见二期需求。
二期需求
对于二期来说,至少需要三个工程,分别是主应用、开源的可拖拽大屏平台子应用、业务代码子应用。
由于我们团队技术栈都为 React 因此主应用和子应用的首选技术栈为 React。
主应用
- 通过 Creact React App 脚手架创建。
npx create-react-app cra-framework --template typescript
- 安装乾坤
npm i qiankun -S
- 安装路由并配置路由
npm install react-router-dom
路由及布局如下图
- 注册并启动子应用
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-vitalsreportWebVitals();
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', },]);// 启动 qiankunstart();
可拖拽大屏平台子应用
- 在
src
目录新增public-path.js
:
if (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;}
- 入口文件
main.js
修改,为了避免根 id#app
与其他的 DOM 冲突,需要限制查找范围。
相关代码主要集中在最后导出bootstrap、mount、unmount函数以及挂载 Vue(有很多 原本存在的与改造成子系统无关的代码)
import Vue from 'vue'import './public-path';import VueRouter from 'vue-router';
// element-uiimport 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 resetsimport '@/assets/styles/common.css'import '@/assets/styles/index.scss'// custome global css
// app router vuex filter mixinsimport App from './App'import router from './router'import store from './store'import * as filter from './filter'import mixins from '@/mixins'import echarts from 'echarts';// 全局定义echartsimport 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)// 全局引入datavimport dataV from '@jiaminghi/data-view'Vue.use(dataV)// anji componentimport 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 controlimport '@/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-cnVue.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;}
- 打包配置修改(
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 如下图
业务代码子应用
- 通过 Creact React App 脚手架创建。
npx create-react-app cra-sub-web --template typescript
- 在
src
目录新增public-path.js
:
if (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;}
- 设置
history
模式路由的base
:
<BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/app-react' : '/'}>
- 入口文件
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-ignoreconsole.log(window.__POWERED_BY_QIANKUN__);
// @ts-ignoreif (!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();}
- 修改
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 如下图
应用间通信
根据文档使用**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 版本移除,不推荐使用
根据 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); 控制台成功打印
将子系统中的window.qiankun.$state.status设置为 0 成功被主应用监听到。