最终效果
最近公司有个新的需求,类似于低代码平台的一个需求,前端主要做的是一个拖拽生成大屏的平台,但是这个平台完全从零开发需要消耗非常多的工时,因此需要从 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-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();
可拖拽大屏平台子应用
- 在
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-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;
}
- 打包配置修改(
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-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();
}
- 修改
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 成功被主应用监听到。