乐趣区

关于微前端:微前端qiankun从搭建到部署的实践

最近负责的新我的项目用到了qiankun,写篇文章分享下实战中遇到的一些问题和思考。

示例代码:https://github.com/fengxianqi/qiankun-example。

在线 demo:http://qiankun.fengxianqi.com/

独自拜访在线子利用:

  • subapp/sub-vue
  • subapp/sub-react

为什么要用 qiankun

我的项目有个性能需要是须要内嵌公司外部的一个现有工具,该工具是独立部署的且是用 React 写的,而咱们的我的项目次要技术选型是vue,因而须要思考嵌入页面的计划。次要有两条路:

  • iframe计划
  • qiankun微前端计划

两种计划都能满足咱们的需要且是可行的。不得不说,iframe计划尽管一般但很实用且老本也低,iframe计划能笼罩大部分的微前端业务需要,而 qiankun 对技术要求更高一些。

技术同学对本身的成长也是有强烈需要的,因而在两者都能满足业务需要时,咱们更心愿能利用一些较新的技术,折腾一些未知的货色,因而咱们决定选用qiankun

我的项目架构

后盾零碎个别都是高低或左右的布局。下图粉红色是基座,只负责头部导航,绿色是挂载的整个子利用,点击头部导航可切换子利用。

参考官网的 examples 代码,我的项目根目录下有基座 main 和其余子利用sub-vuesub-react,搭建后的初始目录构造如下:


├── common     // 公共模块
├── main       // 基座
├── sub-react  // react 子利用
└── sub-vue    // vue 子利用

基座是用 vue 搭建,子利用有 reactvue

基座配置

基座 main 采纳是的 Vue-Cli3 搭建的,它只负责导航的渲染和登录态的下发,为子利用提供一个挂载的容器 div,基座应该放弃简洁(qiankun 官网 demo 甚至间接应用原生 html 搭建),不应该做波及业务的操作。

qiankun 这个库只须要在基座引入,在 main.js 中注册子利用, 为了方便管理,咱们将子利用的配置都放在:main/src/micro-app.js下。

const microApps = [
  {
    name: 'sub-vue',
    entry: '//localhost:7777/',
    activeRule: '/sub-vue',
    container: '#subapp-viewport', // 子利用挂载的 div
    props: {routerBase: '/sub-vue' // 下发路由给子利用,子利用依据该值去定义 qiankun 环境下的路由}
  },
  {
    name: 'sub-react',
    entry: '//localhost:7788/',
    activeRule: '/sub-react',
    container: '#subapp-viewport', // 子利用挂载的 div
    props: {routerBase: '/sub-react'}
  }
]

export default microApps

而后在 src/main.js 中引入

import Vue from 'vue';
import App from './App.vue';
import {registerMicroApps, start} from 'qiankun';
import microApps from './micro-app';

Vue.config.productionTip = false;

new Vue({render: h => h(App),
}).$mount('#app');


registerMicroApps(microApps, {
  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 中,须要申明 micro-app.js 配置的子利用挂载 div(留神 id 肯定要统一),以及基座布局相干的,大略这样:

<template>
  <div id="layout-wrapper">
    <div class="layout-header"> 头部导航 </div>
    <div id="subapp-viewport"></div>
  </div>
</template>

这样,基座就算配置实现了。我的项目启动后,子利用将会挂载到 <div id="subapp-viewport"></div> 中。

子利用配置

一、vue 子利用

用 Vue-cli 在我的项目根目录新建一个 sub-vue 的子利用,子利用的名称最好与父利用在 src/micro-app.js 中配置的名称统一(这样能够间接应用 package.json 中的 name 作为 output)。

  1. 新增 vue.config.js,devServer 的端口改为与主利用配置的统一, 且加上跨域headersoutput配置。
// package.json 的 name 需注意与主利用统一
const {name} = require('../package.json')

module.exports = {
  configureWebpack: {
    output: {library: `${name}-[name]`,
      libraryTarget: 'umd',
      jsonpFunction: `webpackJsonp_${name}`,
    }
  },
  devServer: {
    port: process.env.VUE_APP_PORT, // 在.env 中 VUE_APP_PORT=7788,与父利用的配置统一
    headers: {'Access-Control-Allow-Origin': '*' // 主利用获取子利用时跨域响应头}
  }
}
  1. 新增src/public-path.js
(function() {if (window.__POWERED_BY_QIANKUN__) {if (process.env.NODE_ENV === 'development') {
      // eslint-disable-next-line no-undef
      __webpack_public_path__ = `//localhost:${process.env.VUE_APP_PORT}/`;
      return;
    }
    // eslint-disable-next-line no-undef
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
  }
})();
  1. src/router/index.js改为只裸露 routes,new Router改到 main.js 中申明。
  2. 革新main.js,引入下面的public-path.js,改写 render,增加生命周期函数等,最终如下:
import './public-path' // 留神须要引入 public-path
import Vue from 'vue'
import App from './App.vue'
import routes from './router'
import store from './store'
import VueRouter from 'vue-router'

Vue.config.productionTip = false
let instance = null

function render (props = {}) {const { container, routerBase} = props
  const router = new VueRouter({
    base: window.__POWERED_BY_QIANKUN__ ? routerBase : process.env.BASE_URL,
    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
}

至此,根底版本的 vue 子利用配置好了,如果 routervuex不需用到,能够去掉。

二、react 子利用

  1. 通过 npx create-react-app sub-react 新建一个 react 利用。
  2. 新增 .env 文件增加 PORT 变量,端口号与父利用配置的保持一致。
  3. 为了不 eject 所有 webpack 配置,咱们用 react-app-rewired 计划复写 webpack 就能够了。
  • 首先npm install react-app-rewired --save-dev
  • 新建sub-react/config-overrides.js
const {name} = require('./package.json');

module.exports = {webpack: function override(config, env) {
    // 解决主利用接入后会挂掉的问题:https://github.com/umijs/qiankun/issues/340
    config.entry = config.entry.filter((e) => !e.includes('webpackHotDevClient')
    );
    
    config.output.library = `${name}-[name]`;
    config.output.libraryTarget = 'umd';
    config.output.jsonpFunction = `webpackJsonp_${name}`;
    return config;
  },
  devServer: (configFunction) => {return function (proxy, allowedHost) {const config = configFunction(proxy, allowedHost);
      config.open = false;
      config.hot = false;
      config.headers = {'Access-Control-Allow-Origin': '*',};
      return config;
    };
  },
};
  1. 新增src/public-path.js
if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
  1. 革新index.js, 引入public-path.js,增加生命周期函数等。
import './public-path'
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

function render() {
  ReactDOM.render(
    <App />,
    document.getElementById('root')
  );
}

if (!window.__POWERED_BY_QIANKUN__) {render();
}

/**
 * bootstrap 只会在微利用初始化的时候调用一次,下次微利用从新进入时会间接调用 mount 钩子,不会再反复触发 bootstrap。* 通常咱们能够在这里做一些全局变量的初始化,比方不会在 unmount 阶段被销毁的利用级别的缓存等。*/
export async function bootstrap() {console.log('react app bootstraped');
}
/**
 * 利用每次进入都会调用 mount 办法,通常咱们在这里触发利用的渲染办法
 */
export async function mount(props) {console.log(props);
  render();}
/**
 * 利用每次 切出 / 卸载 会调用的办法,通常在这里咱们会卸载微利用的利用实例
 */
export async function unmount() {ReactDOM.unmountComponentAtNode(document.getElementById('root'));
}
/**
 * 可选生命周期钩子,仅应用 loadMicroApp 形式加载微利用时失效
 */
export async function update(props) {console.log('update props', props);
}

serviceWorker.unregister();

至此,根底版本的 react 子利用配置好了。

进阶

全局状态治理

qiankun通过 initGlobalState, onGlobalStateChange, setGlobalState 实现主利用的全局状态治理,而后默认会通过 props 将通信办法传递给子利用。先看下官网的示例用法:

主利用:

// main/src/main.js
import {initGlobalState} from 'qiankun';
// 初始化 state
const initialState = {user: {} // 用户信息
};
const actions = initGlobalState(initialState);
actions.onGlobalStateChange((state, prev) => {
  // state: 变更后的状态; prev 变更前的状态
  console.log(state, prev);
});
actions.setGlobalState(state);
actions.offGlobalStateChange();

子利用:

// 从生命周期 mount 中获取通信办法,props 默认会有 onGlobalStateChange 和 setGlobalState 两个 api
export function mount(props) {props.onGlobalStateChange((state, prev) => {
    // state: 变更后的状态; prev 变更前的状态
    console.log(state, prev);
  });
  props.setGlobalState(state);
}

这两段代码不难理解,父子利用通过 onGlobalStateChange 这个办法进行通信,这其实是一个公布 - 订阅的设计模式。

ok,官网的示例用法很简略也齐全够用,纯 JavaScript 的语法,不波及任何的 vue 或 react 的货色,开发者可自在定制。

如果咱们间接应用官网的这个示例,那么数据会比拟涣散且调用简单,所有子利用都得申明 onGlobalStateChange 对状态进行监听,再通过 setGlobalState 进行更新数据。

因而,咱们很有必要 对数据状态做进一步的封装设计。笔者这里次要思考以下几点:

  • 主利用要放弃简洁简略,对子利用来说,主利用下发的数据就是一个很纯正的object,以便更好地反对不同框架的子利用,因而主利用不需用到vuex
  • vue 子利用要做到能继承父利用下发的数据,又反对独立运行。

子利用在 mount 申明周期能够获取到最新的主利用下发的数据,而后将这份数据注册到一个名为 global 的 vuex module 中,子利用通过 global module 的 action 动作进行数据的更新,更新的同时主动同步回父利用。

因而,对子利用来说,它不必晓得本人是一个 qiankun 子利用还是一个独立利用,它只是有一个名为 global 的 module,它可通过 action 更新数据,且不再须要关怀是否要同步到父利用 (同步的动作会封装在办法外部,调用者不需关怀),这也是为前面 反对子利用独立启动开发做筹备

  • react 子利用同理(笔者 react 用得不深就不说了)。

主利用的状态封装

主利用保护一个 initialState 的初始数据,它是一个 object 类型,会下发给子利用。

// main/src/store.js

import {initGlobalState} from 'qiankun';
import Vue from 'vue'

// 父利用的初始 state
// Vue.observable 是为了让 initialState 变成可响应:https://cn.vuejs.org/v2/api/#Vue-observable。let initialState = Vue.observable({user: {},
});

const actions = initGlobalState(initialState);

actions.onGlobalStateChange((newState, prev) => {
  // state: 变更后的状态; prev 变更前的状态
  console.log('main change', JSON.stringify(newState), JSON.stringify(prev));

  for (let key in newState) {initialState[key] = newState[key]
  }
});

// 定义一个获取 state 的办法下发到子利用
actions.getGlobalState = (key) => {
  // 有 key,示意取 globalState 下的某个子级对象
  // 无 key,示意取全副
  return key ? initialState[key] : initialState
}

export default actions;

这里有两个留神的中央:

  • Vue.observable是为了让父利用的 state 变成可响应式,如果不必 Vue.observable 包一层,它就只是一个纯正的 object,子利用也能获取到,但会失去响应式,意味着数据扭转后,页面不会更新
  • getGlobalState办法,这个是 有争议 的,大家在 github 上有探讨:https://github.com/umijs/qiankun/pull/729。

一方面,作者认为 getGlobalState 不是必须的,onGlobalStateChange其实曾经够用。

另一方面,笔者和其余提 pr 的同学感觉有必要提供一个 getGlobalState 的 api,理由是 get 办法更方便使用,子利用有需要是不需始终监听 stateChange 事件,它只须要在首次 mount 时通过 getGlobalState 初始化一次即可。在这里,笔者先坚持己见让父利用下发一个 getGlobalState 的办法。

因为官网还不反对 getGlobalState,所以须要显示地在注册子利用时通过 props 去下发该办法:

import store from './store';
const microApps = [
  {
    name: 'sub-vue',
    entry: '//localhost:7777/',
    activeRule: '/sub-vue',
  },
  {
    name: 'sub-react',
    entry: '//localhost:7788/',
    activeRule: '/sub-react',
  }
]

const apps = microApps.map(item => {
  return {
    ...item,
    container: '#subapp-viewport', // 子利用挂载的 div
    props: {
      routerBase: item.activeRule, // 下发根底路由
      getGlobalState: store.getGlobalState // 下发 getGlobalState 办法
    },
  }
})

export default microApps

vue 子利用的状态封装

后面说了,子利用在 mount 时会将父利用下发的 state,注册为一个叫 global 的 vuex module,为了不便复用咱们封装一下:

// sub-vue/src/store/global-register.js

/**
 * 
 * @param {vuex 实例} store 
 * @param {qiankun 下发的 props} props 
 */
function registerGlobalModule(store, props = {}) {if (!store || !store.hasModule) {return;}

  // 获取初始化的 state
  const initState = props.getGlobalState && props.getGlobalState() || {menu: [],
    user: {}};

  // 将父利用的数据存储到子利用中,命名空间固定为 global
  if (!store.hasModule('global')) {
    const globalModule = {
      namespaced: true,
      state: initState,
      actions: {
        // 子利用扭转 state 并告诉父利用
        setGlobalState({commit}, payload) {commit('setGlobalState', payload);
          commit('emitGlobalState', payload);
        },
        // 初始化,只用于 mount 时同步父利用的数据
        initGlobalState({commit}, payload) {commit('setGlobalState', payload);
        },
      },
      mutations: {setGlobalState(state, payload) {
          // eslint-disable-next-line
          state = Object.assign(state, payload);
        },
        // 告诉父利用
        emitGlobalState(state) {if (props.setGlobalState) {props.setGlobalState(state);
          }
        },
      },
    };
    store.registerModule('global', globalModule);
  } else {
    // 每次 mount 时,都同步一次父利用数据
    store.dispatch('global/initGlobalState', initState);
  }
};

export default registerGlobalModule;

main.js中增加 global-module 的应用:

import globalRegister from './store/global-register'

export async function mount(props) {console.log('[vue] props from main framework', props)
  globalRegister(store, props)
  render(props)
}

能够看到,该 vuex 模块在子利用 mount 时,会调用 initGlobalState 将父利用下发的 state 初始化一遍,同时提供了 setGlobalState 办法供内部调用,外部主动告诉同步到父利用。子利用在 vue 页面应用时如下:

export default {
  computed: {
    ...mapState('global', {user: state => state.user, // 获取父利用的 user 信息}),
  },
  methods: {...mapActions('global', ['setGlobalState']),
    update () {this.setGlobalState('user', { name: '张三'})
    }
  },
};

这样就达到了一个成果:子利用不必晓得 qiankun 的存在,它只晓得有这么一个 global module 能够存储信息,父子之间的通信都封装在办法自身了,它只关怀自身的信息存储就能够了。

ps: 该计划也是有毛病的,因为子利用是在 mount 时才会同步父利用下发的 state 的。因而,它只适宜每次只 mount 一个子利用的架构(不适宜多个子利用共存);若父利用数据有变动而子利用又没触发 mount,则父利用最新的数据无奈同步回子利用。想要做到多子利用共存且父动静传子,子利用还是须要用到 qiankun 提供的 onGlobalStateChange 的 api 监听才行,有更好计划的同学能够分享讨论一下。该计划刚好合乎笔者以后的我的项目需要,因而够用了,请同学们依据本人的业务需要来封装。

子利用切换 Loading 解决

子利用首次加载时相当于新加载一个我的项目,还是比较慢的,因而 loading 是不得不加上的。

官网的例子中有做了 loading 的解决, 然而须要额定引入import Vue from 'vue/dist/vue.esm',这会减少主利用的打包体积(比照发现大略减少了 100KB)。一个 loading 减少了 100K,显然代价有点无奈承受,所以须要思考一种更优一点的方法。

咱们的主利用是用 vue 搭建的,而且 qiankun 提供了 loader 办法能够获取到子利用的加载状态,所以自然而然地能够想到:main.js 中子利用加载时,将 loading 的状态传给 Vue 实例,让 Vue 实例响应式地显示 loading。接下来先选一个 loading 组件:

  • 如果主利用应用了 ElementUI 或其余框架,能够间接应用 UI 库提供的 loading 组件。
  • 如果主利用为了放弃简略没有引入 UI 库,能够思考本人写一个 loading 组件,或者找个玲珑的 loading 库,如笔者这里要用到的 NProgress。
npm install --save nprogress

接下来是想方法如何把 loading 状态传给主利用的 App.vue。通过笔者试验发现,new Vue 办法返回的 vue 实例能够通过 instance.$children[0] 来扭转 App.vue 的数据,所以革新一下main.js

// 引入 nprogress 的 css
import 'nprogress/nprogress.css'
import microApps from './micro-app';

// 获取实例
const instance = new Vue({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 办法
let apps = microApps.map(item => {
  return {
    ...item,
    loader
  }
})
registerMicroApps(apps);

start();

PS: qiankun 的 registerMicroApps 办法也监听到子利用的 beforeLoad、afterMount 等生命周期,因而也能够应用这些办法记录 loading 状态,但更好的用法必定是通过 loader 参数传递。

革新主利用的 App.vue,通过 watch 监听isLoading

<template>
  <div id="layout-wrapper">
    <div class="layout-header"> 头部导航 </div>
    <div id="subapp-viewport"></div>
  </div>
</template>

<script>
import NProgress from 'nprogress'
export default {
  name: 'App',
  data () {
    return {isLoading: true}
  },
  watch: {isLoading (val) {if (val) {NProgress.start()
      } else {this.$nextTick(() => {NProgress.done()
        })
      }
    }
  },
  components: {},
  created () {NProgress.start()
  }
}
</script>

至此,loading 成果就实现了。尽管 instance.$children[0].isLoading 的操作看起来比拟骚,但的确比官网的提供的例子老本小很多(体积减少简直为 0),若有更好的方法,欢送大家评论区分享。

抽取公共代码

不可避免,有些办法或工具类是所有子利用都须要用到的,每个子利用都 copy 一份必定是不好保护的,所以抽取公共代码到一处是必要的一步。

根目录下新建一个 common 文件夹用于寄存公共代码,如下面的多个 vue 子利用都能够共用的 global-register.js,或者是可复用的request.jssdk之类的工具函数等。这里代码不贴了,请间接看 demo。

公共代码抽取后,其余的利用如何应用呢?能够让 common 公布为一个 npm 私包,npm 私包有以下几种组织模式:

  • npm 指向本地 file 地址:npm install file:../common。间接在根目录新建一个 common 目录,而后 npm 间接依赖文件门路。
  • npm 指向公有 git 仓库: npm install git+ssh://xxx-common.git
  • 公布到 npm 私服。

本 demo 因为是基座和子利用都汇合在一个 git 仓库上,所以采纳了第一种形式,但理论利用时是公布到 npm 私服,因为前面咱们会拆分基座和子利用为独立的子仓库,反对独立开发,后文会讲到。

须要留神的是,因为 common 是不通过 babel 和 pollfy 的,所以援用者须要在 webpack 打包时显性指定该模块须要编译,如 vue 子利用的 vue.config.js 须要加上这句:

module.exports = {transpileDependencies: ['common'],
}

子利用反对独立开发

微前端一个很重要的概念是拆分,是分治的思维,把所有的业务拆分为一个个独立可运行的模块。

从开发者的角度看,整个零碎可能有 N 个子利用,如果启动整个零碎可能会很慢很卡,而产品的某个需要可能只波及到其中一个子利用,因而开发时只需启动波及到的子利用即可,独立启动专一开发,因而是很有必要反对子利用的独立开发的。如果要反对,次要会遇到以下几个问题:

  • 子利用的登录态怎么保护?
  • 基座不启动时,怎么获取到基座下发的数据和能力?

在基座运行时,登录态和用户信息是寄存在基座上的,而后基座通过 props 下发给子利用。但如果基座不启动,只是子利用独立启动,子利用就没法通过 props 获取到所需的用户信息了。因而,解决办法只能是父子利用都得实现一套雷同的登录逻辑。为了可复用,能够把登录逻辑封装在 common 中,而后在子利用独立运行的逻辑中增加登录相干的逻辑。

// sub-vue/src/main.js

import {store as commonStore} from 'common'
import store from './store'

if (!window.__POWERED_BY_QIANKUN__) {
  // 这里是子利用独立运行的环境,实现子利用的登录逻辑
  
  // 独立运行时,也注册一个名为 global 的 store module
  commonStore.globalRegister(store)
  // 模仿登录后,存储用户信息到 global module
  const userInfo = {name: '我是独立运行时名字叫张三'} // 假如登录后取到的用户信息
  store.commit('global/setGlobalState', { user: userInfo})
  
  render()}
// ...
export async function mount (props) {console.log('[vue] props from main framework', props)

  commonStore.globalRegister(store, props)

  render(props)
}
// ...

!window.__POWERED_BY_QIANKUN__示意子利用处于非 qiankun 内的环境,即独立运行时。此时咱们仍然要注册一个名为 global 的 vuex module,子利用外部同样能够从 global module 中获取用户的信息,从而做到抹平 qiankun 和独立运行时的环境差别。

PS:咱们后面写的 global-register.js 写得很奇妙,可能同时反对两种环境,因而下面能够通过 commonStore.globalRegister 间接援用。

子利用独立仓库

随着我的项目倒退,子利用可能会越来越多,如果子利用和基座都汇合在同一个 git 仓库,就会越来越臃肿。

若我的项目有 CI/CD,只批改了某个子利用的代码,但代码提交会同时触发所有子利用构建,牵一动员全身,是不合理的。

同时,如果某些业务的子利用的开发是跨部门跨团队的,代码仓库如何分权限治理又是一个问题。

基于以上问题,咱们不得不思考将各个利用迁徙到独立的 git 仓库。因为咱们独立仓库了,我的项目可能不会再放到同一个目录下,因而后面通过 npm i file:../common 形式装置的 common 就不实用了,所以最好还是公布到公司的 npm 私服或采纳 git 地址模式。

qiankun-example为了更好展现,仍将所有利用都放在同一个 git 仓库下,请各位同学不要照抄。

子利用独立仓库后聚合治理

子利用独立 git 仓库后,能够做到独立启动独立开发了,这时候又会遇到问题:开发环境都是独立的,无奈一览整个利用的全貌

尽管开发时专一于某个子利用时更好,但总有须要整个我的项目跑起来的时候,比方当多个子利用须要相互依赖跳转时,所以还是要有一个整个我的项目对所有子利用 git 仓库的聚合治理才行,该聚合仓库要求做到可能一键 install 所有的依赖(包含子利用),一键启动整个我的项目。

这里次要思考了三种计划:

  1. 应用git submodule
  2. 应用git subtree
  3. 单纯地将所有子仓库放到聚合目录下并 .gitignore 掉。
  4. 应用 lerna 治理。

git submodulegit subtree 都是很好的子仓库治理计划,但毛病是每次子利用变更后,聚合库还得同步一次变更。

思考到并不是所有人都会应用该聚合仓库,子仓库独立开发时往往不会被动同步到聚合库,应用聚合库的同学就得常常做同步的操作,比拟耗时耗力,不算特地完满。

所以第三种计划比拟合乎笔者目前团队的状况。聚合库相当于是一个空目录,在该目录下 clone 所有子仓库,并gitignore,子仓库的代码提交都在各自的仓库目录下进行操作,这样聚合库能够防止做同步的操作。

因为 ignore 了所有子仓库,聚合库 clone 下来后,仍是一个空目录,此时咱们能够写个脚本scripts/clone-all.sh,把所有子仓库的 clone 命令都写上:

# 子仓库一
git clone git@xxx1.git

# 子仓库二
git clone git@xxx2.git

而后在聚合库也初始化一个package.json,scripts 加上:

  "scripts": {"clone:all": "bash ./scripts/clone-all.sh",},

这样,git clone 聚合库下来后,再 npm run clone:all 就能够做到一键 clone 所有子仓库了。

后面说到聚合库要可能做到一键 install 和一键启动整个我的项目,咱们参考 qiankun 的 examples,应用 npm-run-all 来做这个事件。

  1. 聚合库装置npm i npm-run-all -D
  2. 聚合库的 package.json 减少 install 和 start 命令:
  "scripts": {
    ...
    "install": "npm-run-all --serial install:*",
    "install:main": "cd main && npm i",
    "install:sub-vue": "cd sub-vue && npm i",
    "install:sub-react": "cd sub-react && npm i",
    "start": "npm-run-all --parallel start:*",
    "start:sub-react": "cd sub-react && npm start",
    "start:sub-vue": "cd sub-vue && npm start",
    "start:main": "cd main && npm start"
  },

npm-run-all--serial 示意有程序地一个个执行,--parallel示意同时并行地运行。

配好以上,一键装置npm i, 一键启动npm start

vscode eslint 配置

如果应用 vscode,且应用了 eslint 的插件做主动修复,因为我的项目处于非根目录,eslint 没法失效,所以还须要指定 eslint 的工作目录:

// .vscode/settings.json
{
  "eslint.workingDirectories": [
    "./main",
    "./sub-vue",
    "./sub-react",
    "./common"
  ],
  "eslint.enable": true,
  "editor.formatOnSave": false,
  "editor.codeActionsOnSave": {"source.fixAll.eslint": true},
  "search.useIgnoreFiles": false,
  "search.exclude": {"**/dist": true},
}

子利用相互跳转

除了点击页面顶部的菜单切换子利用,咱们的需要也要求子利用外部跳其余子利用,这会波及到顶部菜单 active 状态的展现问题:sub-vue切换到 sub-react,此时顶部菜单须要将sub-react 改为激活状态。有两种计划:

  • 子利用跳转动作向上抛给父利用,由父利用做真正的跳转,从而父利用晓得要扭转激活状态,有点子组件 $emit 事件给父组件的意思。
  • 父利用监听 history.pushState 事件,当发现路由换了,父利用从而晓得要不要扭转激活状态。

因为 qiankun 临时没有封装子利用向父利用抛出事件的 api,如 iframe 的postMessage,所以计划一有些难度,不过能够将激活状态放到状态治理中,子利用通过扭转 vuex 中的值让父利用同步就行,做法可行但不太好,保护状态在状态治理中有点简单了。

所以咱们这里选计划二,子利用跳转是通过 history.pushState(null, '/sub-react', '/sub-react') 的,因而父利用在 mounted 时想方法监听到 history.pushState 就能够了。因为 history.popstate 只能监听 back/forward/go 却不能监听 history.pushState,所以须要额定全局复写一下history.pushState 事件。

// main/src/App.vue
export default {
  methods: {bindCurrent () {
      const path = window.location.pathname
      if (this.microApps.findIndex(item => item.activeRule === path) >= 0) {this.current = path}
    },
    listenRouterChange () {const _wr = function (type) {const orig = history[type]
        return function () {const rv = orig.apply(this, arguments)
          const e = new Event(type)
          e.arguments = arguments
          window.dispatchEvent(e)
          return rv
        }
      }
      history.pushState = _wr('pushState')

      window.addEventListener('pushState', this.bindCurrent)
      window.addEventListener('popstate', this.bindCurrent)

      this.$once('hook:beforeDestroy', () => {window.removeEventListener('pushState', this.bindCurrent)
        window.removeEventListener('popstate', this.bindCurrent)
      })
    }
  },
  mounted () {this.listenRouterChange()
  }
}

性能优化

每个子利用都是一个残缺的利用,每个 vue 子利用都打包了一份vue/vue-router/vuex。从整个我的项目的角度,相当于将那些模块打包了屡次,会很节约,所以这里能够进一步去优化性能。

首先咱们能想到的是通过 webpack 的 externals 或主利用下发公共模块进行复用。

然而要留神,如果所有子利用都共用一个雷同的模块,从久远来看,不利于子利用的降级,难以两败俱伤。

当初感觉比拟好的做法是:主利用能够下发一些本身用到的模块,子利用能够优先选择主利用下发的模块,当发现主利用没有时则本人加载;子利用也能够间接应用最新的版本而不必父利用下发的。

这个计划参考自 qiankun 微前端计划实际及总结 - 子项目之间的公共插件如何共享,思路说得十分残缺,大家能够看看,本我的项目临时还没加上该性能。

部署

当初网上 qiankun 部署相干的文章简直搜不到,可能是感觉简略没啥好说的吧。但对于还不太熟悉的同学来说,其实会比拟纠结 qiankun 部署的最佳部署计划是怎么的呢?所以觉得很有必要讲一下笔者这里的部署计划,供大家参考。

计划如下:

思考到主利用和子利用共用域名时可能会存在路由抵触的问题,子利用可能会源源不断地增加进来,因而咱们将子利用都放在 xx.com/subapp/ 这个二级目录下,根门路 / 留给主利用。

步骤如下:

  1. 主利用 main 和所有子利用都打包出一份 html,css,js,static,分目录上传到服务器,子利用对立放到 subapp 目录下,最终如:
├── main
│   └── index.html
└── subapp
    ├── sub-react
    │   └── index.html
    └── sub-vue
        └── index.html
  1. 配置 nginx,预期是 xx.com 根门路指向主利用,xx.com/subapp指向子利用, 子利用的配置只需写一份,当前新增子利用也不须要改 nginx 配置,以下应该是微利用部署的最简洁的一份 nginx 配置了。
server {
    listen       80;
    server_name qiankun.fengxianqi.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后就能够了。

本文顺便做了线上 demo 展现:

整站(主利用):http://qiankun.fengxianqi.com/

独自拜访子利用:

  • subapp/sub-vue, 留神察看 vuex 数据的变动。
  • subapp/sub-react

遇到的问题

一、react 子利用启动后,主利用第一次渲染后会挂掉


子利用的热重载竟然会引得父利用间接挂掉,过后齐全懵逼了。还好搜到了相干的 issues/340, 即在复写 react 的 webpack 时禁用掉热重载(加了上面配置禁用后会导致没法热重载,react 利用在开发时得手动刷新了,是不是有点好受。。。):

module.exports = {webpack: function override(config, env) {
    // 解决主利用接入后会挂掉的问题:https://github.com/umijs/qiankun/issues/340
    config.entry = config.entry.filter((e) => !e.includes('webpackHotDevClient')
    );
    // ...
    return config;
  }
};

二、Uncaught Error: application ‘xx’ died in status SKIP_BECAUSE_BROKEN: [qiankun] Target container with #subapp-viewport not existed while xx mounting!

在本地 dev 开发时是齐全失常的,这个问题是部署后在首次关上页面才会呈现的,F5 刷新后又会失常,只能在清掉缓存后复现一次。这个 bug 困扰了几天。

错误信息很清晰,即主利用在挂载 xx 子利用时,用于装载子利用的 dom 不存在。所以一开始认为是 vue 做主利用时,#subapp-viewport还没来得及渲染,因而要尝试确保主利用 mount 后再注册子利用。

// 主利用的 main.js
new Vue({render: h => h(App),
  mounted: () => {
    // mounted 后再注册子利用
    renderMicroApps();},
}).$mount('#root-app');

但该方法不行,甚至 setTimeout 都用上了也不行,需另想办法。

最初逐渐调试发现是我的项目加载了一段高德地图的 js 导致的,该 js 在首次加载时会应用 document.write 去复写整个 html,因而导致了 #subapp-viewport 不存在的报错,所以最初是要想方法去掉该 js 文件就能够了。

小插曲:为什么咱们的我的项目会加载这个高德地图 js?咱们我的项目也没有用到啊,这时咱们陷入了一个思维误区:qiankun 是阿里的,高德也是阿里的,qiankun 不会偷偷在渲染时动静加载高德的 js 做些数据收集吧?十分羞愧会对一个开源我的项目有这个想法。。。实际上,是因为我司写组件库模板的小伙伴遗记移除调试时 public/index.html 用到的这个 js 了,过后还去评论 issue 了(捂脸哭)。把这个讲进去,是想说遇到 bug 时还是要先检查一下本人,别轻易就去质疑他人。

最初

本文从开始搭建到部署十分残缺地分享了整个架构搭建的一些思路和实际,心愿能对大家有所帮忙。要揭示一下的是,本示例可能不肯定最佳的实际,仅作为一个思路参考,架构是会随着业务需要一直调整变动的,只有适合的才是最好的。

示例代码:https://github.com/fengxianqi/qiankun-example。

在线 demo:http://qiankun.fengxianqi.com/

独自拜访在线子利用:

  • subapp/sub-vue
  • subapp/sub-react

最初的最初,喜爱本文的同学还请能棘手给个赞和小星星激励一下,非常感谢看到这里。

一些参考文章

  • 微前端在小米 CRM 零碎的实际
  • 微前端实际
  • 可能是你见过最欠缺的微前端解决方案
  • 微前端在美团外卖的实际
  • qiankun 微前端计划实际及总结
退出移动版