基于
monorepo
架构搭建的微前端模版工程
├── .husky
├── .vscode
|
├──── packages 源码目录
| ├── main-react
| ├── main-vue
| ├── micro-react
| ├── micro-vue
|
├──── scripts shell 脚本
├──── typings 类型文件
|
|-- .browserslistrc
|-- .cz-config.js
|-- .editorconfig
|-- .eslintignore
|-- .eslintrc.js
|-- .gitignore
|-- .markdownlint.json
|-- .npmrc
|-- .prettierignore
|-- .prettierrc.js
|-- .stylelintignore
|-- .stylelint.js
|-- CHANGELOG.md
|-- commitlint.config.js
|-- cspell.config.js
|-- package.json
|-- pnpm-lock.yaml
|-- pnpm-workspace.yaml
|-- README.md
|-- tsconfig.json
该项目基于
monorepo
的架构,pnpm
安装依赖,typescript
编写代码。
-
使用
eslint
,stylelint
校验代码,prettier
格式化代码,i18n Ally
翻译多语言。需要安装相关的vscode
插件 -
全局安装
pnpm
npm i pnpm -g
- 构建主应用
- 构建子应用
- 应用间通信
- 资源共享
- 内存溢出
- 打包部署
- 需要一个渲染子应用的
div
容器 - 初始化的时候手动加载子应用
import classnames from 'classnames';
import _ from 'lodash';
import {
addGlobalUncaughtErrorHandler, // 添加全局未捕获异常处理器
loadMicroApp, // 手动加载一个微应用
prefetchApps, // 预加载子应用
registerMicroApps, // 注册子应用方法
runAfterFirstMounted, // 第一个微应用 mount
setDefaultMountApp, // 设默认启用的子应用
start,
} from 'qiankun';
import React, { useEffect } from 'react';
import type { MenuList } from '../../config/menuList';
import menuList from '../../config/menuList';
import microApps from '../../config/microApps';
import LayoutAside from '../LayoutAside';
import LayoutHeader from '../LayoutHeader';
import LayoutNav from '../LayoutNav';
import './index.scss';
const microAppList = microApps;
// 当前激活应用名称
let activeMicroAppName = '';
// 子应用上限
const microAppLimit = 10;
// 获取当前激活的子应用
const getActiveMicroApp = (
defaultPath = `/${window.location.pathname.split('/')[1]}`
) => {
const activeMicroApp = _.find(microAppList, (item) =>
Array.isArray(item.activeRule)
? item.activeRule.includes(defaultPath)
: item.activeRule === defaultPath
);
return activeMicroApp;
};
// 手动加载子应用
const manualLoadMicroApps = (defaultPath?: string, singular = false) => {
const microApp = getActiveMicroApp(defaultPath);
if (window.activeMicroApp?.name === microApp?.name || !microApp) return;
// 单实例模式
if (singular) {
// 卸载前一个子应用
if (window.activeMicroApp?.getStatus() === 'MOUNTED') {
window.activeMicroApp.unmount();
window.activeMicroApp = null;
window.activatedMicroApp = [];
activeMicroAppName = '';
}
// 加载新的子应用
const newMicroApp = {
name: microApp.name,
...loadMicroApp(microApp, { singular: true }),
};
newMicroApp.mountPromise.then(() => {
activeMicroAppName = microApp.name;
});
window.activeMicroApp = newMicroApp;
window.activatedMicroApp = [window.activeMicroApp];
} else {
const activeMicroApp = _.find(
window.activatedMicroApp,
(item) => item.name === microApp.name
);
// 判断当前子应用是否加载过
if (activeMicroApp) {
window.activeMicroApp = activeMicroApp;
activeMicroApp.mountPromise.then(() => {
activeMicroAppName = microApp.name;
});
} else {
const newMicroApp = {
name: microApp.name,
...loadMicroApp(microApp),
};
newMicroApp.mountPromise.then(() => {
activeMicroAppName = microApp.name;
});
window.activeMicroApp = newMicroApp;
window.activatedMicroApp.push(window.activeMicroApp);
// 超出数量限制,卸载第一个
if (window.activatedMicroApp.length > microAppLimit) {
window.activatedMicroApp.shift()?.unmount();
}
}
}
};
// 自动加载子应用
const autoLoadMicroApps = (menuList: MenuList[]) => {
let defaultPath = menuList[0].routePath;
// 预加载子应用
prefetchApps(microAppList);
// 注册子应用
registerMicroApps(microAppList);
// 设置默认子应用
const activePath = window.location.pathname.split('/')[1];
if (activePath) {
defaultPath = `/${activePath}`;
}
setDefaultMountApp(defaultPath);
// 启动微服务
start({
prefetch: true,
});
// 第一个微应用 mount 后需要调用的方法
runAfterFirstMounted(() => console.log('runAfterFirstMounted'));
// 设置全局未捕获异常处理器
addGlobalUncaughtErrorHandler((event) => console.log(event));
};
const Layout: React.FC = () => {
useEffect(() => {
// 设置默认子应用
let defaultPath = '';
let pathname = window.location.pathname;
if (pathname.split('/')[1]) {
defaultPath = `/${pathname.split('/')[1]}`;
}
if (!defaultPath && menuList.length) defaultPath = menuList[0].routePath;
// 重置子应用数组
if (!window.activatedMicroApp?.length) window.activatedMicroApp = [];
manualLoadMicroApps(defaultPath);
}, []);
return (
<div className="layout">
<LayoutHeader></LayoutHeader>
<LayoutAside></LayoutAside>
<LayoutNav></LayoutNav>
{microAppList.map((item, index) => {
return (
<div
id={item.name}
key={index}
className={classnames('micro-app', {
'active-app': activeMicroAppName === item.name,
})}
></div>
);
})}
</div>
);
};
export default Layout;
- 使用
create-react-app
构建一个react
子应用工程 - 改造
index.tsx
入口文件
import React from 'react';
import ReactDOM from 'react-dom/client';
import Router from './router';
import './public-path';
// eslint-disable-next-line no-underscore-dangle
const POWERED_BY_QIANKUN = window.__POWERED_BY_QIANKUN__;
const root = ReactDOM.createRoot(
document.querySelector('#micro-react-root') as HTMLElement
);
function render() {
root.render(
<React.StrictMode>
<Router />
</React.StrictMode>
);
}
export async function bootstrap() {
console.log('react bootstrap');
}
export async function mount() {
console.log('react mount');
render();
}
export async function update() {
console.log('react update');
render();
}
export async function unmount() {
console.log('react unmount');
root.unmount();
}
// 单独开发环境
if (!POWERED_BY_QIANKUN) mount();
子应用的
craco.config.js
const { name } = require('./package.json');
const port = 3002;
const isDev = process.env.NODE_ENV === 'development';
module.exports = {
webpack: {
configure: (webpackConfig) => {
if (!isDev) {
webpackConfig.output = {
...webpackConfig.output,
// 微前端打包配置
library: `${name}-[name]`,
libraryTarget: `umd`,
};
}
return webpackConfig;
},
},
devServer: {
https: false,
headers: {
'Access-Control-Allow-Origin': '*',
},
},
};
qiankun
官方提供了api
去解决这个问题。这里只做基本演示,也可以用其他中间件去处理应用通信的问题。例如rxjs
,vuex
,redux
- 主应用
import { initGlobalState, MicroAppStateActions } from 'qiankun';
// 初始化 state
const actions: MicroAppStateActions = initGlobalState(state);
actions.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log(state, prev);
});
actions.setGlobalState(state);
actions.offGlobalStateChange();
- 子应用
// 从生命周期 mount 中获取通信方法,使用方式和 master 一致
export function mount(props) {
props.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log(state, prev);
});
props.setGlobalState(state);
}
1.共享模块
- 该项目采用的是
monorepo
架构来共享依赖 - 也可采用
npm
Webpack Externals
Webpack DLL
等方式来共享模块
2.共享资源
props
传递window
传递
qiankun
会将微应用的JS/CSS
内容都记录在全局变量中,如果一直重复的挂载应用没有卸载,会导致内存占用过多,导致页面卡顿
- 微应用卸载的时候清空微应用注册的附加内容及
DOM
元素 - 设置自动销毁时间,去销毁那些长时间挂载的应用
- 设置最大运行应用数量,超过规定的数量的时候吧第一个应用销毁
以上做了这么多,能够部署到服务器上才算成功。我这里用的是
nginx
server {
listen 3000;
server_name localhost;
root /Users/sunweijie/monorepo-microapp/packages/main-react/dist/;
location / {
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
}
server {
listen 3001;
server_name localhost;
root /Users/sunweijie/monorepo-microapp/packages/micro-vue/dist/;
location / {
try_files $uri $uri/ /index.html;
}
}
server {
listen 3002;
server_name localhost;
root /Users/sunweijie/monorepo-microapp/packages/micro-react/dist/;
location / {
try_files $uri $uri/ /index.html;
}
}
-
安装依赖
pnpm i
-
启动应用
pnpm start
-
格式化代码
pnpm format
-
lint
校验代码pnpm lint
-
生成
CHANGELOG.md
pnpm changelog
-
commit
代码git cz