# 微前端框架-乾坤(qiankun)
# 1、简介
framework-qiankun 是一个基于qiankun + sanhui 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。解决iframe不能解决的问题, 如:
- url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。
- UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中.
- 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。
- 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。
# 2、代码下载
下载代码包直接运行即可看效果,主应用内嵌建协云个人端、管理后台、子应用部分页面。
下载应用用于开发时请删掉 src/layout/Index.vue里面的如下代码

# 3、中台基础框架转为微前端
# 3.1 主应用
安装qiankun
$ yarn add qiankun # 或者 npm i qiankun -S新建
src/utils/bus.js代码如下export default new Vue()新建
src/qiankun/index.js代码如下import { registerMicroApps, start } from 'qiankun'; import bus from '@/utils/bus' import store from '@/store' // 将bus挂载在Vue原型,保持父子应用一致 Vue.prototype.$bus = bus registerMicroApps([ { name: 'JXYQY', entry: process.env.VUE_APP_JXYQY_WEB, container: '#qiankun_jxyqy', activeRule: '/jxyqy', }, { name: 'JXY', entry: process.env.VUE_APP_JXY_WEB, container: '#qiankun_jxy', activeRule: '/jxy', props: { bus, systemCode: store.state.systemCode } } ]); export default start新建
src/views/admin/jxy/Index.vue文件,用于匹配建协云个人端<template> <div class="jxy"> <div id="qiankun_jxy" v-show="!id"></div> </div> </template> <script> import start from '@/qiankun/index' export default { data () { return { id: '', type: '' } }, mounted () { // 启动微前端 if (!window.qiankunStarted) { window.qiankunStarted = true; start(); } // 监听代办 this.$bus.$on('qiankun-backlog', (e) => { // 点击代办触发 }) }, beforeDestroy () { // 销毁监听 this.$bus.$off('qiankun-backlog') } } </script>新建
src/views/admin/jxyqy/Index.vue文件,用于匹配管理后台<template> <div id="qiankun_jxyqy"></div> </template> <script> import start from '@/qiankun/index' export default { mounted () { // 启动微前端 if (!window.qiankunStarted) { window.qiankunStarted = true; start(); } } } </script>配置上两个文件的路由如下:
const Layout = () => import('@/layout/Index') export default [ { path: '/login', meta: { title: '登录', isLogin: false}, component: SANHUI.Login }, { path: '/', meta: { title: '布局'}, name: 'layout', component: () => import('@/layout/Index'), children: [ { path: '/userInfo', meta: { title: '个人信息', isMenu: false}, component: SANHUI.UserInfo }, ...module, { path: '/405', meta: { title: '405', isLogin: false}, component: SANHUI.Error405 }, { path: '/jxy/*', meta: { title: '建协云', isMenu: false}, component: () => import('@/views/admin/jxy/Index') }, { path: '/jxyqy/*', meta: { title: '管理后台', isMenu: false}, component: () => import('@/views/admin/jxyqy/Index') } ] }, { path: '*', meta: { title: '404', isLogin: false}, component: SANHUI.Error404 } ]
# 3.2 子应用
在配置文件
vue.config.js中添加 如下代码:// 在devServer中添加允许跨域 headers: { 'Access-Control-Allow-Origin': '*', }, // 在configureWebpack/output中添加 library: `$[name].[hash]`, libraryTarget: 'umd', // 把微应用打包成 umd 库格式 jsonpFunction: `webpackJsonp_[name]`,在
src/router/index.js中修改如下代码:// 注释如下代码 // const router = new VueRouter({ // mode: 'history', // base: process.env.BASE_URL, // routes // }) // // 路由守卫 // router.beforeEach((to, from, next) => { // Vue.prototype.$beforeRouter(to, from, next, process.env, Vue) // }) // 抛出routes export default routes在
src/main.js中修改如下代码修改:
// import router from './router' import routes from './router'删除:
const vm = new Vue({ router, store, render: h => h(App) }).$mount('#app') export default vm添加:()
// 微前端 - 子应用配置 let router = null; let instance = null; if (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; } function render(props = {}) { const { container } = props; router = new VueRouter({ base: '/', mode: 'history', routes, }); router.beforeEach((to, from, next) => { Vue.prototype.$beforeRouter(to, from, next, process.env, Vue) }) instance = new Vue({ router, store, render: h => h(App), }).$mount(container ? container.querySelector('#app') : '#app'); } if (!window.__POWERED_BY_QIANKUN__) { render(); } export default instance export async function bootstrap() { console.log('[vue] vue app bootstraped'); } export async function mount(props) { //props 包含主应用传递的参数 也包括为子应用 创建的节点信息 if (props.systemCode) { store.state.systemCode = props.systemCode } render(props); } export async function unmount() { instance.$destroy(); instance = null; router = null; }新建
src/router/module/other.js把要嵌入主应用的页面重新抛出export default { path: '/other', meta: { title: '布局', isLogin: false}, component: () => import('@/layout/components/OtherBlankLayout'), children: [ { path: '/other/index', meta: { title: '首页'}, component: () => import('@/views/admin/index/Index') }, { path: '/other/page', meta: { title: '页面1'}, component: () => import('@/views/admin/page/Index') }, { path: '/other/page2', meta: { title: '页面2'}, component: () => import('@/views/admin/page2/Index') } ] }修改
layout/components/OtherBlankLayout.vue代码如下<template> <div class="blank-layout"> <router-view></router-view> </div> </template>在共享资源的script标签加ignore,如
<script src="/sanhcdn/vue/dist/vue.min.js" ignore></script> ....如下:

# 3.3 建协云菜单
- 个人端:
以`/jxy/...` 开头:
全部公告: /jxy/other/noticeAll
公告发布: /jxy/other/enterpriseNotice
平台通告: /jxy/other/platformNotice
我的会议: /jxy/other/myConference
会议室预定: /jxy/other/meetingReserve
预警管理: /jxy/other/earlyWarning
待办事项:/jxy/xy/backlog
已办事项:/jxy/xy/backlogProcessedList
超时记录:/jxy/xy/waitDoneOverTimeRecord
超时统计:/jxy/xy/waitDoneOverTimeStatistics
发布附件:/jxy/xy/attachment/publish
发布平台附件:/jxy/xy/attachment/publishPlatform
全部附件:/jxy/xy/attachment/all
通讯录:/jxy/xy/userContactList
综合查询:/jxy/fourLibrary/index
项目信息:/jxy/fourLibrary/projectInfo
单项工程:/jxy/fourLibrary/singleProject
工程信息:/jxy/fourLibrary/unitProject
单体工程:/jxy/fourLibrary/monomerProject
用地规划:/jxy/fourLibrary/landPlanPermission
施工图审查:/jxy/fourLibrary/workDrawingExamine
工程设计:/jxy/fourLibrary/projectDesign
工程招投标:/jxy/fourLibrary/projectBidding
施工许可:/jxy/fourLibrary/workLicence
企业信息:/jxy/fourLibrary/enterpriseInfo
人员信息:/jxy/fourLibrary/personalnfo
综合查询:/jxy/fourLibraryManage/index
项目信息:/jxy/fourLibraryManage/projectInfo
单项工程:/jxy/fourLibraryManage/singleProject
工程信息完善:/jxy/fourLibraryManage/unitProject
单体工程:/jxy/fourLibraryManage/monomerProject
企业信息:/jxy/fourLibraryManage/enterpriseInfo
人员信息:/jxy/fourLibraryManage/personalnfo
开工前资料准备:/jxy/fourLibraryManage/projectPreparation
- 管理后台:
以`/jxyqy/...`开头:
组织维度: /jxyqy/other/orgType
组织管理: /jxyqy/other/orgManagenent
权限管理: /jxyqy/other/roleManagement
用户管理: /jxyqy/other/userManagement
分/子组织: /jxyqy/other/chargeOrganization
应用管理: /jxyqy/other/applicationManagement
菜单管理: /jxyqy/other/selfMenu
资源管理: /jxyqy/other/selfResource
会议室管理: /jxyqy/other/meeting
首页:/jxyqy/portal/index
运营平台:
以`/sub/...`开头:
企业列表: /sub/amdmin/unitList
用户列表: /sub/amdmin/userList
# 4、服务器配置
在代码里修改
Dockerfile文件FROM nginx:1.17.3-alpine MAINTAINER zfe ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone WORKDIR / ADD dist/ /usr/share/nginx/html/ ARG PROFILE_ACTIVE ENV PROFILE_ACTIVE_ENV $PROFILE_ACTIVE COPY nginx.conf /etc/nginx/conf.d/default.conf在代码根目录新增
nginx.conf文件server { listen 80; server_name localhost; #gzip压缩 gzip_static on; gzip_proxied expired no-cache no-store private auth; #charset koi8-r; #access_log /var/log/nginx/host.access.log main; location = /index.html { add_header Cache-Control no-cache; add_header Access-Control-Allow-Origin *; root /usr/share/nginx/html; } 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'; if ($request_method = 'OPTIONS') { return 204; } root /usr/share/nginx/html; try_files $uri $uri/ /index.html; index index.html index.htm; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } }
# 5、微前端接入待办、已办
以待办为列:
在main.js中删掉
base中的路由前缀window.__POWERED_BY_QIANKUN__ ? "/jxy" : "/"效果如下··· router = new VueRouter({ base: '/', mode: 'history', routes, }); ···然后在每个抛出的路由中加上路由前缀
若没有则不用管
新建一个和待办路由一样的路由:
/jxy/xy/backlog,指向的.vue文件如下<template> <div class="jxy"> <!-- 业务单 - 弹框 --> <component :is="CompnentsForm[businessType]" :param="param" @loadList="loadList"></component> </div> </template> <script> import { CompnentsForm } from './formDialog' export default { data () { return { CompnentsForm, param: {}, businessType: '' } }, methods: { loadList() { // // 触发代办列表刷新 this.$bus.$emit('qiankun-backlog-loadList') }, // 截取参数 getUrlParam(url){ var parames = {} let paramArr = url.split('&') paramArr.forEach(item => { let arr = item.split('=') parames[arr[0]] = arr[1] }) return parames }, }, mounted () { // 监听代办 this.$bus.$on('qiankun-backlog', (e) => { // 解析待办参数 this.param = this.getUrlParam(e) // 判断是自己的待办加载弹框组件 if (this.param.type === 'replenishCard') { this.businessType = param.type } else { // 不是则制空 this.businessType = '' } }) }, beforeDestroy () { // 销毁监听 this.$bus.$off('qiankun-backlog') } } </script>联系数字中台配置子应用待办菜单
已办和打开方式相同,路由改为/jxy/xy/backlogProcessedList,监听改为 qiankun-backlog-done
# 6、其他问题
# 6.1 静态资源加载错误
把引入路径改为绝对路径,如
<script src="/sanhcdn/vue/dist/vue.min.js"></script>
# 6.2 vue-pdf 微前端调用报错
注释调如下代码,直接引用云上的封装好的sh-pdf-diglog
// 引入组件
// import { ShPdfDiglog } from '@/components'
// 注册组件
// components: { ShPdfDiglog }
# 6.3 子应用打开会改变主应用项目title
在子应用中:
删除
/store/index.js的state: { projectName: '项目名称' ... }, mutations: { projectInfor(state, payload) { state.projectInfor = payload }, ... }删除
main.js里的document.title = store.state.projectName在
.env文件中新增VUE_APP_PROJRCT_NAME = '项目名称'在
index.html中新增<title><%= VUE_APP_PROJRCT_NAME %></title>
# 6.4 静态资源无法加载
原因:子应用是以相对路径加载资源,默认路径为子应用的ip或域名;当嵌入在微前端里面的时候,js被请求到主应用运行时,默认路径变成主应用的ip和域名,所以以相对路径加载不到资源;
解决方法:
1、以require请求为绝对路径引入
如背景图写法:
.bg {
background-image: url(./bg.png);
...
}
需要改成:
<div :style="{'background-image': `url(${require('./bg.png')})`}"></div>
2、借助 webpack 的 file-loader ,在打包时给其注入完整路径(适用于字体文件和图片体积比较大的项目)
const publicPath = process.env.NODE_ENV === 'production' ? 'https://qiankun.umijs.org/' : `http://localhost:${port}`;
module.exports = {
chainWebpack: (config) => {
config.module
.rule('fonts')
.use('url-loader')
.loader('url-loader')
.options({
limit: 4096, // 小于4kb将会被打包成 base64
fallback: {
loader: 'file-loader',
options: {
name: 'fonts/[name].[hash:8].[ext]',
publicPath,
},
},
})
.end();
config.module
.rule('images')
.use('url-loader')
.loader('url-loader')
.options({
limit: 4096, // 小于4kb将会被打包成 base64
fallback: {
loader: 'file-loader',
options: {
name: 'img/[name].[hash:8].[ext]',
publicPath,
},
},
});
},
};
6.4 主应用嵌入子应用导致子应用界面keeplive失效问题
以下内容全部在子应用中进行操作:
将抛出路由界面 添加 keepAlive 设置为 true
v-if="$router.app._route.meta.keepAlive"

