关于微前端:微前端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 微前端计划实际及总结

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理