# 微前端框架-乾坤(qiankun)

# 1、简介

framework-qiankun 是一个基于qiankun + sanhui 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。解决iframe不能解决的问题, 如:

  1. url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。
  2. UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中.
  3. 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。
  4. 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。

# 2、代码下载

下载代码包直接运行即可看效果,主应用内嵌建协云个人端管理后台子应用部分页面。

下载应用用于开发时请删掉 src/layout/Index.vue里面的如下代码

image-20210926154203063

# 3、中台基础框架转为微前端

# 3.1 主应用

  1. 安装qiankun

    $ yarn add qiankun # 或者 npm i qiankun -S
    
  2. 新建src/utils/bus.js代码如下

    export default new Vue()
    
  3. 新建 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
    
  4. 新建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>
    
  5. 新建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>
    
  6. 配置上两个文件的路由如下:

    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 子应用

  1. 在配置文件vue.config.js中添加 如下代码:

    // 在devServer中添加允许跨域
    headers: {
    	'Access-Control-Allow-Origin': '*',
    },
    
    // 在configureWebpack/output中添加
    library: `$[name].[hash]`,
    libraryTarget: 'umd', // 把微应用打包成 umd 库格式
    jsonpFunction: `webpackJsonp_[name]`,
    
  2. 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
    
    
  3. 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;
    }
    
    
  4. 新建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')
        }
      ]
    }
    
  5. 修改layout/components/OtherBlankLayout.vue代码如下

    <template>
      <div class="blank-layout">
        <router-view></router-view>
      </div>
    </template>
    
    
  6. 在共享资源的script标签加ignore,如

    <script src="/sanhcdn/vue/dist/vue.min.js" ignore></script>
    ....
    

    如下:

    image-20211122102715924

# 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、微前端接入待办、已办

以待办为列:

  1. 在main.js中删掉base中的路由前缀 window.__POWERED_BY_QIANKUN__ ? "/jxy" : "/" 效果如下

    ···
    router = new VueRouter({
        base: '/',
        mode: 'history',
        routes,
      });
    ···
    

    然后在每个抛出的路由中加上路由前缀

    若没有则不用管

  2. 新建一个和待办路由一样的路由:/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>
    
    
  3. 联系数字中台配置子应用待办菜单

已办和打开方式相同,路由改为/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、借助 webpackfile-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"

image-20230904155027970

image-20230904154835816