微前端qiankun方案的项目实践
使用背景
正所谓,天下合久必分,分久必合。
公司本身有个比较老的管理系统,是用jsp上手开发的,到目前为止还在使用,系统业务全部都集中在这个系统里。(合)
后面为了方便独立业务的运作,就开发了新版的独立业务模块的子系统,比如:审核系统、销售系统、运营系统、报表系统、风控系统等等,少说也有七八个。(分)
过了段时间,为了加强各个子系统之间的业务联系,又决定开发一款业务管理平台,具体功能便是账号登录后能看到各个子系统并可以互相跳转和访问数据,这就是所谓的微前端。(合)
其实本身是有考虑过使用iframe
方案对子系统进行嵌套的,但是该方案的缺点比较多,如下:
- dom元素的割裂严重,弹窗只能在iframe内(子应用)展示,弹窗遮罩层无法放大到全局做遮挡。
- 路由状态易丢失,F5刷新一下,页面就跑到默认首页,用户体验极不友好。
- 通信较难,只能通过postmessage传递序列化的消息。
上述所列使我们不得不考虑其他办法,最终选择了其他微前端解决方案,也就是今天文章的主角:qiankun
项目架构
顶部是作为业务管理系统的导航菜单,用于展示子应用名称,作为入口。下方就是激活展示的子应用页面,内部显示效果跟独立子应用无差别,都是左侧菜单,右侧内容。
目录结构
├── main // 基座
├── operational-admin // 运营系统
└── sales-admin // 销售系统
基座配置
基座使用vue-cli3脚手架搭建,只负责导航渲染和登录状态管理,给子应用提供一个挂载的容器div,代码保持简洁不应涉及业务操作。
在基座中引入qiankun库,并在main.js中注册子应用便于管理
main/src/micro-app.js
代码
import store from './store'
const microApps = [
{
name: 'operational-admin',
title: '运营系统',
entry: process.env.VUE_APP_OPERATIONAL,
activeRule: '/operational-admin'
},
{
name: 'sales-admin',
title: '销售系统',
entry: process.env.VUE_APP_SALES,
activeRule: '/sales-admin'
}
]
const apps = microApps.map(item => {
return {
...item,
container: '#subapp-viewport', // 子应用挂载的div
props: {
routerBase: item.activeRule, // 下发基础路由
getGlobalState: store.getGlobalState // 下发getGlobalState方法
}
}
})
export default apps
.env.development
环境代码
VUE_APP_OPERATIONAL=//localhost:8002/subapp/operational-admin/
VUE_APP_SALES=//localhost:8001/subapp/sales-admin/
VUE_APP_API_BASE_URL=/
main/src/main.js
代码
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import { registerMicroApps, start } from 'qiankun'
import microApps from './micro-app'
import 'nprogress/nprogress.css'
import Vuex from 'vuex'
import store from '@/store/index.js'
Vue.config.productionTip = false
Vue.use(Vuex)
const instance = new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
// 定义loader方法,loading改变时,将变量赋值给App.vue的data中的isLoading
function loader (loading) {
if (instance && instance.$children) {
// instance.$children[0] 是App.vue,此时直接改动App.vue的isLoading
instance.$children[0].isLoading = loading
}
}
// 给子应用配置加上loader方法
const apps = microApps.map(item => {
return {
...item,
loader
}
})
registerMicroApps(apps, {
beforeLoad: app => {
console.log('before load app.name====>>>>>', 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: green;', app.name)
}
],
afterUnmount: [
app => {
console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name)
}
]
})
start()
App.vue
中进行基座的布局和挂载
<template>
<div class="layout-content">
<div class="layout-header">头部导航</div>
<div id="subapp-viewport"></div>
</div>
</template>
项目启动后,子应用将会挂载到<div id="subapp-viewport"></div>
容器内。
子应用配置
我们拿运营系统
举例,项目根目录下的operational-admin
是子应用的代码,其名称需与父应用main
在src/micro-app.js
中配置的名称一致。
-
修改
vue.config.js
中代码// package.json的name需注意与主应用一致 const { name } = require('./package.json') module.exports = { configureWebpack: { output: { library: `${name}-[name]`, libraryTarget: 'umd', // 把微应用打包成 umd 库格式 jsonpFunction: `webpackJsonp_${name}`, } }, devServer: { port: process.env.VUE_APP_PORT, // 在.env.development中VUE_APP_PORT=8002,与主应用的配置一致 headers: { 'Access-Control-Allow-Origin': '*' // 主应用获取子应用时跨域响应头 } } }
-
.env.development
环境代码NODE_ENV=development VUE_APP_PREVIEW=true VUE_APP_API_BASE_URL=//localhost:8002/ VUE_APP_PORT=8002
-
新增
src/public-path.js
(function () { if (window.__POWERED_BY_QIANKUN__) { if (process.env.NODE_ENV === 'development') { // eslint-disable-next-line __webpack_public_path__ = `//localhost:${process.env.VUE_APP_PORT}/` return } // eslint-disable-next-line __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ } })()
-
改造
src/router/index.js
import { constantRouterMap } from '@/config/router.config' const { name } = require('../../package.json') const createRouter = () => new Router({ mode: 'hash', base: window.__POWERED_BY_QIANKUN__ ? `/${name}` : process.env.BASE_URL, routes: constantRouterMap }) const router = createRouter() export default router
-
在
main.js
中引入public-path.js
,并改写render,添加生命周期函数import './public-path' // 引入public-path import router from './router' import store from './store' let instance = null function render (props = {}) { const { container } = props instance = new Vue({ router, store, created: bootstraps, render: h => h(App) }).$mount(container ? container.querySelector('#app') : '#app') } if (!window.__POWERED_BY_QIANKUN__) { render() } export async function bootstraps () { 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 () { instance.$destroy() instance.$el.innerHTML = '' instance = null }
子应用配置完毕,sales-admin
子应用同理配置
项目聚合管理
聚合库的目录本身是一个vue-cli
脚手架搭建的框架,这个目录下只有main
主应用,在该目录下clone子应用仓库并忽略掉,子应用仓库代码提交都在各自的仓库下操作,聚合库就可以避免同步操作。
- 克隆子应用的脚本
cripts/clone-all.sh
# 运营系统
git clone git@xxx/operational-admin.git
# 销售系统
git clone git@xxx/sales-admin.git
- 根目录的
package.json
部分代码
"scripts": {
"install": "npm-run-all --serial install:*",
"install:main": "cd main && npm i",
"install:sales-admin": "cd sales-admin && npm i",
"install:operational-admin": "cd operational-admin && npm i",
"start": "npm-run-all --parallel start:*",
"start:main": "cd main && npm start",
"start:sales-admin": "cd sales-admin && npm run serve",
"start:operational-admin": "cd operational-admin && npm run serve",
"build": "npm-run-all build:* && bash ./scripts/bundle.sh",
"build:main": "cd main && npm run build",
"build:sales-admin": "cd sales-admin && npm run build",
"build:operational-admin": "cd operational-admin && npm run build"
}
聚合库安装npm i npm-run-all -D
,用以实现一键下载依赖的功能。
npm-run-all
的--serial
表示有顺序地一个个执行,--parallel
表示同时并行地运行。
一键安装npm i
,一键启动npm start
scripts/bundle.sh
脚本
rm -rf ./dist
mkdir ./dist
mkdir ./dist/subapp
# operational-admin子应用
cp -r ./operational-admin/dist/ ./dist/subapp/operational-admin/
# sales-admin子应用
cp -r ./sales-admin/dist/ ./dist/subapp/sales-admin/
# main基座
cp -r ./main/dist/ ./dist/main/
# cd ./dist
# zip -r mp$(date +%Y%m%d%H%M%S).zip *
# cd ..
echo 'bundle.sh execute success.'
项目部署
主应用域名:xxx.com/subapp/
主应用中运营系统域名:xxx.com/subapp/operational-admin/
主应用中销售系统域名:xxx.com/subapp/sales-admin/
子应用全部都放在xxx.com/subapp/
这个二级目录下,根路径/
留给主应用。
-
主应用main和子应用都打包好,分目录上传到服务器,子应用全部都放到subapp目录下。
├── main │ └── index.html └── subapp ├── operational-admin │ └── index.html └── sales-admin └── index.html
-
配置nginx,
xxx.com
根路径指向主应用,xx.com/subapp
指向子应用server { listen 80; server_name xxx.com; location / { root /data/web/qiankun/main; # 主应用所在的目录 index index.html; try_files $uri $uri/ /index.html; } location /subapp { alias /data/web/qiankun/subapp; try_files $uri $uri/ /index.html; } }
配置完毕后记得重启
nginx -s reload