微前端qiankun+AntDesign组件库实现业务模块彻底解耦
Madman 后端工程师

前言

前面一篇文章Single-spa 成熟的微前端架构方案介绍了要用微前端架构的背景和目的,简单对比了微前端架构方案的技术选型,以及分享了一些微前端的概念图和简单的架构图。

这篇文章属于项目实战,利用qiankun(一个基于 single-spa 的微前端实现库)加Ant Design组件库实现微前端主应用和微应用的路由交互、状态通信等,并且微应用按需加载组件库,微应用支持独立运行模式。

主应用构建

从大部分业务需求上来看,主应用里面需要包括以下能力

  • 网站头部信息或头部导航菜单
  • 网站左侧菜单功能
  • 系统登录页面和页面权限信息分发
  • 能接入不同前端框架的微应用(Vue、React)

创建主应用

微前端解决方案,我们选择qiankun(一个基于 single-spa 的微前端实现库), 系统UI我们选用一个开箱即用的中台前端设计解决方案 Vue Antd Admin

1
git clone --depth=1 https://github.com/iczer/vue-antd-admin.git

admin项目拉下来安装依赖

1
2
3
yarn # or npm install

yarn add qiankun # 或者 npm i qiankun -S

Vue Antd Admin改造为qiankun主应用

主应用qiankun注册微应用逻辑

安装qiankun依赖后在src目录下面新增microAppRegister.js文件,用户注册微应用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165

import { registerMicroApps, start, setDefaultMountApp, runAfterFirstMounted, initGlobalState } from 'qiankun'

export default function microAppRegister (vm) {
if(!window.isQiankunStart) {
window.isQiankunStart = true

let langInfo = vm.$store.state.setting.lang

/*
*主应用的生命周期
* (这不是必须的,可以省略)
*
* */
const mainLifeCycles = {
beforeLoad: [
app => {
console.log('[LifeCycle] before load %c%s', 'color: green;', app.name)
}
],
beforeMount: [
app => {
console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name)
}
],
afterMount : [
app => {
// 处理微应用切换,公共数据没更新
console.log('[LifeCycle] after mount %c%s', 'color: red;', app.name)
let newLangInfo = vm.$store.state.setting.lang
if(newLangInfo !== langInfo) {
langInfo = newLangInfo
actions.setGlobalState({
langInfo: newLangInfo
})
}
}
],
afterUnmount: [
app => {
console.log(
'[LifeCycle] after unmount %c%s',
'color: green;',
app.name
)
}
]
}

/*
* registerMicroApps(apps, lifeCycles?)
* @param apps {Array<RegistrableApp>} - 必选,微应用的一些注册信息
* @param lifeCycles {LifeCycles} - 可选,全局的微应用生命周期钩子
*
* 当微应用信息注册完后
* 一旦浏览器url发生变化
* 便会自动触发qiankun的匹配逻辑
* 所有activeRule规则匹配上的微应用就会被插入到主应用指定的container中
* 同时以此调用微应用暴露出的生命周期钩子函数
*
* */
const loader = loading => {
console.log('LOADER %c%s', 'color:yellow', loading)
vm.$store.commit('microApp/loadingToggle', loading)
}
let apps = vm.$store.getters['microApp/microApps']
// 用户信息
let userInfo = vm.$store.getters['account/user']
console.log('registerMicroApps', apps)
registerMicroApps(
apps.map(app => {
let appInfo = {...app}
appInfo.props = Object.assign({}, appInfo.props || {}, {
userInfo: userInfo,
langInfo: langInfo
})
return { ...appInfo, loader }
}),
mainLifeCycles
)

/*
*
* setDefaultMountApp(appLink)
* @param {string} appLink 必选,设置默认加载的微应用相匹配的URL(注意与apps中的activeRule保持一致)
*
* BTW, 如果设置了默认加载的微应用URL,则主应用中就不要设置‘/’的默认路由了,后者会被前者覆盖
*
* */
setDefaultMountApp(apps[0].activeRule)

/*
* 启动 qiankun
*
* start(opts?)
* @param {Options} opts
*
*
* Options
* @param {boolean | 'all' | string[] | (( apps: RegistrableApp[] ) => { criticalAppNames: string[]; minorAppsName: string[] })} prefetch 可选,是否开启预加载,默认为 true
* @param {boolean | { strictStyleIsolation?: boolean, experimentalStyleIsolation?: boolean }} sandbox 可选,是否开启沙箱,默认为 true
* @param {boolean | ((app: RegistrableApp<any>) => Promise<boolean>)} singular 可选,是否为单实例场景,单实例指的是同一时间只会渲染一个微应用。默认为 true
* @param {Function} fetch 可选,自定义的 fetch 方法
* @param {(url: string) => string } getPublicPath 可选
* @param {(url: string) => string } getTemplate 可选
* @param {(url: string) => string } excludeAssetFilter 可选,指定部分特殊的动态加载的微应用资源(css/js) 不被qiankun 劫持处理
*
* */
start({
prefetch: true,
// 开启严格模式会造成一些有全局作用的插件或组件异常,比如antd的Modal组件
// sandbox: { strictStyleIsolation: true },
singular: true
})

/*
* runAfterFirstMounted(effect)
* 第一个微应用 mount 后需要调用的方法,比如开启一些监控或者埋点脚本。
* 这个方法不是必须的,可以不调用。
*
* @param {() => void} 必选
*
* */
runAfterFirstMounted(() => {
console.log('[MainApp] first app mounted')
})
}
}


// 初始化 state
export const state = {
testAttr: 'Hi Channing',
scrollToBottom: false,
microAppsRouterMap: [],
isLoadingMicro: false
}

console.log('initGlobalState...')

export const actions = initGlobalState(state)

/* 暴露三个全局通信方法给主应用的子组件 */
export const {
onGlobalStateChange,
setGlobalState,
offGlobalStateChange
} = actions

/* 全局通信方法的使用demo */
actions.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log('主应用全局监听到state发生变化!!!!!!!!!!!!!!!!!!!!!!!!!!')
console.log('state', state)
console.log('prev', prev)
})
// state.testAttr = 'Hi Channing again!!!!!!!'
// state.isLoadingMicro = true
// actions.setGlobalState(state)

actions.getGlobalState = (key) => {
return key ? state[key] : state
}
// actions.offGlobalStateChange()

在src/store/modules下面添加microApp.js,用户管理微应用参数状态,动态加载微应用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default {
namespaced: true,
state: {
apps: []
},
getters: {

},
mutations: {
setApps (state, apps) {
state.apps = apps
}
}
}

修改src/config/config.js, 新增配置 asyncRoutes: true 设置路由为异步路由,因为我们需要用异步动态路由来控制权限。

主应用动态路由配置

需要修改部分模拟数据和路由注册逻辑,兼容一级目录为微应用入口的逻辑

  1. 把mock数据的root.children的一级目录添加部分属性,如 appName: 'micro-vue'path: 'micro-vue'entry: 'http://localhost:9000',这些属性是为了注册微应用用的。
  2. 在src/store/modules下面新增microApp.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export default {
namespaced: true,
state: {
isLoading: false,
apps: []
},
getters: {
microApps: state => {
return state.apps
}
},
mutations: {
loadingToggle: (state, loading) => {
state.isLoading = loading
console.log("loadingToggle~~~~~~~~~~~~~~~~~~",loading)
},
setApps (state, apps) {
state.apps = apps
}
}
}

  1. 修改src/utils/routerUtil.js里面的路由加载逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 从动态路由配置提取微应用注册信息。
let apps = rootRouterConfig.map(item => {
if (item.router === 'microApp') {
console.log(item)
return {
name: item.appName,
entry: item.entry,
container: '#micro-page',
activeRule: `/${item.path}`,
$meta: {
title: item.name
},
props: {
asyncRouterConfig: item.children
}
}
}
})
apps = apps.filter(res => res !== undefined)
store.commit('microApp/setApps', apps)
1
2
3
4
5
6
7
8
9
10
11
12
// 主应用把微应用路由只保留 微应用的前缀路由加/*,因为微应用的路由配置在对应微应用解析。
routes = routes.map(r => {
if (r.path === '/') {
r.children.map(rc => {
if (rc.meta.microApp) {
rc.children = []
rc.path = `${rc.path}/*`
}
})
}
return r
})
1
2
3
4
5
6
7
// 重新解析一份完全版的菜单路由给渲染导航菜单用
const menuFinalRoutes = mergeRoutes(basicOptions.routes, allRoutes)
formatRoutes(menuFinalRoutes)
menuRouter = initRouter(store.state.setting.asyncRoutes)
menuRouter.options = {...menuRouter.options, routes: menuFinalRoutes}
menuRouter.matcher = new Router({...menuRouter.options, routes:[]}).matcher
menuRouter.addRoutes(menuFinalRoutes)

微应用构建

微应用可以用Vue的Vue cli 或 React的Create React App

Vue微应用创建

Vue的微应用用vue-cli创建好项目后,可以参照qiankun的vue微应用实践文档

在适配Vue Antd Admin 的动态路由解析需要做一定的代码修改,可以参考我的实现demo

React微应用创建

可以用React官方的Create React App, 也可以尝试Vite构建项目,我前面有一篇文章介绍了Vite的魔力

  1. 在 src 目录新增 public-path.js
    注意:运行时的 publicPath 和构建时的 publicPath 是不同的,两者不能等价替代。

    1
    2
    3
    if (window.__POWERED_BY_QIANKUN__) {
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
    }
  2. 设置 history 模式路由的 base:

1
<BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/app-react' : '/'}>
  1. 入口文件 index.js 修改,为了避免根 id #root 与其他的 DOM 冲突,需要限制查找范围。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import './public-path';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

function render(props) {
const { container } = props;
ReactDOM.render(<App />, container ? container.querySelector('#root') : document.querySelector('#root'));
}

if (!window.__POWERED_BY_QIANKUN__) {
render({});
}

export async function bootstrap() {
console.log('[react16] react app bootstraped');
}

export async function mount(props) {
console.log('[react16] props from main framework', props);
render(props);
}

export async function unmount(props) {
const { container } = props;
ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') : document.querySelector('#root'));
}
  1. 修改 webpack 配置
    安装插件 @rescripts/cli,当然也可以选择其他的插件,例如 react-app-rewired
1
npm i -D @rescripts/cli

根目录新增 .rescriptsrc.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const { name } = require('./package');

module.exports = {
webpack: (config) => {
config.output.library = `${name}-[name]`;
config.output.libraryTarget = 'umd';
config.output.jsonpFunction = `webpackJsonp_${name}`;
config.output.globalObject = 'window';

return config;
},

devServer: (_) => {
const config = _;

config.headers = {
'Access-Control-Allow-Origin': '*',
};
config.historyApiFallback = true;
config.hot = false;
config.watchContentBase = false;
config.liveReload = false;

return config;
},
};

package.json 添加脚本:

1
2
3
4

"dev": "rescripts start",
"build:micro": "rescripts build",
"test:micro": "rescripts test",

详细的API可以参考qiankun的官方文档

我的React的微应用demo

微应用接入主应用

首先微前端项目肯定是一个项目群,我的demo项目群地址:https://github.com/micro-antd-admin
这个群组里面有3个项目,一个基于Antd Admin集成qiankun的主应用,一个Vue微应用,一个React微应用

更新demo项目后,运行两个微应用 npm run dev 会有两个本地服务端口。

然后去主应用的 src/mock/user/routes.js 模拟数据添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{
router: 'microApp',
name: 'Vue 微应用',
appName: 'micro-vue',
path: 'micro-vue',
entry: 'http://localhost:9000',
icon: 'appstore',
children: [
{
router: 'parent',
name: '二级目录',
icon: 'ant-design',
children: ['test']
},
{
router: 'home',
icon: 'calendar',
name: '首页',
},
{
router: 'about',
icon: 'bulb',
name: '关于'
}
]
},
{
router: 'microApp',
name: 'React 微应用',
appName: 'micro-react',
path: 'micro-react',
entry: 'http://localhost:3000',
children: ['home', 'about', 'test']
}

以上demo如果有问题欢迎留言沟通😺

  • 本文标题:微前端qiankun+AntDesign组件库实现业务模块彻底解耦
  • 本文作者:Madman
  • 创建时间:2021-04-22 16:31:31
  • 本文链接:https://www.patpat.site/开发/前端/微前端qiankun-AntDesign组件库实现业务模彻底解耦.html
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
 评论