成熟项目接入微前端-qiankun
一、接入原因
- 部分企业希望基于主应用做二次开发。公司希望能够保留源码去接入企业二次开发新增的模块功能。
- 项目越来越大,运行和打包速度都很慢
二、配置主应用基座
1. 安装qiankun
yarn add qiankun
2. 注册接入的微应用
在src新建micro/index.js
import NProgress from 'nprogress';
import { start, registerMicroApps, addGlobalUncaughtErrorHandler } from 'qiankun';
/**
* 注册微应用
* 第一个参数 - 微应用的注册信息
* 第二个参数 - 全局生命周期钩子
*/
registerMicroApps(
[
{
name: 'MicroAppReact',
// 开发环境入口
entry: '//localhost:3200',
// 主应用为打包后的代码时,便于nginx配置到统一域名
// entry: '/micro/react/',
container: '#container',
activeRule: '/react',
},
],
{
beforeLoad(app) {
NProgress.start();
console.log('before load', app.name);
return Promise.resolve();
},
afterMount(app) {
NProgress.done();
console.log('after mount', app.name);
return Promise.resolve();
},
},
);
/**
* 添加全局的未捕获异常处理器
*/
addGlobalUncaughtErrorHandler((event) => {
console.error(321, event);
});
// 导出 qiankun 的启动函数
export default start;
3. 添加微应用容器
在src/micro新建template.js
import React from 'react';
function Template() {
return <div id="container" />;
}
export default Template;
4. 新增微应用路由
配置微应用在主应用的菜单
// /src/views/router.js
/* ----------------------微应用接入----------------------- */
{
key: 'invoice-record',
loader: () => import('../micro/template'),
path: '/react',
},
5. 在入口文件启动qiankun
// src/index.jsx
import startQiankun from './micro';
startQiankun();
三、接入微应用-react
因为当前我们的技术栈是react,所以这里只做react的接入介绍。但是qiankun本身支持接入多种技术栈的微应用。如有需要自行查看官网
1.创建新项目
cra脚手架创建react项目
create-react-app react-micro
2.覆盖配置文件
可以直接npm run eject暴露webpack等配置项,也可以直接依赖第三方库覆盖配置
yarn add @rescripts/cli
根目录新建.rescriptsrc.js
const path = require('path')
const { name } = require('./package');
const resolve = dir => path.join(__dirname, dir)
const webpackConfig = {
webpack: config => {
config.output.library = `${name}-[name]`;
config.output.libraryTarget = 'umd';
config.output.jsonpFunction = `webpackJsonp_${name}`;
config.output.globalObject = 'window';
// config.output.publicPath = '/micro/react/',
config.resolve.extensions = ['.js', '.jsx']
// 配置别名
config.resolve.alias = {
'@components': resolve('src/components'),
'@config': resolve('src/config'),
'@hooks': resolve('src/hooks'),
'@redux': resolve('src/redux'),
'@services': resolve('src/services'),
// '@static': resolve('src/static'),
'@utils': resolve('src/utils'),
'@views': resolve('src/views'),
}
return config;
},
devServer: _ => {
const config = _;
config.headers = {
'Access-Control-Allow-Origin': '*',
};
config.historyApiFallback = true;
config.hot = false;
config.watchContentBase = false;
config.liveReload = false;
// 配置代理服务
config.proxy = {
'/api': {
target: 'https://test.abc.cn',
changeOrigin: true,
secure: false,
},
}
return config;
},
};
module.exports = [
['use-antd', {
theme: {
// 全局主色
'@primary-color': '#0077FF',
// 链接色
'@link-color': '#0077FF',
'@link-hover-color': '#3392FF',
'@link-active-color': '#005FCC',
// 成功色
'@success-color': '#13CE66',
// 警告色
'@warning-color': '#F7BA2A',
// 错误色
'@error-color': '#FF4949',
// 字体
'@font-family':
'"Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif',
// 主字号
'@font-size-base': '12px',
// 组件/浮层圆角
'@border-radius-base': '0',
'@table-padding-horizontal': '12px',
'@tabs-horizontal-margin': '0 20px 0 0',
'@tabs-horizontal-padding-lg': '16px 20px',
// 菜单
'@menu-item-font-size': '13px',
'@menu-item-height': '41px',
}
}],
webpackConfig
]
3.修改package.json-为了执行到覆盖的配置
"scripts": {
"start": "rescripts start",
"build": "rescripts build",
"test": "rescripts test",
"eject": "react-scripts eject"
},
4.配置微应用运行端口--也就是开发环境主应用配置的entry
根目录新建.env.development
// .env.development
// 运行端口
PORT = 3200
// websocket监听热更新
WDS_SOCKET_PORT = 3200
5.入口文件导出微应用生命周期钩子
注意:要求微应用需要导出生命周期钩子函数,qiankun 内部通过 import-entry-html 加载微应用,如果微应用没有导出这三个生命周期钩子函数,则微应用会加载失败。
import './public-path'
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
function render(props) {
const { container } = props;
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
container ? container.querySelector('#root') : document.querySelector('#root')
);
}
if (!window.__POWERED_BY_QIANKUN__) {
render({});
}
export async function bootstrap() {
console.log('micro app react bootstrap')
}
export async function mount(props) {
console.log('micro app react mount, props11111', props)
render(props)
}
export async function unmount(props) {
console.log('micro app react unmount, props', props)
const { container } = props
ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') : document.querySelector('#root'))
}
// 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();
6.创建微应用页面
在src新建pages文件夹,新建Home.jsx, List.jsx
// Home.js
import React from 'react'
export default function Home() {
return (
<div>
我是React微应用的主页
</div>
)
}
7.添加路由
修改App.js
import { BrowserRouter, Route } from 'react-router-dom'
import './App.css';
import Home from './pages/Home';
import List from './pages/List';
function App() {
return (
<BrowserRouter className="App" basename={window.__POWERED_BY_QIANKUN__ ? '/react' : '/'} >
<Route path="/" exact component={Home} />
<Route path="/list" exact component={List} />
</BrowserRouter>
);
}
export default App;
到这里就已经完成了主应用接入微应用了,分别启动主应用和微应用,就能在主应用菜单下看到对应的微应用模块。
四、应用之间通信
1.通信方式
一种是官方提供的应用间通信方式 - Actions,另一种是Shared 通信,主应用和微应用各自维护redux状态池。我这里先介绍这次用到的Actions通
2.通信原理
-
我们可以先注册 观察者 到观察者池中,然后通过修改 globalState 可以触发所有的 观察者 函数,从而达到组件间通信的效果。
-
qiankun 内部提供了 initGlobalState 方法用于注册 MicroAppStateActions 实例用于通信,该实例有三个方法,分别是:
-
setGlobalState:设置 globalState - 设置新的值时,内部将执行 浅检查,如果检查到 globalState 发生改变则触发通知,通知到所有的 观察者 函数。
-
onGlobalStateChange:注册 观察者 函数 - 响应 globalState 变化,在 globalState 发生改变时触发该 观察者函数。
-
offGlobalStateChange:取消 观察者 函数 - 该实例不再响应 globalState 变化
3.主应用配置
- 在主应用中注册一个 MicroAppStateActions 实例并导
// src/shared/actions.jsx
import { initGlobalState } from 'qiankun';
const initialState = {};
const actions = initGlobalState(initialState);
export default actions;
- 在注册 MicroAppStateActions 实例后,我们在需要通信的组件中使用该实例,并注册 观察者函数,我们这里以登录之后传递用户信息和权限信息给子应用为例。
import { generatePath } from 'react-router-dom';
import router from '@views/router';
// 引入actions
import actions from '../shared/actions';
/**
* 按钮级别
*/
const BUTTON_LEVEL = 4;
const getRouter = (menuCode) => router.find((item) => item.key === menuCode);
const getChildren = (list = [], menuCode = null) => {
const items = [];
list.forEach((item) => {
if (item.parentCode === menuCode && item.level !== BUTTON_LEVEL) {
const routerItem = getRouter(item.menuCode) || {};
const { path, params } = routerItem;
items.push({
...item,
...routerItem,
url: path ? generatePath(path, params) : '',
children: getChildren(list, item.menuCode),
});
}
});
return items;
};
const getDefaultRouter = (items = []) => {
let defaultRouter = null;
items.forEach((item) => {
if (!defaultRouter) {
if (item.path) {
defaultRouter = item;
} else {
defaultRouter = getDefaultRouter(item.children);
}
}
});
return defaultRouter;
};
export default {
user: (state = null, { type, payload }) => {
let navItems = [];
let defaultRouter = null;
switch (type) {
case 'user/login':
// 请求得到用户信息的时候放入全局状态池
actions.setGlobalState({ ...payload });
navItems = getChildren(payload.menuList);
defaultRouter = getDefaultRouter(navItems);
return {
...state,
...payload,
defaultRouter,
navItems,
};
case 'user/logout':
return null;
case 'user/set': {
return {
...state,
...payload,
};
}
default:
return state;
}
},
};
4.微应用配置
此时主应用已经把用户信息和权限信息放在全局状态池中,微应用可以通过注册观察者去监听数据的变化
- 设置一个 Actions 实例
const emptyAction = () => {
// 警告:提示当前使用的是空 Action
console.error('这个action是空的')
}
class Actions {
// 默认值为空 Action
actions = {
setGlobalState: emptyAction,
onGlobalStateChange: emptyAction,
}
// 设置 actions
setActions(actions) {
if (actions) {
const { setGlobalState, onGlobalStateChange } = actions
this.actions.setGlobalState = setGlobalState
this.actions.onGlobalStateChange = onGlobalStateChange
}
}
// 映射
setGlobalState(...args) {
return this.actions.setGlobalState?.(...args)
}
//映射
onGlobalStateChange(...args) {
return this.actions.onGlobalStateChange?.(...args)
}
}
const actions = new Actions()
export default actions
- 在入口文件 index.js 的 render 函数中注入真实的Actions
import './public-path'
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import actions from './shared';
function render(props) {
if (props) {
// 注入 actions 实例
actions.setActions(props);
}
const { container } = props;
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
container ? container.querySelector('#root') : document.querySelector('#root')
);
}
if (!window.__POWERED_BY_QIANKUN__) {
render({});
}
export async function bootstrap() {
console.log('micro app react bootstrap')
}
export async function mount(props) {
console.log('micro app react mount, props', props)
render(props)
}
export async function unmount(props) {
console.log('micro app react unmount, props', props)
const { container } = props
ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') : document.querySelector('#root'))
}
// 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();
- 现在在页面就可以去获取全局状态池的数据了。
// src/pages/Home.jsx
import React, { useEffect, useState } from 'react'
import actions from '../shared';
export default function Home() {
const [name, setName] = useState('');
useEffect(() => {
actions.onGlobalStateChange(state => {
const {userName} = state
console.log('micro app react state', state)
setName(userName)
}, true)
}, [])
return (
<div>
我是React微应用的主页---{name}
</div>
)
}
以上,就完成了主应用和微应用之间的通信。
五、配置热更新
1.热更新原理
webpack-dev-server运行时会启动一个http 的server和一个websocket的server,代码变更之后会重新构建产生新的hash值的文件,通过websocket告诉浏览器(在message里面通知,类似-----a["{"type":"hash","data":"f4fdfa57"}"]),客户端就会发起2个请求,也就是webpack的hot-update。
2.微应用配置
写死微应用websocket请求的端口
// src/.env.development
// 运行端口
PORT = 3200
// 热更新监听端口
WDS_SOCKET_PORT = 3200
六、部署--nginx配置
注意点:
- 微应用配置publicPath与nginx配置的前缀保持一致
- 主应用的entry与nginx配置的前缀保持一致
server {
listen 8005;
listen [::]:8005;
server_name cmptest.jss.cn;
location / {
# 主应用静态资源路径
root /Users/zengxiaobai/projects/nuonuo/qiankun_demo/react_main;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /micro/react {
# 开发环境配置静态资源路径
proxy_pass http://localhost:3200/;
# 生产环境静态资源路径
# root /Users/zengxiaobai/projects/nuonuo/qiankun_demo;
# index index.html index.htm;
# try_files $uri $uri/ /micro/react/index.html;
add_header "Access-Control-Allow-Origin" $http_origin;
# 允许请求方法
add_header "Access-Control-Allow-Methods" "*";
# 允许请求的 header
add_header "Access-Control-Allow-Headers" "*";
}
location /baseweb/ {
# 应用接口反向代理
proxy_pass https://test.abc.cn;
}
location /customization/ {
# 应用接口反向代理
proxy_pass https://test.abc.cn;
}
}
七、demo地址
地址:
八、遇到的困难和解决方式
1.主应用的路由使用了Suspense,按需加载,刷新的时候处于loading状态,就找不到container容器引起报错
解决办法:在主应用入口文件限启动微应用的时间。
import startQiankun from './micro';
let container;
if (!container) {
const interval = setInterval(() => {
container = document.getElementById('container');
if (container) {
console.log(2);
clearInterval(interval);
startQiankun();
}
}, 100);
}
2.主应用只给压缩包的情况下,微应用需要主应用的权限,如何进项二次开发
解决办法:二次开发的团队启动一个本地nginx。微应用代理到localhost:3200,参考前面的nginx部署