精华内容
下载资源
问答
  • 微前端qiankun

    2021-04-20 16:31:57
    <p>qiankun主应用在开发环境怎么配置应用的代理</p>
  • 微前端qiankun集成

    千次阅读 2020-03-23 21:57:12
    关于ant desgin of vue 微前端qiankun项目集成 qiankun介绍 qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。 什么是微前端 微前端架构具备以下几个...

    关于ant desgin of vue 微前端qiankun项目集成

    qiankun介绍

    qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。

    什么是微前端

    微前端架构具备以下几个核心价值:

    • 技术栈无关
      主框架不限制接入应用的技术栈,子应用具备完全自主权
    • .独立开发、独立部署
      子应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
    • 增量升级
      在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略
    • 独立运行时
      每个子应用之间状态隔离,运行时状态不共享

    步骤

    1. 下载qiankun
    $ git clone https://github.com/umijs/qiankun.git
    
    1. 安装依赖
    $ yarn install
    $ yarn examples:install
    
    1. 运行qiankun项目
    $ yarn examples:start
    
    1. 查看项目是否跑成功
    http://localhost:7099
    

    集成ant desgin of vue项目到qiankun

    1. 主项目main中的index.html中修改侧边栏
    <ul class="mainapp-sidemenu">
        <li onclick="push('/react16')">React16</li>
        <li onclick="push('/react15')">React15</li>
        <li onclick="push('/vue')">Vue</li>
        <li onclick="push('/angular9')">Angular9</li>
        <li onclick="push('/project')">蚂蚁</li>
     </ul>
    
    1. 注册子应用(在主项目main中的index.js中)
    registerMicroApps(
      [
        {
          name: 'react16',
          entry: '//localhost:7100',
          render,
          activeRule: genActiveRule('/react16'),
        },
        {
          name: 'react15',
          entry: '//localhost:7102',
          render,
          activeRule: genActiveRule('/react15'),
        },
        {
          name: 'vue',
          entry: '//localhost:7101',
          render,
          activeRule: genActiveRule('/vue'),
        },
        {
          name: 'angular9',
          entry: '//localhost:7103',
          render,
          activeRule: genActiveRule('/angular9'),
        },
        {
          name: 'project',
          entry: '//localhost:8000',
          render,
          activeRule: genActiveRule('/project'),
        },
      ],
      {
        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);
          },
        ],
        afterUnmount: [
          app => {
            console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name);
          },
        ],
      },
    );
    
    1. 子项目中新增一个便于qiankun监听url变幻响应不同子项目的js文件,与app.vue文件同级别新增public-path.js文件
    if (window.__POWERED_BY_QIANKUN__) {
      // eslint-disable-next-line no-undef
      __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
    }
    
    1. 子项目main.js文件改造,暴露子项目的三个周期bootstrap(),mount(props),unmount()
    // ie polyfill
    import './public-path'
    import '@babel/polyfill'
    
    import Vue from 'vue'
    import App from './App.vue'
    import router from './router'
    import store from './store/'
    import { VueAxios } from './utils/request'
    
    // mock
    import './mock'
    
    import bootstraps from './core/bootstrap'
    import './core/use'
    
    // 去掉权限
    import './permission' // permission control
    
    import './utils/filter' // global filter
    
    Vue.config.productionTip = false
    
    // mount axios Vue.$http and this.$http
    Vue.use(VueAxios)
    let instance = null
    function render () {
      // router = new VueRouter({
      //   base: window.__POWERED_BY_QIANKUN__ ? '/project' : '/',
      //   mode: 'history',
      //   routes
      // })
    
      instance = new Vue({
        router,
        store,
        created () {
          bootstraps()
        },
        render: h => h(App)
      }).$mount('#app')
    }
    // new Vue({
    //   router,
    //   store,
    //   created () {
    //     bootstrap()
    //   },
    //   render: h => h(App)
    // }).$mount('#app')
    if (!window.__POWERED_BY_QIANKUN__) {
      render()
    }
    
    export async function bootstrap () {
      console.log('[vue] vue app bootstraped')
    }
    
    export async function mount (props) {
      console.log('[vue] props from main framework', props)
      render()
    }
    
    export async function unmount () {
      instance.$destroy()
      instance = null
      // router = null
    }
    
    
    1. 子项目router改造(router文件夹中的index.js中)
    export default new Router({
      // mode: 'history',
      base: window.__POWERED_BY_QIANKUN__ ? '/project' : '/',
      mode: 'history',
      scrollBehavior: () => ({
        y: 0
      }),
      routes: constantRouterMap.concat(asyncRouterMap)
    })
    
    1. 子项目中vue.config.js改造,注意此时的端口号得与之前主项目中的端口号一致
    const path = require('path')
    const webpack = require('webpack')
    const packageName = require('./package.json').name
    
    function resolve (dir) {
      return path.join(__dirname, dir)
    }
    
    // vue.config.js
    module.exports = {
      /*
        Vue-cli3:
        Crashed when using Webpack `import()` #2463
        https://github.com/vuejs/vue-cli/issues/2463
    
       */
      /*
      pages: {
        index: {
          entry: 'src/main.js',
          chunks: ['chunk-vendors', 'chunk-common', 'index']
        }
      },
      */
      outputDir: 'dist',
      assetsDir: 'static',
      filenameHashing: true,
      configureWebpack: {
        output: {
          library: `${packageName}-[name]`,
          libraryTarget: 'umd',
          jsonpFunction: `webpackJsonp_${packageName}`
        },
        plugins: [
          // Ignore all locale files of moment.js
          new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
        ]
      },
    
      chainWebpack: config => {
        config.resolve.alias
          .set('@$', resolve('src'))
          .set('@api', resolve('src/api'))
          .set('@assets', resolve('src/assets'))
          .set('@comp', resolve('src/components'))
          .set('@views', resolve('src/views'))
          .set('@layout', resolve('src/layout'))
          .set('@static', resolve('src/static'))
    
        const svgRule = config.module.rule('svg')
        svgRule.uses.clear()
        svgRule
          .oneOf('inline')
          .resourceQuery(/inline/)
          .use('vue-svg-icon-loader')
          .loader('vue-svg-icon-loader')
          .end()
          .end()
          .oneOf('external')
          .use('file-loader')
          .loader('file-loader')
          .options({
            name: 'assets/[name].[hash:8].[ext]'
          })
        /* svgRule.oneOf('inline')
          .resourceQuery(/inline/)
          .use('vue-svg-loader')
          .loader('vue-svg-loader')
          .end()
          .end()
          .oneOf('external')
          .use('file-loader')
          .loader('file-loader')
          .options({
            name: 'assets/[name].[hash:8].[ext]'
          })
        */
      },
    
      css: {
        loaderOptions: {
          less: {
            modifyVars: {
              /* less 变量覆盖,用于自定义 ant design 主题 */
              /*
              'primary-color': '#F5222D',
              'link-color': '#F5222D',
              'border-radius-base': '4px',
              */
            },
            javascriptEnabled: true
          }
        }
      },
    
      devServer: {
        // development server port 8000
        hot: true,
        disableHostCheck: true,
        overlay: {
          warnings: false,
          errors: true
        },
        headers: {
          'Access-Control-Allow-Origin': '*'
        },
        port: 8000
        // proxy: {
        //   '/api': {
        //     // target: 'https://mock.ihx.me/mock/5baf3052f7da7e07e04a5116/antd-pro',
        //     target: 'https://mock.ihx.me/mock/5baf3052f7da7e07e04a5116/antd-pro',
        //     ws: false,
        //     changeOrigin: true
        //   }
        // }
      },
    
      lintOnSave: undefined,
      // babel-loader no-ignore node_modules/*
      transpileDependencies: []
    }
    
    
    展开全文
  • 之所以会有这个思考,是因为看了一篇文章 基于 qiankun微前端最佳实践(图文并茂) - 应用间通信篇 ,其中有介绍使用 Shared 通信,精辟之处在于 // micro-app-vue/src/main.js //... /** * 渲染函数 * 主应用...

    注意:文中的父应用和主应用只是名称不同,但都指主应用基座。

    之所以会有这个思考,是因为看了一篇文章 基于 qiankun 的微前端最佳实践(图文并茂) - 应用间通信篇 ,其中有介绍使用 Shared 通信,精辟之处在于

    // micro-app-vue/src/main.js
    //...
    
    /**
     * 渲染函数
     * 主应用生命周期钩子中运行/子应用单独启动时运行
     */
    function render(props = {}) {
      // 当传入的 shared 为空时,使用子应用自身的 shared
      // 当传入的 shared 不为空时,主应用传入的 shared 将会重载子应用的 shared
      const { shared = SharedModule.getShared() } = props;
      SharedModule.overloadShared(shared);
    
      router = new VueRouter({
        base: window.__POWERED_BY_QIANKUN__ ? "/vue" : "/",
        mode: "history",
        routes,
      });
    
      // 挂载应用
      instance = new Vue({
        router,
        render: (h) => h(App),
      }).$mount("#app");
    }
    

    const { shared = SharedModule.getShared() } = props 利于解构赋值指定默认值的方式来实现父子应用共用同一个 shared ,看到这里我就想在 Vue 中我们一般使用 Vuex 进行数据集中状态处理,那我如果直接把父应用的 Vuex 创建的 store 传到子应用怎么样?

    直接将父应用 store 传给子应用

    详细代码可以查看 https://github.com/fxss5201/micro-app-test main 分支和 https://github.com/fxss5201/micro-app-test-vue main 分支:

    父应用:

    scr/micro/apps.ts

    import store from '@/store'
    
    const apps = [
      /**
       * name: 微应用名称 - 具有唯一性
       * entry: 微应用入口 - 通过该地址加载微应用
       * container: 微应用挂载节点 - 微应用加载完成后将挂载在该节点上
       * activeRule: 微应用触发的路由规则 - 触发路由规则后将加载该微应用
       */
      {
        name: 'VueMicroApp',
        entry: '//localhost:8111',
        container: '#frame',
        activeRule: '/vue',
        props: {
          // 此处将父应用的 store 传入子应用
          store
        }
      }
    ]
    
    export default apps
    

    store 是在父应用中通过 Vuex 创建的:

    src/store/index.ts

    import Vue from 'vue'
    import Vuex from 'vuex'
    
    Vue.use(Vuex)
    
    export default new Vuex.Store({
      state: {
        token: 'store123456'
      },
      mutations: {
        setToken (state, val) {
          state.token = val
        }
      },
      actions: {
      },
      modules: {
      }
    })
    

    之后就需要在子应用中进行接收处理。

    子应用:

    src/main.js

    import './public-path'
    import Vue from 'vue'
    import VueRouter from 'vue-router'
    import App from './App.vue'
    import routes from './router'
    import microStore from './store'
    import actions from '@/shared/actions'
    import './plugins/element.js'
    
    Vue.config.productionTip = false
    
    let router = null
    let instance = null
    function render (props = {}) {
      console.log('micro-app-test-vue')
      console.log(props)
      // 这里通过解构赋值方式把父应用创建的store传入
      const { container, store = microStore } = props
    
      if (props) {
        // 注入 actions 实例
        actions.setActions(props)
      }
    
      router = new VueRouter({
        base: window.__POWERED_BY_QIANKUN__ ? '/vue/' : '/',
        mode: 'history',
        routes
      })
    
      instance = new Vue({
        router,
        store,
        render: (h) => h(App)
      }).$mount(container ? container.querySelector('#app') : '#app')
    }
    
    // 独立运行时
    if (!window.__POWERED_BY_QIANKUN__) {
      render()
    }
    
    export async function bootstrap () {
      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
      router = null
    }
    

    在父应用中有4个菜单,分别如下:

      menus: [
        {
          key: 'Home',
          title: '主应用-主页',
          icon: 'el-icon-location',
          path: '/'
        },
        {
          key: 'About',
          title: '主应用-关于',
          icon: 'el-icon-location',
          path: '/about'
        },
        {
          key: 'VueMicroAppHome',
          title: 'Vue子应用-主页',
          icon: 'el-icon-menu',
          path: '/vue/'
        },
        {
          key: 'VueMicroAppAbout',
          title: 'Vue子应用-关于',
          icon: 'el-icon-menu',
          path: '/vue/about'
        }
      ]
    

    例子效果图
    点击按钮可切换值。

    父应用的 App.vue 文件:

    <template>
      <div>
        <el-container>
          <el-aside width="200px">
            <main-menu :menus="menus"></main-menu>
          </el-aside>
          <el-container>
            <el-header>
              <div>Header</div>
              <div class="head-content">
                <span>主应用Action通信:</span>
                <el-divider direction="vertical"></el-divider>
                <span>actionToken: {{ actionToken }}</span>
                <el-divider direction="vertical"></el-divider>
                <el-button size="mini" @click="setMainAtionToken">设置actionToken为mainAtionToken</el-button>
              </div>
              <div class="head-content">
                <span>主应用Props(vuex)通信:</span>
                <el-divider direction="vertical"></el-divider>
                <span>vuexToken: {{ vuexToken }}</span>
                <el-divider direction="vertical"></el-divider>
                <el-button size="mini" @click="setMainVuexToken">设置vuexToken为mainVuexToken</el-button>
              </div>
            </el-header>
            <el-main>
              <!-- 主应用渲染区,用于挂载主应用路由触发的组件 -->
              <router-view v-show="$route.name" />
    
              <!-- 子应用渲染区,用于挂载子应用节点 -->
              <section v-show="!$route.name" id="frame"></section>
            </el-main>
            <el-footer>Footer</el-footer>
          </el-container>
        </el-container>
      </div>
    </template>
    
    <script lang="ts">
    import { Component, Vue, Watch } from 'vue-property-decorator'
    import MainMenu from '@/components/MainMenu.vue'
    import actions from '@/shared/actions'
    
    @Component({
      components: {
        MainMenu
      }
    })
    
    export default class App extends Vue {
      /**
       * 菜单列表
       * key: 唯一 Key 值
       * title: 菜单标题
       * icon?: 图标
       * path: 菜单对应的路径
       */
      menus = [
        {
          key: 'Home',
          title: '主应用-主页',
          icon: 'el-icon-location',
          path: '/'
        },
        {
          key: 'About',
          title: '主应用-关于',
          icon: 'el-icon-location',
          path: '/about'
        },
        {
          key: 'VueMicroAppHome',
          title: 'Vue子应用-主页',
          icon: 'el-icon-menu',
          path: '/vue/'
        },
        {
          key: 'VueMicroAppAbout',
          title: 'Vue子应用-关于',
          icon: 'el-icon-menu',
          path: '/vue/about'
        }
      ]
    
      // 采用Action通信的Token
      actionToken = ''
    
      get vuexToken (): string {
        return this.$store.state.token
      }
    
      mounted (): void {
        console.log(this.menus)
    
        actions.onGlobalStateChange((state, prevState) => {
          // state: 变更后的状态; prevState: 变更前的状态
          console.log('主应用观察者:token 改变前的值为 ', prevState.token)
          console.log('主应用观察者:改变后的 token 的值为 ', state.token)
          this.actionToken = state.token
        }, true)
      }
    
      setMainAtionToken (): void {
        actions.setGlobalState({ token: 'mainAtionToken' })
      }
    
      setMainVuexToken (): void {
        this.$store.commit('setToken', 'mainVuexToken')
      }
    
      @Watch('vuexToken', { immediate: true })
      onVuexTokenChange (val: string, oldVal: string): void {
        // vuex中token值: val变更后的状态; oldVal: 变更前的状态
        console.log('主应用vuex中token值改变前的值为 ', oldVal)
        console.log('主应用vuex中token值改变后的值为 ', val)
      }
    }
    </script>
    

    子应用的 App.vue 文件:

    <template>
      <div id="app-box">
        <div>
          <div class="content">
            <span>子应用Action通信:</span>
            <el-divider direction="vertical"></el-divider>
            <span>actionToken: {{ actionToken }}</span>
            <el-divider direction="vertical"></el-divider>
            <el-button size="mini" @click="setMicroActionToken">设置actionToken为microActionToken</el-button>
          </div>
          <div class="content">
            <span>子应用Props(vuex)通信:</span>
            <el-divider direction="vertical"></el-divider>
            <span>vuexToken: {{ vuexToken }}</span>
            <el-divider direction="vertical"></el-divider>
            <el-button size="mini" @click="setMicroVuexToken">设置vuexToken为microVuexToken</el-button>
          </div>
          <router-link to="/">Home</router-link> |
          <router-link to="/about">About</router-link>
        </div>
        <router-view/>
      </div>
    </template>
    
    <script>
    import actions from '@/shared/actions'
    export default {
      name: 'app',
      data () {
        return {
          actionToken: '',
          vuexToken: ''
        }
      },
      mounted () {
        /**
          由于在 main.js 中 const { container, store = microStore } = props
          store 如果独立运行用的是自己的Store,如果作为子应用运行,则用的是主应用的Store,此时主应用的Store在子应用中
          不是动态的,所以只能初始化赋值,再订阅主应用Store的mutation来修改当前数据
    
          为了统一处理,可能都需要通过初始化赋值,再订阅主应用Store的mutation来修改当前数据的方式
        */
        // 初始化拿到值
        this.vuexToken = this.$store.state.token
    
        console.log(this.$store)
        console.log('window.__POWERED_BY_QIANKUN__', window.__POWERED_BY_QIANKUN__)
        if (window.__POWERED_BY_QIANKUN__) {
          actions.onGlobalStateChange((state, prevState) => {
            // state: 变更后的状态; prevState: 变更前的状态
            console.log('子应用观察者:token 改变前的值为 ', prevState.token)
            console.log('子应用观察者:改变后的 token 的值为 ', state.token)
    
            const { token } = state
            this.actionToken = token
          }, true)
        }
    
        // 订阅 store 的 mutation
        this.$store.subscribe((mutation, state) => {
          console.log(mutation.type)
          console.log(mutation.payload)
          console.log(state)
          console.log(this.$store)
          this.vuexToken = state.token
        })
      },
      methods: {
        setMicroActionToken () {
          if (window.__POWERED_BY_QIANKUN__) {
            actions.setGlobalState({ token: 'microActionToken' })
          }
        },
    
        setMicroVuexToken () {
          this.$store.commit('setToken', 'microVuexToken')
        }
      },
      watch: {
        vuexToken (val, oldVal) {
          console.log('子应用vuex中token值改变前的值为 ', oldVal)
          console.log('子应用vuex中token值改变后的值为 ', val)
        }
      }
    }
    </script>
    

    父子应用都有对应的按钮点击切换状态值得事件,并且也通过 qiankun 的官方通信方式 action 通信 和 propsVuex 创建的 store 的方式通信,propsVuex 创建的 store 通信方式就有点问题,由 propsVuex 创建的 store ,注意此时的 store 是在父应用中 Vue.use(Vuex)new 的,所以此时的 store 在子应用中无法使用 computed 计算属性,在子应用的 App.vue 文件也有对应的解释,暂时通过初始化赋值和订阅主应用 storemutation 来修改当前数据。这样虽然可以通信,但还是有点麻烦,而且后续也不清楚在子应用中使用主应用的 store 还会遇到哪些坑,所以就有了下面的思考。

    eventBus 通信

    详细代码可以查看
    https://github.com/fxss5201/micro-app-test bus 分支 或者 https://github.com/fxss5201/micro-app-test-js bus 分支 和
    https://github.com/fxss5201/micro-app-test-vue bus 分支:

    为啥新加了一个项目,https://github.com/fxss5201/micro-app-test 这个主应用基座采用的是 TypeScript ,而子应用基座采用的是 JavaScript ,导致有些文件不方便直接复制使用,所以就新建了 https://github.com/fxss5201/micro-app-test-js 项目,也是采用 JavaScript ,当然你也可以都用 TypeScript ,我这里只是做演示。

    下面以 https://github.com/fxss5201/micro-app-test-js bus 分支 和 https://github.com/fxss5201/micro-app-test-vue bus 分支作讲解,也就是使用 JavaScript ,TypeScript 分支可以自行查看 https://github.com/fxss5201/micro-app-test 项目,思想都是一致的。

    父应用:

    src/plugins/bus.js 创建 eventBus

    import Vue from 'vue'
    import store from './../store'
    
    // 使用 Event Bus
    const bus = new Vue({
      data: {
        // 保持初始化时与store数据一致
        // 基本类型可以这样用,引用类型请用cloneDeep深拷贝
        // https://www.lodashjs.com/docs/lodash.cloneDeep#_clonedeepvalue
        token: store.state.tokenModule.token
      }
    })
    
    export default bus
    

    src/plugins/busOn.js eventBus$on 统一放置的地方

    export default {
      install (thisArg) {
        thisArg.$bus.$on('setBusToken', (val) => {
          thisArg.$bus.token = val
          thisArg.$store.commit('tokenModule/setToken', val)
        })
      }
    }
    

    src/micro/apps.js

    import bus from './../plugins/bus'
    
    const apps = [
      /**
       * name: 微应用名称 - 具有唯一性
       * entry: 微应用入口 - 通过该地址加载微应用
       * container: 微应用挂载节点 - 微应用加载完成后将挂载在该节点上
       * activeRule: 微应用触发的路由规则 - 触发路由规则后将加载该微应用
       */
      {
        name: 'VueMicroApp',
        entry: '//localhost:8111',
        container: '#frame',
        activeRule: '/vue',
        props: {
          bus
        }
      }
    ]
    
    export default apps
    

    src/main.js

    import Vue from 'vue'
    import App from './App.vue'
    import router from './router'
    import store from './store'
    import './plugins/element.js'
    import startQiankun from './micro'
    import bus from './plugins/bus'
    
    startQiankun({ prefetch: false })
    Vue.config.productionTip = false
    // 将bus挂载在Vue原型,保持父子应用一致
    Vue.prototype.$bus = bus
    
    new Vue({
      router,
      store,
      render: h => h(App)
    }).$mount('#app')
    

    并且把在父子应用间通信的数据单独放到 Vuex 的一个 modules 中,所以父应用的 store 文件变成如下:

    src/store/index.js

    import Vue from 'vue'
    import Vuex from 'vuex'
    import tokenModule from './tokenModule'
    
    Vue.use(Vuex)
    
    export default new Vuex.Store({
      state: {
      },
      mutations: {
      },
      actions: {
      },
      modules: {
        tokenModule
      }
    })
    

    src/store/tokenModule.js

    export default {
      namespaced: true,
      state: {
        token: 'store123456'
      },
      mutations: {
        setToken (state, val) {
          console.log('main', val)
          state.token = val
        }
      },
      actions: {
      },
      modules: {
      }
    }
    

    之后就是父应用的 App.vue 的改造:

    <template>
      <div>
        <el-container>
          <el-aside width="200px">
            <main-menu :menus="menus"></main-menu>
          </el-aside>
          <el-container>
            <el-header>
              <div>Header</div>
              <div class="head-content">
                <span>主应用Action通信:</span>
                <el-divider direction="vertical"></el-divider>
                <span>actionToken: {{ actionToken }}</span>
                <el-divider direction="vertical"></el-divider>
                <el-button size="mini" @click="setMainAtionToken">设置actionToken为mainAtionToken</el-button>
              </div>
              <div class="head-content">
                <span>主应用Props(bus)通信:</span>
                <el-divider direction="vertical"></el-divider>
                <span>busToken: {{ busToken }}</span>
                <el-divider direction="vertical"></el-divider>
                <el-button size="mini" @click="setMainBusToken">设置busToken为mainBusToken</el-button>
              </div>
            </el-header>
            <el-main>
              <!-- 主应用渲染区,用于挂载主应用路由触发的组件 -->
              <router-view v-show="$route.name" />
    
              <!-- 子应用渲染区,用于挂载子应用节点 -->
              <section v-show="!$route.name" id="frame"></section>
            </el-main>
            <el-footer>Footer</el-footer>
          </el-container>
        </el-container>
      </div>
    </template>
    
    <script>
    import MainMenu from '@/components/MainMenu.vue'
    import actions from '@/shared/actions'
    import busOn from './plugins/busOn'
    
    export default {
      name: 'app',
      components: {
        MainMenu
      },
      data () {
        return {
          menus: [
            {
              key: 'Home',
              title: '主应用-主页',
              icon: 'el-icon-location',
              path: '/'
            },
            {
              key: 'About',
              title: '主应用-关于',
              icon: 'el-icon-location',
              path: '/about'
            },
            {
              key: 'VueMicroAppHome',
              title: 'Vue子应用-主页',
              icon: 'el-icon-menu',
              path: '/vue/'
            },
            {
              key: 'VueMicroAppAbout',
              title: 'Vue子应用-关于',
              icon: 'el-icon-menu',
              path: '/vue/about'
            }
          ],
          actionToken: ''
        }
      },
      computed: {
        busToken () {
          return this.$store.state.tokenModule.token
        }
      },
      mounted () {
        actions.onGlobalStateChange((state, prevState) => {
          // state: 变更后的状态; prevState: 变更前的状态
          console.log('主应用观察者:token 改变前的值为 ', prevState.token)
          console.log('主应用观察者:改变后的 token 的值为 ', state.token)
          this.actionToken = state.token
        }, true)
    
        // bus.$on('setBusToken', (val: string) => {
        //   this.$store.commit('tokenModule/setToken', val)
        // })
    
        // 多个eventBus统一书写地方
        busOn.install(this)
      },
      methods: {
        setMainAtionToken () {
          actions.setGlobalState({ token: 'mainAtionToken' })
        },
        setMainBusToken () {
          // 防止多次commit setToken,所以将commit setToken放在eventBus中去做,此处仅emit eventBus
          // this.$store.commit('tokenModule/setToken', 'mainBusToken')
          this.$bus.$emit('setBusToken', 'mainBusToken')
        }
      },
      watch: {
        busToken: {
          immediate: true,
          handler (val, oldVal) {
            // vuex中token值: val变更后的状态; oldVal: 变更前的状态
            console.log('主应用vuex中token值改变前的值为 ', oldVal)
            console.log('主应用vuex中token值改变后的值为 ', val)
          }
        }
      }
    }
    </script>
    

    接下来说说子应用的修改,子应用:

    看过项目的应该会发现父子应用 src/plugins/bus.jssrc/plugins/busOn.jssrc/store/tokenModule.js 这3个文件是一致的,这里是专门把两个用一致的思想,就是方便在父子应用间复制粘贴,所以父子应用要么全用 JavaScript 或者 typeScript 。

    src/main.js

    import './public-path'
    import Vue from 'vue'
    import VueRouter from 'vue-router'
    import App from './App.vue'
    import routes from './router'
    import store from './store'
    import actions from '@/shared/actions'
    import './plugins/element.js'
    import microBus from './plugins/bus'
    
    Vue.config.productionTip = false
    
    let router = null
    let instance = null
    function render (props = {}) {
      console.log('micro-app-test-vue')
      console.log(props)
      // 通过解构赋值默认值的方式,当父应用传了 bus 就应用父应用的 bus ,未传则应用子应用的 bus
      const { container, bus = microBus } = props
      Vue.prototype.$bus = bus
    
      if (props) {
        // 注入 actions 实例
        actions.setActions(props)
      }
    
      router = new VueRouter({
        base: window.__POWERED_BY_QIANKUN__ ? '/vue/' : '/',
        mode: 'history',
        routes
      })
    
      instance = new Vue({
        router,
        store,
        render: (h) => h(App)
      }).$mount(container ? container.querySelector('#app') : '#app')
    }
    
    // 独立运行时
    if (!window.__POWERED_BY_QIANKUN__) {
      render()
    }
    
    export async function bootstrap () {
      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
      router = null
    }
    

    src/App.vue

    <template>
      <div id="app-box">
        <div>
          <div class="content">
            <span>子应用Action通信:</span>
            <el-divider direction="vertical"></el-divider>
            <span>actionToken: {{ actionToken }}</span>
            <el-divider direction="vertical"></el-divider>
            <el-button size="mini" @click="setMicroActionToken">设置actionToken为microActionToken</el-button>
          </div>
          <div class="content">
            <span>子应用Props(bus)通信:</span>
            <el-divider direction="vertical"></el-divider>
            <span>busToken: {{ busToken }}</span>
            <el-divider direction="vertical"></el-divider>
            <el-button size="mini" @click="setMicroBusToken">设置busToken为microBusToken</el-button>
          </div>
          <router-link to="/">Home</router-link> |
          <router-link to="/about">About</router-link>
        </div>
        <router-view/>
      </div>
    </template>
    
    <script>
    import actions from '@/shared/actions'
    import busOn from './plugins/busOn'
    
    export default {
      name: 'app',
      data () {
        return {
          actionToken: ''
        }
      },
      computed: {
        busToken () {
          return this.$store.state.tokenModule.token
        }
      },
      mounted () {
        console.log('window.__POWERED_BY_QIANKUN__', window.__POWERED_BY_QIANKUN__)
        if (window.__POWERED_BY_QIANKUN__) {
          actions.onGlobalStateChange((state, prevState) => {
            // state: 变更后的状态; prevState: 变更前的状态
            console.log('子应用观察者:token 改变前的值为 ', prevState.token)
            console.log('子应用观察者:改变后的 token 的值为 ', state.token)
    
            const { token } = state
            this.actionToken = token
          }, true)
        }
    
        // 多个eventBus统一书写地方
        busOn.install(this)
        if (window.__POWERED_BY_QIANKUN__) {
          // 嵌入父应用中进入页面的时候初始化数据
          this.$bus.$emit('setBusToken', this.$bus.token)
        }
      },
      methods: {
        setMicroActionToken () {
          if (window.__POWERED_BY_QIANKUN__) {
            actions.setGlobalState({ token: 'microActionToken' })
          }
        },
    
        setMicroBusToken () {
          // 防止多次commit setToken,所以将commit setToken放在eventBus中去做,此处仅emit eventBus
          // this.$store.commit('tokenModule/setToken', 'microBusToken')
          this.$bus.$emit('setBusToken', 'microBusToken')
        }
      },
      watch: {
        busToken (val, oldVal) {
          console.log('子应用vuex中token值改变前的值为 ', oldVal)
          console.log('子应用vuex中token值改变后的值为 ', val)
        }
      }
    }
    </script>
    

    这种通信方式主要采用的就是 eventBus$emit 触发事件和 $on 监听事件,如果子应用单独使用,则使用子应用的 eventBus 去更改 store 中的值,如果子应用嵌在父应用中使用时,则使用父应用的 eventBus 。这种方案的好处就是子应用嵌入父应用及单独使用时不用做任何的适配处理。

    action + Vuex 通信

    详细代码可以查看
    https://github.com/fxss5201/micro-app-test-js action-vuex 分支(https://github.com/fxss5201/micro-app-test action-vuex 分支) 和
    https://github.com/fxss5201/micro-app-test-vue action-vuex 分支:

    action + Vuex 通信主要是使用官方的 action 进行通信,之后在将值更新到 vuex 中:

    主应用:

    src/App.vue

    <template>
      <div>
        <el-container>
          <el-aside width="200px">
            <main-menu :menus="menus"></main-menu>
          </el-aside>
          <el-container>
            <el-header>
              <div>Header</div>
              <div class="head-content">
                <span>主应用Action通信:</span>
                <el-divider direction="vertical"></el-divider>
                <span>actionToken: {{ actionToken }}</span>
                <el-divider direction="vertical"></el-divider>
                <el-button size="mini" @click="setMainAtionToken">设置actionToken为mainAtionToken</el-button>
              </div>
              <div class="head-content">
                <span>主应用Action + Vuex通信:</span>
                <el-divider direction="vertical"></el-divider>
                <span>actionVuexToken: {{ actionVuexToken }}</span>
                <el-divider direction="vertical"></el-divider>
                <el-button size="mini" @click="setMainActionVuexToken">设置actionVuexToken为mainActionVuexToken</el-button>
              </div>
            </el-header>
            <el-main>
              <!-- 主应用渲染区,用于挂载主应用路由触发的组件 -->
              <router-view v-show="$route.name" />
    
              <!-- 子应用渲染区,用于挂载子应用节点 -->
              <section v-show="!$route.name" id="frame"></section>
            </el-main>
            <el-footer>Footer</el-footer>
          </el-container>
        </el-container>
      </div>
    </template>
    
    <script>
    import MainMenu from '@/components/MainMenu.vue'
    import actions from '@/shared/actions'
    
    export default {
      name: 'app',
      components: {
        MainMenu
      },
      data () {
        return {
          menus: [
            {
              key: 'Home',
              title: '主应用-主页',
              icon: 'el-icon-location',
              path: '/'
            },
            {
              key: 'About',
              title: '主应用-关于',
              icon: 'el-icon-location',
              path: '/about'
            },
            {
              key: 'VueMicroAppHome',
              title: 'Vue子应用-主页',
              icon: 'el-icon-menu',
              path: '/vue/'
            },
            {
              key: 'VueMicroAppAbout',
              title: 'Vue子应用-关于',
              icon: 'el-icon-menu',
              path: '/vue/about'
            }
          ],
          actionToken: ''
        }
      },
      computed: {
        actionVuexToken () {
          return this.$store.state.tokenModule.token
        }
      },
      mounted () {
        actions.onGlobalStateChange((state, prevState) => {
          // state: 变更后的状态; prevState: 变更前的状态
          console.log('主应用观察者:token 改变前的值为 ', prevState.token)
          console.log('主应用观察者:改变后的 token 的值为 ', state.token)
    
          this.actionToken = state.token
          this.$store.commit('tokenModule/setToken', state.token)
        }, true)
      },
      methods: {
        setMainAtionToken () {
          actions.setGlobalState({ token: 'mainAtionToken' })
        },
        setMainActionVuexToken () {
          // 注意这里用actions去修改,在onGlobalStateChange中去commit
          actions.setGlobalState({ token: 'mainActionVuexToken' })
        }
      },
      watch: {
        actionVuexToken: {
          immediate: true,
          handler (val, oldVal) {
            // vuex中token值: val变更后的状态; oldVal: 变更前的状态
            console.log('主应用vuex中token值改变前的值为 ', oldVal)
            console.log('主应用vuex中token值改变后的值为 ', val)
          }
        }
      }
    }
    </script>
    

    子应用:

    src/App.vue

    <template>
      <div id="app-box">
        <div>
          <div class="content">
            <span>子应用Action通信:</span>
            <el-divider direction="vertical"></el-divider>
            <span>actionToken: {{ actionToken }}</span>
            <el-divider direction="vertical"></el-divider>
            <el-button size="mini" @click="setMicroActionToken">设置actionToken为microActionToken</el-button>
          </div>
          <div class="content">
            <span>子应用Action + Vuex通信:</span>
            <el-divider direction="vertical"></el-divider>
            <span>actionVuexToken: {{ actionVuexToken }}</span>
            <el-divider direction="vertical"></el-divider>
            <el-button size="mini" @click="setMicroActionVuexToken">设置actionVuexToken为microActionVuexToken</el-button>
          </div>
          <router-link to="/">Home</router-link> |
          <router-link to="/about">About</router-link>
        </div>
        <router-view/>
      </div>
    </template>
    
    <script>
    import actions from '@/shared/actions'
    
    export default {
      name: 'app',
      data () {
        return {
          actionToken: ''
        }
      },
      computed: {
        actionVuexToken () {
          return this.$store.state.tokenModule.token
        }
      },
      mounted () {
        console.log('window.__POWERED_BY_QIANKUN__', window.__POWERED_BY_QIANKUN__)
        if (window.__POWERED_BY_QIANKUN__) {
          actions.onGlobalStateChange((state, prevState) => {
            // state: 变更后的状态; prevState: 变更前的状态
            console.log('子应用观察者:token 改变前的值为 ', prevState.token)
            console.log('子应用观察者:改变后的 token 的值为 ', state.token)
    
            const { token } = state
            this.actionToken = token
            this.$store.commit('tokenModule/setToken', token)
          }, true)
        }
      },
      methods: {
        setMicroActionToken () {
          if (window.__POWERED_BY_QIANKUN__) {
            actions.setGlobalState({ token: 'microActionToken' })
          }
        },
    
        setMicroActionVuexToken () {
          if (window.__POWERED_BY_QIANKUN__) {
            actions.setGlobalState({ token: 'microActionVuexToken' })
          } else {
            this.$store.commit('tokenModule/setToken', 'microActionVuexToken')
          }
        }
      },
      watch: {
        actionVuexToken (val, oldVal) {
          console.log('子应用vuex中token值改变前的值为 ', oldVal)
          console.log('子应用vuex中token值改变后的值为 ', val)
        }
      }
    }
    </script>
    

    action + vuex 通信方式就是子应用需要根据 window.__POWERED_BY_QIANKUN__ 的值做一些适配,比如:

        setMicroActionVuexToken () {
          if (window.__POWERED_BY_QIANKUN__) {
            // 嵌在父应用中时使用 actions
            actions.setGlobalState({ token: 'microActionVuexToken' })
          } else {
            // 独立使用时commit
            this.$store.commit('tokenModule/setToken', 'microActionVuexToken')
          }
        }
    

    微应用建议使用 history 模式的路由,需要设置路由 base,值和它的 activeRule 是一样的。

    总结一下,上面的通信方式视情况而定,可以单独使用也可以混合使用。

    展开全文
  • 往期文章推荐 一篇文章教你搭建一个前后端分离(gitlab-cicd+docker+vue+django)的自动化部署的网站,干货满满! Docker入门,这一篇就够了 技术栈简介 微前端 ...微前端架构具备以下几个核心价值

    往期文章推荐

    一篇文章教你搭建一个前后端分离(gitlab-cicd+docker+vue+django)的自动化部署的网站,干货满满!

    Docker入门,这一篇就够了

    技术栈简介

    • 微前端
    • qiankun
    • docker 不了解的建议先看一下我之前的介绍,一看就明白
    • gitlab-ci/cd 这里是自动化部署的知识,可以了解一下
    • nginx

    建议配合视频解说更快理解

    什么是微前端

    微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。

    微前端架构具备以下几个核心价值:

    • 技术栈无关
      主框架不限制接入应用的技术栈,微应用具备完全自主权

    • 独立开发、独立部署
      微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新

    • 增量升级

      在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略

    • 独立运行时
      每个微应用之间状态隔离,运行时状态不共享

    什么是qiankun

    qiankun 是一个生产可用的微前端框架,它基于 single-spa,具备 js 沙箱、样式隔离、HTML Loader、预加载 等微前端系统所需的能力。qiankun 可以用于任意 js 框架,微应用接入像嵌入一个 iframe 系统一样简单。

    qiankun 的核心设计理念

    引用地址:https://qiankun.umijs.org/zh/guide

    • 简单

      由于主应用微应用都能做到技术栈无关,qiankun 对于用户而言只是一个类似 jQuery 的库,你需要调用几个 qiankun 的 API 即可完成应用的微前端改造。同时由于 qiankun 的 HTML entry 及沙箱的设计,使得微应用的接入像使用 iframe 一样简单。

    • 解耦/技术栈无关

      微前端的核心目标是将巨石应用拆解成若干可以自治的松耦合微应用,而 qiankun 的诸多设计均是秉持这一原则,如 HTML entry、沙箱、应用间通信等。这样才能确保微应用真正具备 独立开发、独立运行 的能力。

    为什么不用Iframe

    引用地址:https://www.yuque.com/kuitos/gky7yw/gesexv

    如果不考虑体验问题,iframe 几乎是最完美的微前端解决方案了。

    iframe 最大的特性就是提供了浏览器原生的硬隔离方案,不论是样式隔离、js 隔离这类问题统统都能被完美解决。但他的最大问题也在于他的隔离性无法被突破,导致应用间上下文无法被共享,随之带来的开发体验、产品体验的问题。

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

    其中有的问题比较好解决(问题1),有的问题我们可以睁一只眼闭一只眼(问题4),但有的问题我们则很难解决(问题3)甚至无法解决(问题2),而这些无法解决的问题恰恰又会给产品带来非常严重的体验问题, 最终导致我们舍弃了 iframe 方案。

    微前端的核心价值

    https://www.yuque.com/kuitos/gky7yw/rhduwc

    项目的构想

    在说具体技术实现前,我们先来看下我们想要实现个什么东西。

    微前端示意图

    image-20210705141310079.png
    主应用负责登录状态的管理和导航的显示

    子应用会根据主应用导航的点击而动态加载

    部署逻辑

    部署的思路有很多,我这里说说我尝试过的方式:

    • 只使用一个nginx容器,通过监听不同端口,部署多个应用,再在主应用的端口里面添加对应路由代理到子应用

      这种方式最简单但是不适合 gitlab-ci/cd 的自动化部署,所以我只是最初测试一下nginx部署微前端的实现

    • 使用多个nginx容器,每个容器暴露一个端口,再通过主应用添加对应路由代理到子应用

      这种方式可以实现,但是会在服务器暴露多个端口,安全性会降低,而且外部也可以通过端口直接访问子应用

    • 使用多个nginx容器,只暴露主应用的端口,主应用去连通子应用,然后通过nginx代理访问

      这种方式最理想,只需要暴露一个端口,所有代理都在容器间,对外是无感的,下面是实现的图示

    image-20210705142948593.png

    qiankun

    安装qiankun

    $ yarn add qiankun # 或者 npm i qiankun -S
    

    在主应用中注册微应用

    import { registerMicroApps, addGlobalUncaughtErrorHandler, start } from 'qiankun';
    
    const apps = [
      {
        name: 'ManageMicroApp',
        entry: '/system/', // 本地开发的时候使用 //localhost:子应用端口
        container: '#frame',
        activeRule: '/manage',
      },
    ]
    
    /**
     * 注册微应用
     * 第一个参数 - 微应用的注册信息
     * 第二个参数 - 全局生命周期钩子
     */
    registerMicroApps(apps,{
      // qiankun 生命周期钩子 - 微应用加载前
      beforeLoad: (app: any) => {
        console.log("before load", app.name);
        return Promise.resolve();
      },
      // qiankun 生命周期钩子 - 微应用挂载后
      afterMount: (app: any) => {
        console.log("after mount", app.name);
        return Promise.resolve();
      },
    });
    
    /**
     * 添加全局的未捕获异常处理器
     */
    addGlobalUncaughtErrorHandler((event: Event | string) => {
      console.error(event);
      const { message: msg } = event as any;
      // 加载失败时提示
      if (msg && msg.includes("died in status LOADING_SOURCE_CODE")) {
        console.error("微应用加载失败,请检查应用是否可运行");
      }
    });
    
    start();
    

    当微应用信息注册完之后,一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑,所有 activeRule 规则匹配上的微应用就会被插入到指定的 container 中,同时依次调用微应用暴露出的生命周期钩子。

    如果微应用不是直接跟路由关联的时候,你也可以选择手动加载微应用的方式:

    import { loadMicroApp } from 'qiankun';
    
    
    loadMicroApp({
      name: 'app',
      entry: '//localhost:7100',
      container: '#yourContainer',
    });
    

    微应用

    微应用不需要额外安装任何其他依赖即可接入 qiankun 主应用。

    1. 导出相应的生命周期钩子

    微应用需要在自己的入口 js (通常就是你配置的 webpack 的 entry js) 导出 bootstrapmountunmount 三个生命周期钩子,以供主应用在适当的时机调用。

    import Vue from 'vue';
    import VueRouter from 'vue-router';
    
    import './public-path';
    import App from './App.vue';
    import routes from './routes';
    import SharedModule from '@/shared'; 
    
    Vue.config.productionTip = false;
    
    let instance = null;
    let router = null;
    // 如果子应用独立运行则直接执行render
    if (!window.__POWERED_BY_QIANKUN__) {
      render();
    }
    
    /**
     * 渲染函数
     * 主应用生命周期钩子中运行/子应用单独启动时运行
     */
    function render(props = {}) {
      // SharedModule用于主应用于子应用的通讯
      // 当传入的 shared 为空时,使用子应用自身的 shared
      // 当传入的 shared 不为空时,主应用传入的 shared 将会重载子应用的 shared
      const { shared = SharedModule.getShared() } = props;
      SharedModule.overloadShared(shared);
    
      router = new VueRouter({
        base: window.__POWERED_BY_QIANKUN__ ? '/manage/' : '/',
        mode: 'history',
        routes
      });
    
      // 挂载应用
      instance = new Vue({
        router,
        render: (h) => h(App)
      }).$mount('#app');
    }
    
    /**
     * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
     * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
     */
    export async function bootstrap() {
      console.log('vue app bootstraped');
    }
    /**
     * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
     */
    export async function mount(props) {
      console.log('vue mount', props);
      render(props);
    }
    /**
     * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
     */
    export async function unmount() {
      console.log('vue unmount');
      instance.$destroy();
      instance = null;
      router = null;
    }
    /**
     * 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
     */
    export async function update(props) {
      console.log('update props', props);
    }
    

    上述代码中还引用了一个public-path的文件:

    if (window.__POWERED_BY_QIANKUN__) {
      // eslint-disable-next-line no-undef
      __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
    }
    

    这个主要解决的是微应用动态载入的 脚本、样式、图片 等地址不正确的问题。

    2. 配置微应用的打包工具

    除了代码中暴露出相应的生命周期钩子之外,为了让主应用能正确识别微应用暴露出来的一些信息,微应用的打包工具需要增加如下配置:

    webpack:

    const packageName = require('./package.json').name;
    
    
    module.exports = {
      publicPath: '/system/', //这里打包地址都要基于主应用的中注册的entry值
      output: {
        library: 'ManageMicroApp', // 库名,与主应用注册的微应用的name一致
        libraryTarget: 'umd', // 这个选项会尝试把库暴露给前使用的模块定义系统,这使其和CommonJS、AMD兼容或者暴露为全局变量。
        jsonpFunction: `webpackJsonp_${packageName}`,
      },
    };
    

    关键点总结

    • 主应用注册时的配置

      const apps = [
        {
          name: 'ManageMicroApp',
          entry: '/system/',  // http://localhost/system/ 这里会通过nginx代理指向对应的子应用地址
          container: '#frame',
          activeRule: '/manage',
        },
      ]
      

      主应用注册微应用时,entry 可以为相对路径,activeRule 不可以和 entry 一样(否则主应用页面刷新就变成微应用)

    • vue路由的base

      router = new VueRouter({
        base: window.__POWERED_BY_QIANKUN__ ? '/manage/' : '/',
        mode: 'history',
        routes
      });
      

      如果是主应用调用的那么路由的base为/manage/

    • webpack打包配置

      module.exports = {
        publicPath: '/system/',
      };
      

      对于 webpack 构建的微应用,微应用的 webpack 打包的 publicPath 需要配置成 /system/,否则微应用的 index.html 能正确请求,但是微应用 index.html 里面的 js/css 路径不会带上 /system/

    到这里我们把微前端的配置做好了,接下来就是nginx的配置。

    生产环境Nginx配置

    先把主应用的nginx配置挂一下

        server {
            listen       80;
            listen       [::]:80 default_server;
            server_name  localhost;
            root         /usr/share/nginx/html;
    
            location / {
                try_files $uri $uri/ /index.html;
                index index.html;
            }
    				# 前面我们配置的子应用entry是/system/,所以会触发这里的代理,代理到对应的子应用
            location /system/ {
        				 # -e表示只要filename存在,则为真,不管filename是什么类型,当然这里加了!就取反
                 if (!-e $request_filename) {
                    proxy_pass http://192.168.1.2; # 这里的ip是子应用docker容器的ip
                 }
        				 # -f filename 如果 filename为常规文件,则为真
                 if (!-f $request_filename) {
                    proxy_pass http://192.168.1.2;
                 }
                 # docker运行的nginx不识别localhost的 所以这种写法会报502
                 # proxy_pass  http://localhost:10200/;
                 proxy_set_header Host $host;
             }
    
            location /api/ {
                proxy_pass http://后台地址IP/;
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header REMOTE-HOST $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            }
    
            error_page 404 /404.html;
                location = /40x.html {
            }
    
            error_page 500 502 503 504 /50x.html;
                location = /50x.html {
            }
        }
    

    再看一下子应用的

    server {
        listen       80;
        listen       [::]:80 default_server;
        server_name  _2;
        root         /usr/share/nginx/html;
    
        # 这里必须加上允许跨域,否则主应用无法访问
        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';
    
        location / {
            try_files $uri $uri/ /index.html;
            index index.html;
        }
    
        location /api/ {
            proxy_pass http://后台地址IP/;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header REMOTE-HOST $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    
        error_page 404 /404.html;
            location = /40x.html {
        }
    
        error_page 500 502 503 504 /50x.html;
            location = /50x.html {
        }
    }
    

    dockerfile配置

    这里先看一下子应用的

    # 直接使用nginx镜像
    FROM nginx
    # 把上面配置的conf文件替换一下默认的
    COPY nginx.conf /etc/nginx/nginx.conf
    # nginx默认目录下需要能看见index.html文件
    COPY dist/index.html /usr/share/nginx/html/index.html
    # 再回头看一下部署逻辑图和qiankun注意点,必须要把所有的资源文件放到system文件下index.html才能正确加载
    COPY dist /usr/share/nginx/html/system
    

    再看一下主应用的

    # 这里主应用没有直接使用nginx,因为nginx反向代理的/api/会出现404的问题,原因未知!
    FROM centos
    # 安装nginx
    RUN yum install -y nginx
    # 跳转到/etc/nginx
    WORKDIR /etc/nginx
    # 替换配置文件
    COPY nginx.conf nginx.conf
    # 跳转到/usr/share/nginx/html
    WORKDIR /usr/share/nginx/html
    # 主应用正常打包,所以直接把包放进去就行
    COPY dist .
    # 暴露80端口
    EXPOSE 80
    # 运行nginx
    CMD nginx -g "daemon off;"
    

    gitlab-ci/cd配置

    先看一下子应用的,只说重点的

    image: node
    
    stages:
      - install
      - build
      - deploy
      - clear
    
    cache:
      key: modules-cache
      paths:
        - node_modules
        - dist
    
    安装环境:
      stage: install
      tags:
        - vue
      script:
        - npm install yarn
        - yarn install
    
    打包项目:
      stage: build
      tags:
        - vue
      script:
        - yarn build
    
    部署项目:
      stage: deploy
      image: docker
      tags:
        - vue
      script:
      	# 通过dockerfile构建项目的镜像
        - docker build -t rainbow-system .
        # 如果存在之前创建的容器先删除
        - if [ $(docker ps -aq --filter name=rainbow-admin-system) ];then docker rm -f rainbow-admin-system;fi
        # 通过刚刚的镜像创建一个容器 给容器指定一个网卡rainbow-net,这个网卡是我们自定义,创建方式后面会说,然后给定一个ip
        - docker run -d --net rainbow-net --ip 192.168.1.2 --name rainbow-admin-system rainbow-system
    
    清理docker:
      stage: clear
      image: docker
      tags:
        - vue
      script:
        - if [ $(docker ps -aq | grep "Exited" | awk '{print $1 }') ]; then docker stop $(docker ps -a | grep "Exited" | awk '{print $1 }');fi
        - if [ $(docker ps -aq | grep "Exited" | awk '{print $1 }') ]; then docker rm $(docker ps -a | grep "Exited" | awk '{print $1 }');fi
        - if [ $(docker images | grep "none" | awk '{print $3}') ]; then docker rmi $(docker images | grep "none" | awk '{print $3}');fi
    

    再看一下主应用的,省略重复的,直接看重点

    部署项目:
      stage: deploy
      image: docker
      tags:
        - vue3
      script:
        - docker build -t rainbow-admin .
        - if [ $(docker ps -aq --filter name=rainbow-admin-main) ];then docker rm -f rainbow-admin-main;fi
        # 给容器指定一个网卡rainbow-net,然后给定一个ip,然后通过--link与之前创建的子应用连通,重点!
        - docker run -d -p 80:80 --net rainbow-net --ip 192.168.1.1 --link 192.168.1.2 --name rainbow-admin-main rainbow-admin
    

    上面说到了docker的自定义网卡,生成的命令如下:

    $ docker network create --driver bridge --subnet 192.168.0.0/16 --gateway 192.168.0.1 rainbow-net
    

    总结

    到这里我们已经实现了qiankun+docker配合gitlab-ci/cd的自动化部署,中间遇到很多坑,然后走出了一条相对合理的解决方案,有问题欢迎讨论。

    展开全文
  • (给前端大全加星标,提升前端技能)作者:zxh1307https://juejin.im/post/5ea55417e51d4546e347fda9导语最近在做微前端的项目 , 过程中真是踩了不少坑 , 在有限的资料中不断试错 , 默默无语两行泪 哈哈. 在此次将踩坑...

    (给前端大全加星标,提升前端技能)

    作者:zxh1307

    https://juejin.im/post/5ea55417e51d4546e347fda9

    导语

    最近在做微前端的项目 , 过程中真是踩了不少坑 , 在有限的资料中不断试错 , 默默无语两行泪 哈哈.  在此次将踩坑部分都记录下来, 让更多的人少走点弯路 ,   此项目使用 蚂蚁金服qiankun 为基础作为开发 . 话不多说 开讲 !!!

    那什么是 qiankun 呢

    qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。

    什么是微前端

    微前端架构具备以下几个核心价值:

    • 技术栈无关

      主框架不限制接入应用的技术栈,微应用具备完全自主权

    • 独立开发、独立部署

      微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新

    • 增量升级

      在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略

    • 独立运行时

      每个微应用之间状态隔离,运行时状态不共享

    摘自 qiankun官方文档

    主应用配置

    此次项目 主应用与 子应用均为 vue ,

    下载 qiankun

    npm install qiankun

    在主应用中注册微应用

    // 导入乾坤函数

    封装 render 方法

    此方法在main.js 中要初始调用一次, 主要用来挂载主应用 , 之后子应用分别依次调用 ,所以故作判断. 传入的参数分别为 子应用 的 HTML 和 加载状态 content 字段 我们用 vuex 存储 起来,方便使用


    let app = null;

    function render({ appContent, loading }{
      if (!app) {
        app = new Vue({
          router,
          store,
          renderh => h(App),
        }).$mount('#app');
        
      } else {
        store.commit('microApp/changeCenter', appContent);
        store.commit('microApp/changeLoading', loading);
      }

    }

    微应用注册

    下文中的apps 可以为获取后数据 , 注册微应用 本文案例比较简单,方便大家理解 ,

    在注册自应用的参数 ** container 与 render** 踩坑比较多,下边会着重讲解.


    function genActiveRule(routerPrefix{
      return location => location.pathname.startsWith(routerPrefix);
    }

    //传递给子应用的数据
    let msg = {
    ![](https://user-gold-cdn.xitu.io/2020/4/27/171bbc5de042ec98?w=1811&h=959&f=gif&s=4951066)
      data:'修炼爱情的辛酸,学会放好以前的渴望'
    }

    let apps = [
      {
        name'linjunjie'
        entry'//localhost:215',  // 改成自己子应用的端口号
        container:'#subView'//节点 id   //  沙盒模式 
        // render:render,  // 普通模式   
        activeRule: genActiveRule('/star'),
        props:msg
      }
    ]
       //注册的子应用 参数为数组
    registerMicroApps(apps,{
      beforeLoad: [
        app => {
          console.log(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);
        },
      ],
      afterUnmount: [
        app => {
          console.log('[LifeCycle] after unmount %c%s''color: green;', app.name);
        },
      ],
    });


    setDefaultMountApp('/star/linjunjie')

    //开启沙盒模式
    start({ 
       sandbox :{strictStyleIsolationtrue}
    })

    当微应用信息注册完之后,一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑,所有 activeRule 规则匹配上的微应用就会被插入到指定的 container 中,同时依次调用微应用暴露出的生命周期钩子。

    主应用为子应用准备的 展示元素

    <div id="app"><div id="nav"><div @click="onChangePage('/star/linjunjie')" >林俊杰div><div @click="onChangePage('/star/zhangyixin')" >张艺兴div>div><div id="subView" class="sub-content-wrap" v-html="content">div>div>template><script>import { mapState } from 'vuex';export default{
        data(){return {
          }
        },computed:{//获取子应用HTML 数据
           ...mapState('microApp', ['content']),
           ...mapState('microApp', ['mircoAppLoading']),
        },methods:{//定义跳转方法
          onChangePage(url){console.log(url)this.routerGo(url, '我喜爱的男明星')
          },
          routerGo(href = '/', title = null, stateObj = {}) {window.history.pushState(stateObj, title, href); 
          },
        }
     }
    script>

    子应用配置

    关于子应用的配置相对较简单 , 不需要额外下载qiankun 主要将生命钩子 导出即可

    导出响应的生命钩子

    导出 bootstrapmountunmount 三个生命周期钩子,以供主应用在适当的时机调用。注意,实例化路由时,判断当运行在qiankun环境时,路由要添加前缀,前缀与主应用注册子应用函数genActiveRule("/subdemo")内的参数一致

    'star' 值需要与主应用的值对应 genActiveRule("/star") 中的值需要商定好 主应用与微应用都要使用

    如果 new VueRouter 不在main.js  中 配置 ,请将此配置移动到 main.js  方便管理

    import routes 

    配置微应用的打包工具

    除了代码中暴露出相应的生命周期钩子之外,为了让主应用能正确识别微应用暴露出来的一些信息,微应用的打包工具需要在vue.config.js 中 增加如下配置:

    const packageName = require('./package.json').name;

    module.exports = {
    output: {
    library: `${packageName}-[name]`,
    libraryTarget: 'umd',
    jsonpFunction: `webpackJsonp_${packageName}`,
    },
    };

    子应用判断

    子应用中新建 publicPath.js  在main.js 引入
    if (

    处理 资源加载问题

    配置 vue.config.js

    module.exports = {

    vue.config.js 完整配置

    const path = 

    踩坑记录

    当前页面为子应用时, 刷新页面404

    以下方式均为主应用配置

    • 方式一 删除 mode 配置项

      mode: 'history', //   将此配置代码删除
    • 方式二 配置404  页面

    如果没有注释掉mode: 'history'  此参数 将404 页面重新导向  home首页

    {
        path'*',
        name'indexNotFound',
        componentresolve => require(['@/components/home'], resolve),
        children: HomeChild,
    },

    子应用 样式隔离 开始沙箱模式 遇到的问题

    • 主应用配置sandbox :{strictStyleIsolation: true}渲染模式由 render 模式 改为 containercontainer:'#subView', 此时 子应用的 挂载 dom  为    谨记主 container :#+id

    • 子应用配置 上文有提到  主要代码 截取

    new Vue({

    遇到的问题: 开启沙箱模式,如果是 采用 render 模式会报错 ,故选择container 模式

    效果图

    写到这里,项目已经构建完成了 让我们来看看效果吧

    这里是完整代码 方便大家学习 代码github地址:https://github.com/zxh1307/qiankun-vue

    项目问题

    • 为啥我项目启动后看不到子应用的效果

      将master 主应用 main.js 中 注册的 子应用的端口号 改成自己项目的端口号即可

    结语

    开发中还有其他坑 忘记记录了, 千万记得项目部署子应用资源跨域的问题 , 需要Nginx配置跨域问题

    推荐阅读  点击标题可跳转

    万字解析微前端、微前端框架qiankun以及源码

    微前端框架是怎么导入加载子应用的

    了解什么是微前端

    觉得本文对你有帮助?请分享给更多人

    关注「前端大全」加星标,提升前端技能

    8a7f4837b811e8bf4b4c909fdcd452bf.png

    好文章,我在看❤️

    展开全文

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 996
精华内容 398
关键字:

微前端qiankun