qiankun微前端实践

款冬

前言

在介绍具体的接入方案和文档之前,先简单介绍一下,什么是微前端,以及为什么我们的前端工程要做微前端化的改造?

什么是微前端?

微前端,用一句话来解释就是:将多个独立的前端应用聚合在一起组成一个全新的应用,各个应用直接彼此独立,互不干扰。也就是说,一个微前端应用给用户的感观就是一个完整的应用,但是在技术角度上是由一个个独立的应用组合通过某种方式组合而成的。再举个大家都知道都例子,ifream就是微前端应用都一种,现在仍然活跃在很多控制台系统中,但是因为其自身存在的一些缺陷,现在业内有了更好的解决方案,这就是后面要重点介绍的内容。

微前端的价值?

为什么要做微前端?这也是要看业务场景的,微前端的价值我认为主要体现在两个方面:1.解决历史大工程的迭代问题2.控制台的应用集成,为用户提供统一入口。这两方面可以分开进行阐述:

历史大工程的迭代问题

这是很常见的业务场景,一般有2种非常典型的痛点问题:

1.一个后管平台迭代了很多版本之后,整个工程的体积很大,本地编译的速度很慢,而且经常是多人分别开发不同模块,不管哪个模块的改动,也需要整个工程重新进行编译、打包、部署。

2.原先的工程是用旧的技术栈(如jquery),现在要集成新的功能模块,想要新的技术栈(如react、vue)进行迭代。或是团队同学技术栈不一样,希望各自负责的模块开发可以用不同的技术栈进行开发,最终都能集成到原应用中。

控制台的应用集成

这个也很好理解,希望将各个应用的入口进行收拢,进行统一管理。解决零散的页面入口问题,提升用户体验。

正文

言归正传,下面结合实际的工程例子,开始详细介绍本次微前端的技术方案和接入文档:

技术选型

在调研了业内多个微前端的框架后,最终定下来基于 "乾坤" 框架(文档点这里)进行微前端的解决方案改造,选用 "乾坤"的原因有2点,1.框架的可靠性和口碑较好,githup(详情点这里)上有8.2k的star。2. 接入简单,不会对原有工程造成很大"破坏"。关于框架的地址和文档自行移步,不再重复介绍。

技术方案

应用分层

因为面向的都是后管系统,后管系统有一些通用的特点:需要登录鉴权,有左侧的菜单栏和上方的个人信息导航栏,中间部分是具体的页面内容。基于这个特点,可以把系统分成主应用(登录+菜单+导航)和子应用(具体的内容页面)两个部分。

截屏2021-01-18 下午7.58.37.png

主应用定位

主应用的定位是一个架子,主要是集成登录、菜单、导航,以及提供全局数据和公共方法,具体的功能模块尽量放到子应用中

子应用定位

子应用的设计需要额外考虑独立性,如果是在微前端的环境中,子应用是作为整体应用的一部分的,因此会与主应用有着紧密的联系。但是子应用本身也应该做到相对的独立,脱离了主应用也能单独运行,因此在方案的设计中,要考虑子应用既能集成在主应用中以微前端的架构运行,也要保证子应用能够脱离主应用单独运行。

配置流程

乾坤应用的配置流程其实很简单,只需要分别在主应用和子应用进行各自需要的配置即可。主应用中需要安装好qiankun框架,配置需要挂载的子应用的挂载规则,开启qiankun应用。而在子应用中需要在入口文件中暴露出 bootstrap、mount、unmount 三个生命周期钩子,以供主应用在适当的时机调用,接收主应用传递的props。此外,为了让主应用能正确识别微应用暴露出来的一些信息,需要在对应的打包工具中增加少量的配置信息。

截屏2021-01-24 下午3.47.55.png

主应用配置

应用的注册和挂载指的是在主应用中注册微应用,设置好在微应用在主应用中挂载的页面元素,以及触发子应用加载的页面url。用官网的例子来说明:

// 主应用的入口文件
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
  {
    name: 'react app', // 应用名称,必须唯一
    entry: '//localhost:7100', // 子应用的地址
    container: '#yourContainer', // 页面中挂载子应用的元素节点
    activeRule: '/yourActiveRule', // 触发子应用加载的页面ur
  },
  {
    name: 'vue app',
    entry: { scripts: ['//localhost:7100/main.js'] },
    container: '#yourContainer2',
    activeRule: '/yourActiveRule2',
  },
]);
start(); // 运行乾坤

一般上述的方式已经能满足大多数常见的应用场景来,如果微应用不是直接跟路由关联或是有需要手动触发子应用加载的场景,比如子应用嵌套的场景,需要在一个子应用中手动去加载另一个子应用。这时候乾坤提供了一个 loadMicroApp 的方法进行子应用的手动加载:

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

子应用配置

首先在入口文件中暴露生命周期钩子函数,bootstrap 只会在子应用首次加载时触发一次,在这里可以初始一些子应用的全局数据;mount 会在每次子应用加载时都触发,在这里常用来触发子应用的渲染方法,同时接收主应用下发的 props ,进行父子通信;unmount 会在每次子应用卸载时都触发,在这里卸载微应用的应用实例,清空一些全局数据。

export async function bootstrap() {
  console.log('app bootstraped');
}

export async function mount(props) {
  console.log('app mount');
  console.log('props from main framework', props);
}

export async function unmount() {
  console.log('app unmount')
}

其次,在 webpack 打包配置文件中添加一些识别的配置信息,这里以官网的 vue 子应用作为例子:

const { name } = require('./package');
module.exports = {
  devServer: {
    headers: {
      'Access-Control-Allow-Origin': '*', // 开启应用间的跨域访问
    },
  },
  configureWebpack: {
    output: {
      library: `${name}-[name]`, // 名称的匹配
      libraryTarget: 'umd',// 把微应用打包成 umd 库格式
      jsonpFunction: `webpackJsonp_${name}`,
    },
  }
};

主应用和子应用通信

在乾坤中,提供了两种方式进行主应用和子应用的通信:

方式一:提供了发布订阅模式实现父子应用的通信,提供了 initGlobalState 方法定义全局状态,并返回通信方法。主应用和子应用中都可以通过 setGlobalState 按一级属性设置全局状态,并通过 onGlobalStateChange 方法监听全局状态的变化,有变更触发 callback,一般在主应用中进行全局状态的初始化,子应用中进行监听获取。

方式二:提供了基于 props 传参的模式,主应用将需要传递给子应用的属性和方法通过 props 的形式进行传递,子应用在生命周期的钩子函数中进行接收,是一种单向数据流模式。

考虑到业务中一般只需要主应用中给子应用进行登录信息、用户信息等相关全局信息的传递,且希望子应用能够保持相对独立,减少与主应用的耦合,决定采用方式二的形式进行通信。为了方便全局状态的统一管理,对父子应用的通信模块进行了二次封装,具体的使用方式如下:

第一步:主应用中封装应用通信类

// sharedStore.js
// 下面都是伪代码,提供大致的设计思路和使用方式,具体的使用方式跟框架有关,后面具体例子中会有详细介绍

import store from './xxx'; // 引入全局store,根据框架不同方式不一样
import utils from './xxx' // 引入共有工具函数

class SharedStore {

  /**
   * 获取 global model
   */
  getGlobal() {
    const { global } = store.getState(); // 获取store中的global
    return global || {};
  }

  /**
   * 子定义方法例子
   */
  fetchDictData(dict) {
    return store.dispatch({
      type: "dict/fetchDictData",
      payload: dict
    });
  }
  
   /**
   * 获取共有工具函数例子
   */
  getSharedUtils() {
   const { commonErrorHandler, myAjax } = utils;
   return {
           commonErrorHandler,
        myAjax
   }
  }
}

const sharedStore = new SharedStore();
export default sharedStore;

第二步:主应用通过props传递

在主应用中注册子应用的配置时,将需要下发给子应用的属性或是方法,通过props参数传递给子应用。子应用在生命周期的钩子函数的入参中就能拿到对应的props,再根据需要自行进行存储使用。在这里,我们把刚刚在主应用中封装好的 sharedStore 实例传递给子应用。

// 主应用中的入口文件
import sharedStore from '../src/utils/sharedStore.js'; // 主应用通信类文件

...
{
    name: 'xxx'
    entry: 'xxx',
    container: '#xxx',
    activeRule: 'xxx',
    props: {
      sharedStore
    }
  },
...

第三步:子应用在生命周期函数中接收props

子应用生命周期函数中的入参就是props,直接就可以从入参中拿到传递的属性或者方法,拿到之后根据业务需要进一步进行存储。举例来说,可以挂载到对应的全局实例中或是存在localstorage中,也可以混入子应用的store中作为初始化的值。在具体组件中需要使用时,能通过恰当的方式取到即可。在这里,我们在子应用中也写一个对应的 sharedStore 类,同时兼容子应用独立运行和在乾坤环境中运行:

// sharedStore.js
import store from './xxx'; // 子应用自身store,根据框架不同方式不一样
import utils from './xxx' // 子应用自身工具函数
import dictData from './xxx' // 子应用自定义数据对象
import { getDvaApp } from 'umi';
import { get } from './request';

// 独立运行时的数据管理
class selfStore {
  /**
   * 获取 global model
   */
  getGlobal() {
    const { global } = store.getState(); // 从子应用的store中获取
    return global || {};
  }


    /**
   * 自定义方法例子
   */
  fetchDictData(dict) {
    return {
        dictData: {
          ...dictData
      }
    }
  }
  
  /**
   * 获取共有工具函数
   */
  getSharedUtils() {
       return {
        ...utils
    }
  }
}

// 兼容独立运行和qinkun子应用的全局数据
class SharedStore {
  static store = new selfStore();

  /**
   * 重载 store
   */
  static overloadStore(store) {
    SharedStore.store = store;
  }

  /**
   * 获取 store 实例
   */
  static getStore() {
    return SharedStore.store;
  }
}

export default SharedStore;

然后在子应用生命周期函数中调用 sharedStore 类重载 store 数据:

// app.js
import sharedStore from './utils/sharedStore.js'; // 子应用通信类文件 

export async function mount(props) {
   sharedStore.overloadStore(props.sharedStore); // props.sharedStore 是主应用中传递的共享数据
}

这样之后,不论子应用是独立运行或是在乾坤的环境中,如果要用到 shareStore 中的属性和方法,都统一从 shareStore 的 getStore 方法中获取就可以了。

各个应用样式隔离

这个问题乾坤框架做了一定的处理,在运行时有一个sandbox的参数,默认情况下沙箱可以确保单实例场景子应用之间的样式隔离,但是无法确保主应用跟子应用、或者多实例场景的子应用样式隔离。如果要解决主应用和子应用的样式问题,目前有2种方式:

  1. 在乾坤种配置 { strictStyleIsolation: true } 时表示开启严格的样式隔离模式。这种模式下 qiankun 会为每个微应用的容器包裹上一个 shadow dom 节点,从而确保微应用的样式不会对全局造成影响。但是基于 ShadowDOM 的严格样式隔离并不是一个可以无脑使用的方案,大部分情况下都需要接入应用做一些适配后才能正常在 ShadowDOM 中运行起来,这个在 qiankun 的 issue 里面有一些讨论和使用经验。
  2. 人为用 css 前缀来隔离开主应用和子应用,在组件层面用 css scoped进行组件层面的样式区分,在 css框架层面可以给css组件库加上不同的前缀,比如文档中的 antd 例子:
  3. 配置 webpack 修改 less 变量
{
  loader: 'less-loader',
+ options: {
+   modifyVars: {
+     '@ant-prefix': 'yourPrefix',
+   },
+   javascriptEnabled: true,
+ },
}

b. 配置 antd ConfigProvider

import { ConfigProvider } from 'antd';
   
export const MyApp = () => (
  <ConfigProvider prefixCls="yourPrefix">
    <App />
  </ConfigProvider>
);

公共资源的处理

公共资源的处理,大致可以分为两类:第三方依赖库、通用方法和组件,乾坤本身目前还没有提供官方的解决方案(文档中说后续会有),所有目前都是使用者自己进行处理的。在方案设计中考虑到需要支持子应用在乾坤环境中和单独运行时,下面分别展开说一下各自的处理方法:

第三方依赖库

指的是像react、lodash这样的在主应用和子应用中都会用到的通用依赖包,在主应用中引入 cdn 资源,因为在乾坤应用中,所有子应用能访问到主应用中引入的 cdn 资源,同时乾坤本身对外链资源作了缓存,外链内容请求到之后,会记录到一个全局变量中,下次再次使用,会先从这个全局变量中取,解决了重复加载的问题。同时在子应用中,需要配置 webpack 的 external 参数,当在乾坤环境中,external中配置第三方依赖库,这样子应用打包时就不会把这部分的npm包资源打包到node_module中,减少应用包的体积;当在独立运行时,external配置为空,这样打包时会正常打包对应的npm包资源。

// webpack配置文件

module.exports = {
  //...
  externals: {
    react: 'react'
  }
};

通用方法和组件

指的是在父子应用或是多个子应用中共享的组件或函数方法,对于这种资源的处理有2种方式:

  1. 发布到 npm 包平台,需要用到的应用自行安装 npm 包,这是一种广泛运用的组件共享方式。
  2. 通过父应用 props 下发给子应用,子应用拿到后进行使用。为了通用化这种使用模式,可以在主应用中封装一个插件统一管理需要共享给子应用的组件,将这个插件通过 props 下发给子应用,子应用安装成全局组件后使用即可。以 vue 框架为例,首先在主应用中导出一个插件用来给子应用注册全局组件:
// shareComponent.js, 主应用中的共享组件管理插件

import SideBar from '../components/sideBar' //自定义公共组件
import TopBar from '../components/topBar' //自定义公共组件

const shareComponents = [SideBar, TopBar]; // 需要共享给子应用的组件
//vue插件的install方法
const install = function (Vue) {
  shareComponents.forEach(component => {
    Vue.component(component.name, component); //注册为Vue全局组件
  });
};

export default {
  install,
};

然后将这个插件作为props下发给子应用:

// 主应用中的入口文件
import shareComponent from '../src/utils/shareComponent.js'

...
{
    name: 'xxx'
    entry: 'xxx',
    container: '#xxx',
    activeRule: 'xxx',
    props: {
          shareComponent
    }
  },
...

接着在子应用的钩子函数中进行接收并安装

// 子应用入口文件
...
export async function mount(props) {
  console.log('[vue] props from main framework', props);
  Vue.use(props.shareComponent)
}
...

最后在子应用的任意页面中就可以正常使用:

// 子应用的一个组件中
<template>
      <TopBar></TopBar>
</template>

主应用和子应用的路由模式

不管是 vue 还是 react 应用,路由模式都有 history 和 hash 两种模式,主应用和子应用可以自由搭配不同的路由模式,一共会出现 2 * 2 = 4 种搭配,从使用角度来说,4种搭配都是可以正常使用,只是不同模式下 主应用下的 activeRule 和 子应用下的 route 的 basePath 会有不同,同时子应用内的路由跳转方式需要做相应的调整。经过对比这几种模式的优缺点,最终采用的是主应用history 模式搭配子应用 hash 模式。这么选择的原因是:

  1. 主应用用history模式,可以兼容子应用的hash或history模式,不需要调整旧项目的子应用内部的路由跳转方式
  2. 如果是子应用是新项目,用hash模式搭配主项目的history模式,模式清晰,从url上就能直观区分出当前是处在主应用还是子应用的页面。
  3. 本身项目中没有子应用互相跳转的场景,子应用的hash模式满足了当下需求,如果后续有跳转需求,可以通过调用主应用下发的跳转方法通过主应用来跳转。

子应用错误处理

乾坤提供了全局错误监听方法,在主应用中开启后,可以监听到所有子应用中抛出的异常,根据业务需要在错误回调函数中进行自定义处理即可。

// app.js
import { addGlobalUncaughtErrorHandler } from 'qiankun';
addGlobalUncaughtErrorHandler(event => console.error('子应用异常', event));

应用的部署

主应用和子应用可以部署在同一个服务器下,通过目录层级进行区分;也可以部署在不同的服务器上,这时主应用的就需要通过nginx做一次反向代理,把指向子应用的访问路径转发到子应用的服务入口。部署这一块官网的文档上介绍得十分清楚,在这里就不再多说了,详见 乾坤应用的部署

其他

子应用是否运行在乾坤中

标识:window.__POWERED_BY_QIANKUN__

嵌套子应用

这是一种较为复杂的场景,暂时没有遇到需要的场景,在本地跑了下demo,通过,此外这篇文章中给出了两种解决方案:qiankun的嵌套。在qiankun 的 issue里也有很多关于子应用嵌套的讨论,整体的结论是qiankun是支持的,但是因为工程和业务场景的不同可能会遇到bug,这点需要结合实际项目会有更大的发言权,在这里就不展开了。

接入文档

根据我们团队现有的前端应用现状,接入文档分成umi应用和其他应用(包括vue和react)两大类型来介绍。

umi应用

umi应用有个特点是umi框架通过插件的模式做了react - dva - layout 这种从框架层,到ui层的高度封装,开发者不需要关注实现的细节,只需要按照规范进行使用即可,而同样地,umi应用通过 @umijs/plugin-qiankun 插件(文档链接)对乾坤框架进行来集成,因此用法需要单独说明,下面的umi的例子,基于默认路由和侧边栏layout布局。

第一步:安装 @umijs/plugin-qiankun 插件

npm install --save @umijs/plugin-qiankun

第二步:主应用配置

  1. 插件注册并装置子应用
  2. 如果配置是写死的,不需要从接口获取,且 props 里面传递的共享数据也跟 store 或是接口无关,可以直接在 umi 的 config.js 文件中进行配置
//config.js文件
import { defineConfig } from 'umi';

export default defineConfig({
    qiankun: {
    master: {
      // 注册子应用信息
      apps: [
        {
          name: 'app-organization', // 唯一 id
          entry: '//localhost:7001', // html entry
          props: {
              ...
          }
        },
        {
          name: 'app-platform',
          entry: '//localhost:7002',
           props: {
              ...
          }
        },
      ],
      
      // 使用路由绑定的方式装置子应用
      // 子应用的routes,会被合并到主应用的routes中
      routes: [ 
        {
          path: '/app-organization',
          microApp: 'app-organization',
          microAppProps: {
            autoSetLoading: true, // 开启子应用页面自动loading
          }
        },
        {
          path: '/app-platform',
          microApp: 'app-platform',
          microAppProps: {
            autoSetLoading: true,
          }
        }   
      ]
    },
  },
    ......
})

b. 否则,需要把 乾坤的 配置写在 主应用的 app.js 文件中,在运行时进行加载,运行时加载的参数会合并到 config.js 中。

//app.js
import sharedStore from '../src/utils/sharedStore.js'; // 主应用通信类文件

export const qiankun = {
    apps: [
      {
        name: 'app-organization', // 唯一 id
        entry: '//localhost:8001', // html entry
        props: {
          sharedStore, //因为 sharedStore 与主应用 store 相关,所以要写在 app.js 中
        }
      },
      {
        name: 'app-platform',
        entry: '//localhost:8002',
        props: {
          sharedStore,
        }
      },
    ],
}
  1. 封装sharedStore

    封装的目的和思路在上文中已经有说明,在这里给出在umi中具体的应用例子:

// sharedStore.js
import { getDvaApp } from 'umi';
import utils from './utils' // 引入共有工具函数

class SharedStore {

  /**
   * 获取 global model
   */
  getGlobal() {
    const dva = getDvaApp();
    const { global } = dva._store.getState();
    return global || {};
  }

  /**
   * 更新字典数据
   */
  fetchDictData(dict) {
    const dva = getDvaApp();
    return dva._store.dispatch({
      type: "dict/fetchDictData",
      payload: dict
    });
  }
  
   /**
   * 获取共有工具函数
   */
  getSharedUtils() {
   const { commonErrorHandler, myAjax } = utils;
   return {
           commonErrorHandler,
        myAjax
   }
  }
  
}

const sharedStore = new SharedStore();
export default sharedStore;
  1. 修改菜单中子应用页面路径

这里的意思是将左侧菜单中对应的页面路径中关于子应用的要进行处理加上 "/app-organization" 或 "/app-platform" 前缀,这样才能指向子应用。这里的处理可以改接口返回中的路径地址,也可以在前端写一个统一处理函数进行处理。同时如果在页面的业务逻辑中有手动通过 history.push 进行跳转到子应用的地方,也要进行前缀处理。

  1. 修改主应用页面挂载元素

因为 @umijs/plugin-qiankun 插件的默认页面挂载元素的id是 root-master,所以如果用了默认的 document.ejs 模版,需要将模版中 body 元素下的挂载节点 id 从 "root",修改成 "root-master"。同时如果有全局样式 global.css,也需要将其中的样式 "#root" 修改成 "#root-master"。

// document.ejs文件
<html>
<head>
  <meta charSet="utf-8" />
  <title>Your App</title>
</head>
<body>
  <div id="root-master"></div>
</body>
</html>

5.应用端口配置(可选)

因为要同时启动主应用和多个子应用,为了端口管理的方便,可以在 src 下新建一个 .env 文件,在文件中写死应用启动的端口,这样每次启动时端口是固定的,不会根据应用启动的先后发生变化。

// .env文件
PORT=8000

第三步:子应用配置

  1. 插件注册
// config.js
import { defineConfig } from 'umi';
export default defineConfig({
    qiankun: {
    slave: {}
  }
  ...
})
  1. 配置运行时生命周期钩子
// app.js
import sharedStore from './utils/sharedStore.js'; // 子应用通信类文件 

export const qiankun = {
  // 应用加载之前
  async bootstrap(props) {
    console.log('app-platform bootstrap', props);
  },
  // 应用 render 之前触发
  async mount(props) {
    console.log('app2-platform mount', props);
    // 1.重写类的方式将主应用中传递的实例混入子应用中
    sharedStore.overloadStore(props.sharedStore); // props.sharedStore 是主应用中传递的全局数据
  },
  // 应用卸载之后触发
  async unmount(props) {
    console.log('app2-platform unmount', props);
  },
};

3.封装sharedStore

// sharedStore.js
import { getDvaApp } from 'umi';
import { get } from './request';

// 独立运行时的数据管理
class selfStore {
  /**
   * 获取 global model
   */
  getGlobal() {
    // 从store中加载
    const dva = getDvaApp();
    const { global } = dva._store.getState();
    return global || {};
  }

    /**
   * 更新字典数据
   */
  fetchDictData(dict) {
    return {
        dictData: {
          APP_1: 'SP',
        APP_2: 'SE',
        APP_3: 'SL',
      }
    }
  }
  
  /**
   * 获取共有工具函数
   */
  getSharedUtils() {
       return {}
  }
}

// 兼容独立运行和qinkun子应用的全局数据
class SharedStore {
  static store = new selfStore();

  /**
   * 重载 store
   */
  static overloadStore(store) {
    SharedStore.store = store;
  }

  /**
   * 获取 store 实例
   */
  static getStore() {
    return SharedStore.store;
  }
}

export default SharedStore;

4.应用端口配置(可选)

// .env文件
PORT=8001

第四步:应用部署

该项目中的主应用和子应用的前端部署走的是 oss 访问的方式,umi 应用打包后会生成一份 index.html、umi.js 和 umi.css 文件,只需要把这份文件放置在 oss 文件路径下,浏览器访问对应文件路径下的 index.html文件可。因此生产环境和本地开发不同的有两点:1.主应用配置中的 publicPath 要调整到 oss 的根目录(即放置 umi.js 和 umi.css 的目录),而本地开发时 publicPath 对应的是 '/'。2.主应用中 qiankun 的 app 入口配置要调整成子应用 index.html 对应的 oss 。

vue应用

第一步:主应用安装 qiankun

npm i qiankun -S

第二步:主应用配置

  1. 配置入口文件,挂载子应用
  2. 主应用中使用 vue-router 进行路由管理,如果不需要 vue-router,去掉相关的配置即可
  3. 启动乾坤的 start 方法放到 $nextTick 中调用,为了避免出现主应用还没挂载完成,子应用找不到其页面挂载元素而报错
  4. shareComponent、sharedStore的方案是通用的,在上文已经介绍过,不再重复展开。
import Vue from 'vue'
import App from './App.vue'
import VueRouter from 'vue-router'
import routes from './routes'

import  shareComponent from '../src/utils/shareComponent'; // 公享组件
import sharedStore from '../src/utils/sharedStore.js'; // 共享通信类文件
import { registerMicroApps, start } from 'qiankun';


Vue.use(VueRouter)

Vue.config.productionTip = false

const router = new VueRouter({
  routes,
  mode: "history",
})
const vueApp = new Vue({
  router,
  render: h => h(App),
}).$mount('#app')


registerMicroApps([
  {
    name: 'slave-one', // app name registered
    entry: '//localhost:8082',
    container: '#slave-view',
    activeRule: '/qiankun-demo-slave-one',
    props: {
      sharedStore,
      shareComponent,
    }
  },
  {
    name: 'slave-two', // app name registered
    entry: '//localhost:8083',
    container: '#slave-view',
    activeRule: '/qiankun-demo-slave-two',
    props: {
      sharedStore,
      shareComponent,
    }
  },
  {
    name: 'slave-three',
    entry: '//localhost:3000',
    container: '#slave-view',
    activeRule: '/qiankun-demo-slave-three',
    props: {
      sharedStore,
      shareComponent,
    }
  },
]);

vueApp.$nextTick(()=>{
  start(); //为了避免出现主应用还没挂载完成,子应用找不到 #slave-view 元素而报错
})
  1. 配置子应用在页面中的加载位置
  2. 页面的布局固定好左侧菜单和上方导航组件,中间的部分进行页面内容的展示
  3. 通过 $route.name 的有无来判断是加载主应用还是子应用的页面,当主应用中的路由中匹配当前url则是主应用自身的页面,否则展示子应用页面。
// App.vue
<template>
  <div id="app">
    <div class="left-menu">
       <sideBar></sideBar>
    </div>
    <div class="right-content">
      <topBar></topBar>
      <div class="main-content">
          <div id="master-view" v-if="$route.name">
            <router-view></router-view>
          </div>
          <div id="slave-view" v-else></div>
      </div>
    </div>
    
  </div>
</template>

<script>
import sideBar from './components/sideBar';
import topBar from './components/topBar';

export default {
  name: 'App',
  components: {
    sideBar,
    topBar
  },
}
</script>
...

第二步:子应用配置

  1. 配置 public-path.js 文件
if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; // 这是为了在乾坤环境下动态设置 webpack 的 publicPath,防止资源加载出错
}
  1. 配置入口文件
  2. 在入口文件中引入 public-path.js
  3. 子应用如果也用了vue-router,要注意new vueRouter() 的 base 在乾坤环境下要加上和主应用中配置的 activeRule 一致的前缀,一般可以约定都用子应用的工程名字。否则子应用内部的路由跳转会出错。
  4. 在子应用的 unmount 钩子中一定要卸载元素,否则会有多个子应用切换时的bug,这个在乾坤的issue里有提到
import './public-path'
import Vue from 'vue'
import App from './App.vue'
import VueRouter from 'vue-router'
import routes from './routes'

Vue.use(VueRouter)
Vue.config.productionTip = false;

const packageName = require("../package.json").name; // 统一规范,取工程名字,且必须跟主应用配置下的 activeRule 一致
let router = null;
let instance = null;

function render(props = {}) {
  const { container } = props;
  router = new VueRouter({
    base: window.__POWERED_BY_QIANKUN__ ? `/${packageName}` : '/',
    mode: 'hash',
    routes,
  });
  instance = new Vue({
    router,
    render: h => h(App),
  }).$mount(container ? container.querySelector('#app') : '#app'); // 挂载时局部进行查找,避免多个子应用的相互影响
}
// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

// 微应用必须暴露的三个加载生命周期hooks
export async function bootstrap() {
  console.log('[vue] vue app bootstraped');
}
export async function mount(props) {
  console.log('[vue] props from main framework', props);
  Vue.use(props.shareComponent) // 注册共享组件
  render(props); // 渲染应用
}
export async function unmount() {
  instance.$destroy();
  instance.$el.innerHTML = '';
  instance = null;
  router = null;
}

3.配置webpack

const { name } = require('./package');
module.exports = {
  devServer: {
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
  configureWebpack: {
    output: {
      library: `${name}-[name]`,
      libraryTarget: 'umd',// 把微应用打包成 umd 库格式
      jsonpFunction: `webpackJsonp_${name}`,
    },
  },
};

第三步:应用部署

主应用和子应用在不同服务器独立部署,具体方案在上文中已经有说明,跟着官方文档操作即可。

react应用

流程跟 vue 应用大同小异,参考上方的 vue 配置流程例子和官网的教程:react微应用

总结

经过对老项目(umi工程)对微前端实际改造,以及本地新项目(vue工程)对demo的测试。借助乾坤框架 + 自定义的封装处理,基本上实现了微前端的应用的平稳接入。总体感受,微前端确实能以一种优雅的方式解决前端工程臃肿和多人协作开发的问题。但是需要注意的是,是否需要微前端要从项目实际情况出发,比如简单的单个应用根本不需要采用,反而会增加开发成本。此外微前端的子应用的拆分方式和力度也需要从实际项目出发,并不是越细越好,适合实际的才是最好的。

参考资料

乾坤官方文档

qiankun issue

@umi/plugin-qiankun文档

qiankun 微前端方案实践及总结(一)

qiankun 微前端实践总结(二)

让前端走进微时代, 微微一弄很哇塞!

阅读 782

山外de楼
前端学习的一些心得体会,与大家共勉。

前端小小弄潮儿, 内推杭州一二线互联网大厂前端开发、前端实习生~

1.3k 声望
15 粉丝
0 条评论
你知道吗?

前端小小弄潮儿, 内推杭州一二线互联网大厂前端开发、前端实习生~

1.3k 声望
15 粉丝
宣传栏