一些微前端架构的实践总结_微前端最佳实践
前言:在平时接触的项目中需要用到微前端架构解决巨石应用的问题。在经过一段时间的调研和实践中总结出一些微前端架构的经验。
技术选择
- 主应用:react(基于umi和antd-pro)
- 子应用:react(基于umi搭建)、vue(基于vue-cli搭建) ——(举两种常用的技术栈,其他技术栈以后再补充)
基础配置
主应用qiankun配置(umi项目)
使用@umijs/plugin-qiankun配置
该方法适合子应用都是基于umi架构的react项目,如果子应用是vue项目(项目复杂度比较高),用该插件配置的主应用会出现切换vue子应用时,因为js沙箱失效导致404问题。
yarn add qiankun
yarn add @umijs/plugin-qiankun -D
// config/config.ts
export default defineConfig({
.... // 其他配置代码
qiankun: {
master: {
apps: [
{
name: 'sub-app-1',
entry: '//localhost:8001'
},
{
name: 'sub-vue-app',
entry: '//localhost:9528'
}
]
}
}
})
// config/routes.ts
export default [
.... // 其他路由配置
{
name: 'sub-app-1',
icon: 'smile',
path: '/sub-app-1',
microApp: 'sub-app-1',
microAppProps: {
className: 'sub-app-1',
},
},
{
name: 'sub-vue-app',
icon: 'smile',
path: '/sub-vue-app',
microApp: 'sub-vue-app',
microAppProps: {
className: 'sub-vue-app',
},
},
]
不使用@umijs/plugin-qiankun配置,直接用qiankun提供的配置方法
该方法兼容不同技术栈的子应用
yarn add qiankun
// app.tsx
import { registerMicroApps, setDefaultMountApp, start } from 'qiankun';
const microAppsOptions = [
{
name: 'sub-app-1',
entry: '//localhost:8001',
container: '#subapp-container',
activeRule: '/sub-app-1',
className: 'sub-app-1',
},
{
name: 'sub-vue-app',
entry: '//localhost:9528',
container: '#subapp-container',
activeRule: '/sub-vue-app',
className: 'sub-vue-app',
},
];
registerMicroApps(microAppsOptions);
setDefaultMountApp('/sub-app-1');
....
// 添加子应用渲染容器
export const layout: RunTimeLayoutConfig = ({ initialState }) => {
....
childrenRender: (children) => {
let activeClassName = '';
microAppsOptions.forEach((item) => {
if (history.location.pathname.startsWith(item.activeRule)) {
activeClassName = item.className;
}
});
return (
<>
{children}{' '}
{history.location.pathname !== loginPath && (
<div id="subapp-container" className={activeClassName} />
)}
</>
);
},
}
// config/routes.ts
export default [
.... // 其他路由配置
{
name: 'sub-app-1',
icon: 'smile',
path: '/sub-app-1',
microApp: 'sub-app-1',
},
{
name: 'sub-vue-app',
icon: 'smile',
path: '/sub-vue-app',
microApp: 'sub-vue-app',
},
]
子应用qiankun配置
vue项目
- 入口文件main.js
// 声明一个变量,可以用于卸载
let instance: any = null;
let router: any = null;
// 挂载到自己的html中,基座会拿到这个挂载后的html插入进去
function render(props = {}) {
const container:any = (props as any).container;
router = new VueRouter({
base: (window as any).__POWERED_BY_QIANKUN__ ? '/sub-vue-app/' : '/',
mode: 'history',
routes,
});
instance = new Vue({
router,
store,
render: h => h(App)
}).$mount(container ? container.querySelector('#app') : '#app')
}
// // webpack打包公共文件路径
if ((window as any).__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = (window as any).__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
// 独立运行
if (!(window as any).__POWERED_BY_QIANKUN__) {
render()
}
// 子组件的协议,必须暴露三个函数
export async function bootstrap(props: any) {
console.log('bootstrap函数:', props)
}
export async function mount(props: any) {
console.log('mount函数:', props)
render(props)
}
export async function unmount(props: any) {
console.log('unmount函数:', props)
instance.$destroy()
instance = null
}
- 配置文件vue.config.js
const port = process.env.port || process.env.npm_config_port || 9528 // dev port
const { name } = require('./package.json');
module.exports = {
devServer: {
port,
headers: {
'Access-Control-Allow-Origin': '*'
}
},
configureWebpack: {
output: {
library: `${name}`,
libraryTarget: 'umd', // 把微应用打包成 umd 库格式
jsonpFunction: `webpackJsonp_${name}`,
}
},
}
react项目(umi项目)
- 安装 qiankun 插件 @umijs/plugin-qiankun
yarn add @umijs/plugin-qiankun -D
- 插件注册(.umirc.ts)
export default defineConfig({
..... // 其他配置代码
qiankun: {
slave: {}
}
});
- 配置运行时生命周期钩子(可选)
插件会自动为你创建好 qiankun 子应用需要的生命周期钩子,但是如果你想在生命周期期间加一些自定义逻辑,可以在子应用的 src/app.ts 里导出 qiankun 对象,并实现每一个生命周期钩子,其中钩子函数的入参 props 由主应用自动注入。
export const qiankun = {
// 应用加载之前
async bootstrap(props) {
console.log('app1 bootstrap', props);
},
// 应用 render 之前触发
async mount(props) {
console.log('app1 mount', props);
},
// 应用卸载之后触发
async unmount(props) {
console.log('app1 unmount', props);
},
};
react项目(ant-design-pro项目)
- 安装 qiankun 插件 @umijs/plugin-qiankun
yarn add @umijs/plugin-qiankun -D
- 当使用ant-design-pro搭建项目时,默认运行会加上以项目名称为命名的根路径(ROUTER_BASE),接入主应用及部署时需要确认子应用单独运行时的根路径名称(ROUTER_BASE)和在主应用里的根路径名称(MICRO_ROUTE_BASE)
// src/consts.ts
export const MICRO_ROUTER_BASE = '/sub-app-1/';
export const ROUTER_BASE = '/app-1/';
- 插件注册(config/config.ts)
....
import { MICRO_ROUTER_BASE, ROUTER_BASE } from '../src/consts';
const { REACT_APP_ENV, UMI_ENV } = process.env;
const isProd = UMI_ENV === 'prod';
const logoPath = isProd ? ROUTER_BASE : '/';
defaultSettings.logo = logoPath + 'logo.svg';
export default defineConfig({
...
manifest: {
basePath: ROUTER_BASE,
},
qiankun: {
slave: {},
},
define: {
MICRO_ROUTER_BASE,
ROUTER_BASE,
},
...
});
- 全局配置ROUTER_BASE(src/pages/document.ejs)
...
<body>
<script>
if (window.__POWERED_BY_QIANKUN__) {
window.routerBase = "<%= context.config.define.MICRO_ROUTER_BASE %>"
} else {
window.routerBase = "<%= context.config.define.ROUTER_BASE %>"
}
</script>
....
</body>
主子应用实现通信配置
主应用umi项目使用@umijs/plugin-qiankun配置
向umi子应用通信(使用@umijs/plugin-qiankun提供的通信方式)
- 在主应用app.ts中导出通信方法
export function useQiankunStateForSlave(): object {
const { initialState, setInitialState } = useModel('@@initialState');
return {
initialState,
setInitialState,
};
}
- 在子应用组件中使用qiankunStateFromMaster获取主应用数据
import { useModel } from 'umi';
import styles from './index.less';
function IndexPage() {
// 获取主应用的登录信息
const masterProps = useModel('@@qiankunStateFromMaster');
const { initialState } = masterProps;
return (
<>
<h1 className={styles.title}>Page index</h1>
<div>从主应用获取用户名:{initialState?.currentUser?.name}</div>
<div>从主应用获取的色值:{initialState?.currentTheme?.theme?.primaryColor}</div>
</>
);
}
export default IndexPage;
向vue子应用通信(使用qiankun提供的通信方式)
- 在主应用src目录下新建action.ts文件
import { initGlobalState, MicroAppStateActions } from 'qiankun';
// 初始化 state
const actions: MicroAppStateActions = initGlobalState({ color: '#1890ff' });
actions.onGlobalStateChange((state, prev) => {
console.log(state, prev);
});
actions.offGlobalStateChange();
export default actions;
- 在子应用入口文件main.js配置
主应用传递的数据initialState、监听数据方法onGlobalStateChange、设置数据方法setGlobalState都会以参数的形式由qiankun生命周期mount函数传递给全局的render函数,配合vuex修改全局状态。
/* eslint-disable */
import './public-path.js';
import Vue from 'vue';
import VueRouter from 'vue-router';
import App from './App.vue';
import routes from './router';
import store from './store';
Vue.config.productionTip = false;
// 声明一个变量,可以用于卸载
let instance: any = null;
let router: any = null;
// 挂载到自己的html中,基座会拿到这个挂载后的html插入进去
function render(props = {}) {
const container:any = (props as any).container;
const currentUser = (props as any)?.initialState?.currentUser;
const currentTheme = (props as any)?.initialState?.currentTheme;
store.dispatch('user/getInfo', {name: currentUser?.name || ''});
store.dispatch('theme/getColor', currentTheme?.theme?.primaryColor || '');
(props as any).onGlobalStateChange((state:any, prev:any) => {
// state: 变更后的状态; prev 变更前的状态
console.log(state, prev);
store.dispatch('theme/getColor', state?.color || '')
});
router = new VueRouter({
base: (window as any).__POWERED_BY_QIANKUN__ ? '/sub-vue-app/' : '/',
mode: 'history',
routes,
});
instance = new Vue({
router,
store,
render: h => h(App)
}).$mount(container ? container.querySelector('#app') : '#app')
}
// // webpack打包公共文件路径
// if ((window as any).__POWERED_BY_QIANKUN__) {
// __webpack_public_path__ = (window as any).__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
// }
// 独立运行
if (!(window as any).__POWERED_BY_QIANKUN__) {
render()
}
// 子组件的协议,必须暴露三个函数
export async function bootstrap(props: any) {
console.log('bootstrap函数:', props)
}
export async function mount(props: any) {
console.log('mount函数:', props)
render(props)
}
export async function unmount(props: any) {
console.log('unmount函数:', props)
instance.$destroy()
instance = null
}
主应用umi项目不使用@umijs/plugin-qiankun配置,直接用qiankun官方文档提供的方式通信
向umi子应用通信
- 在主应用src目录下新建action.ts文件
import { initGlobalState, MicroAppStateActions } from 'qiankun';
import storage from 'store';
import { MAIN_APP_THEME } from '@/consts';
// 初始化 state
const actions: MicroAppStateActions = initGlobalState({ color: '#1890ff' });
store.set(MAIN_APP_THEME, '#1890ff')
actions.onGlobalStateChange((state, prev) => {
console.log(state, prev);
});
actions.offGlobalStateChange();
export default actions;
- 在子应用组件中使用qiankunStateFromMaster获取变化后的主应用变化的数据,用localStorage获取主应用用localStorage存储的初始值
import { useModel, useEffect } from 'umi';
import storage from 'store';
import { MAIN_APP_THEME } from '@/consts';
import styles from './index.less';
function IndexPage() {
const { initialState, setInitialState } = useModel('@@initialState');
// 获取主应用的登录信息
const masterProps = useModel('@@qiankunStateFromMaster');
useEffect(() => {
if (masterProps) {
const storageColor = storage.get(MAIN_APP_THEME);
setInitialState((s) => ({
...s,
settings: {
...((initialState && initialState.settings) || {}),
currentTheme: {primaryColor: storageColor},
},
}));
const { onGlobalStateChange } = masterProps;
if (onGlobalStateChange) {
onGlobalStateChange((state: any) => {
// state: 变更后的状态; prev 变更前的状态
const currentTheme = {primaryColor: state.color};
setInitialState((s) => ({
...s,
settings: {
...((initialState && initialState.settings) || {}),
currentTheme,
},
}));
}
}
}
}, [])
return (
<div>
<h1 className={styles.title}>Page index</h1>
<div>从主应用获取的色值:{initialState?.currentTheme?.primaryColor}</div>
</div>
);
}
export default IndexPage;
向vue子应用通信(使用qiankun提供的通信方式)
- 在主应用src目录下新建action.ts文件
import { initGlobalState, MicroAppStateActions } from 'qiankun';
import storage from 'store';
import { MAIN_APP_THEME } from '@/consts';
// 初始化 state
const actions: MicroAppStateActions = initGlobalState({ color: '#1890ff' });
store.set(MAIN_APP_THEME, '#1890ff')
actions.onGlobalStateChange((state, prev) => {
console.log(state, prev);
});
actions.offGlobalStateChange();
export default actions;
- 主应用传递的监听数据方法onGlobalStateChange、设置数据方法setGlobalState 都会已参数的形式由qiankun生命周期mount函数传递给全局的render函数,配合vuex修改全局状态, 用localStorage获取主应用用localStorage存储的初始值
/* eslint-disable */
import './public-path.js';
import Vue from 'vue';
import VueRouter from 'vue-router';
import App from './App.vue';
import routes from './router';
import store from './store';
import { MAIN_APP_THEME } from './consts';
Vue.config.productionTip = false;
// 声明一个变量,可以用于卸载
let instance: any = null;
let router: any = null;
// 挂载到自己的html中,基座会拿到这个挂载后的html插入进去
function render(props = {}) {
const container:any = (props as any).container;
// const currentUser = (props as any)?.initialState?.currentUser;
const currentColor = store.get(MAIN_APP_THEME);
// store.dispatch('user/getInfo', {name: currentUser?.name || ''});
store.dispatch('theme/getColor', currentColor || '');
(props as any).onGlobalStateChange((state:any, prev:any) => {
// state: 变更后的状态; prev 变更前的状态
console.log(state, prev);
store.dispatch('theme/getColor', state?.color || '')
});
router = new VueRouter({
base: (window as any).__POWERED_BY_QIANKUN__ ? '/sub-vue-app/' : '/',
mode: 'history',
routes,
});
instance = new Vue({
router,
store,
render: h => h(App)
}).$mount(container ? container.querySelector('#app') : '#app')
}
// // webpack打包公共文件路径
// if ((window as any).__POWERED_BY_QIANKUN__) {
// __webpack_public_path__ = (window as any).__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
// }
// 独立运行
if (!(window as any).__POWERED_BY_QIANKUN__) {
render()
}
// 子组件的协议,必须暴露三个函数
export async function bootstrap(props: any) {
console.log('bootstrap函数:', props)
}
export async function mount(props: any) {
console.log('mount函数:', props)
render(props)
}
export async function unmount(props: any) {
console.log('unmount函数:', props)
instance.$destroy()
instance = null
}
本文为原创内容,若转载请注明出处,转发感激不尽。
相关文章
- MyBatis如何实现分页查询?_mybatis collection分页查询
- 通过Mybatis Plus实现代码生成器,常见接口实现讲解
- MyBatis-Plus 日常使用指南_mybatis-plus用法
- 聊聊:Mybatis-Plus 新增获取自增列id,这一次帮你总结好
- MyBatis-Plus码之重器 lambda 表达式使用指南,开发效率瞬间提升80%
- Spring Boot整合MybatisPlus和Druid
- mybatis 代码生成插件free-idea-mybatis、mybatisX
- mybatis-plus 团队新作 mybatis-mate 轻松搞定企业级数据处理
- Maven 依赖范围(scope) 和 可选依赖(optional)
- Trace Sql:打通全链路日志最后一里路