微前端qiankun方案的项目实践

使用背景

正所谓,天下合久必分,分久必合。

公司本身有个比较老的管理系统,是用jsp上手开发的,到目前为止还在使用,系统业务全部都集中在这个系统里。(合)

后面为了方便独立业务的运作,就开发了新版的独立业务模块的子系统,比如:审核系统、销售系统、运营系统、报表系统、风控系统等等,少说也有七八个。(分)

过了段时间,为了加强各个子系统之间的业务联系,又决定开发一款业务管理平台,具体功能便是账号登录后能看到各个子系统并可以互相跳转和访问数据,这就是所谓的微前端。(合)

其实本身是有考虑过使用iframe方案对子系统进行嵌套的,但是该方案的缺点比较多,如下:

  1. dom元素的割裂严重,弹窗只能在iframe内(子应用)展示,弹窗遮罩层无法放大到全局做遮挡。
  2. 路由状态易丢失,F5刷新一下,页面就跑到默认首页,用户体验极不友好。
  3. 通信较难,只能通过postmessage传递序列化的消息。

上述所列使我们不得不考虑其他办法,最终选择了其他微前端解决方案,也就是今天文章的主角:qiankun

项目架构

顶部是作为业务管理系统的导航菜单,用于展示子应用名称,作为入口。下方就是激活展示的子应用页面,内部显示效果跟独立子应用无差别,都是左侧菜单,右侧内容。

20221021111043

目录结构

├── 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是子应用的代码,其名称需与父应用mainsrc/micro-app.js中配置的名称一致。

  1. 修改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': '*' // 主应用获取子应用时跨域响应头
        }
      }
    }
    
  2. .env.development环境代码

    NODE_ENV=development
    VUE_APP_PREVIEW=true
    VUE_APP_API_BASE_URL=//localhost:8002/
    VUE_APP_PORT=8002
    
  3. 新增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__
      }
    })()
    
  4. 改造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
    
  5. 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子应用仓库并忽略掉,子应用仓库代码提交都在各自的仓库下操作,聚合库就可以避免同步操作。

  1. 克隆子应用的脚本cripts/clone-all.sh
# 运营系统
git clone git@xxx/operational-admin.git

# 销售系统
git clone git@xxx/sales-admin.git
  1. 根目录的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

  1. 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/这个二级目录下,根路径/留给主应用。

  1. 主应用main和子应用都打包好,分目录上传到服务器,子应用全部都放到subapp目录下。

    ├── main
    │   └── index.html
    └── subapp
        ├── operational-admin
        │   └── index.html
        └── sales-admin
            └── index.html
    
  2. 配置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

posted @ 2022-04-27 14:39  ykCoder  阅读(162)  评论(0编辑  收藏  举报