微前端的入门讲解

433 阅读7分钟

在一个公司里,特别是企业级软件里,经常会要求把多个业务线集成在一个站点上。这会造成什么问题。

  1. 随着业务增长,前端代码也就越来越多,不过开发环境还是线上环境打包时间都会越来越长。
  2. 随着业务增多,相对的每个业务的更新也会增多,发版频率会变得越来越多。每次其中一个业务更新一个问题,基本上其它业务都需要进行升级、测试和上线。

一、微前端要解决的什么问题呢

上线慢

现在的前端,都是工程化项目,每次上线都需要进行编译,随着业务扩展,代码量的增多,编译速度会越来越慢。每次发版编译甚至需要一二十分钟。如果碰到需要npm安装依赖,这将又是一场灾难。

耦合度高

由于所有的业务都在一个项目里,即使我们文件目录做的多漂亮,依然避免不了会存在相互引用的问题,一旦影响范围考虑的不全面,代码的更新很可能会影响我们想不到的问题地方。

技术单一

在同一个项目里一旦我们技术选型定了,即使新的功能来了,我们依然只能按照之前框架来做。

二、微前端实现方案

由于上诉问题,所以有了微前端的出现,它是一种类似于后端微前端的前端架构,根据我们要解决的问题,我们知道一个微前端的方案需要满足以下几个条件:

  1. 主应用中可以集成子应用
  2. 单独部署
  3. 团队自治
  4. 可以相互通信
  5. 样式和js隔离

在早期我们其实已经有了微前端的实现方案,只是用iframe来实现的。随着前端技术的发展,出现single-spa、qiankuan、icestark等。

接下来我们简单说下几种实现方案

iframe

iframe其实可以满足微前端的条件,完美隔离,js、css都是独立的运行环境。不限制使用,页面可以多个iframe来组合业务。当然它的缺点也很明显:

1,无法保持路由状态,刷新后路由状态丢失

2,子应用的相互之间交互比较困难

路由转发模式

是指通过路由,转发到不同独立前端的应用上,常用的方案是nginx进行代理分发。

优点:实现简单、子应用间技术栈无关、不需要对先用应用改造

缺点:

  • 每次切换应用时,浏览器会重新加载页面;
  • 多个子应用无法并存
  • 子应用间通信比较困难
  • 子应用的信息不通用,比如登录信息

single-spa

可以参考其官网single-spa.js.org/

优点:

  • 切换应用时,浏览器不需要重新加载
  • 完全与技术无关
  • 多个子应用可以并存

缺点:

  • 需要对原有应用进行改造
  • 有额外的学习成本
  • 关于子应用加载、隔离、通信等需要自己实现
  • 子应用间相同资源重复加载

qiankun

qiankun主要是基于single-spa进行的二次封装,它具有single-spa所有的优点,解决了需要开发人员自己编写加载子应用、通信、隔离等逻辑,实现了样式隔离、js沙箱等功能。同样存在子应用间相同资源重复加载的问题。下图是qiankun官网给到的能力图:

img

具体可参考官网地址:qiankun.umijs.org/zh

接下来我们主要说下qiankun在项目中的使用以及遇到的问题。

三、qiankun在企业里的使用

先来看下下图qiankun的工作流程

微前端流程.drawio3.png

我们先来看下主应用和子应用是如何配置,以下是官网中的给出的例子

qiankun有两种加载机制,一种是基于路由的加载,一种是手动加载,我们今天只说基于路由的加载

1.1、主应用配置

1,按照qiankun组件

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

2,在主应用中配置子应用信息

import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
  {
    name: 'react app', // app name registered
    entry: '//localhost:7100',
    container: '#yourContainer',
    activeRule: '/yourActiveRule',
  },
  {
    name: 'vue app',
    entry: { scripts: ['//localhost:7100/main.js'] },
    container: '#yourContainer2',
    activeRule: '/yourActiveRule2',
  },
]);

start();

1.2、子应用配置

子应用不需要按照额外的qiankun的组件

1,导出相应的生命周期钩子(如果不需要特殊处理,这一步也可以不做处理),以供主应用在适当的时机调用。
/**
 * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
 * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
 */
export async function bootstrap() {
  console.log('react app bootstraped');
}

/**
 * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
 */
export async function mount(props) {
  ReactDOM.render(<App />, props.container ? props.container.querySelector('#root') : document.getElementById('root'));
}

/**
 * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
 */
export async function unmount(props) {
  ReactDOM.unmountComponentAtNode(
    props.container ? props.container.querySelector('#root') : document.getElementById('root'),
  );
}

/**
 * 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
 */
export async function update(props) {
  console.log('update props', props);
}

2,配置微应用的打包工具(这一步是需要的)

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

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

2、路由劫持

以上边主项目里的配置为例

微前端流程-第 2 页.drawio.png

1,url变化时会触发window上的hashchange或者popstate,我们知道vue-router也是通过这两个事件拦截的路由变化,所有的路由变化都经过qiankun内部函数urlReroute统一处理。

2,urlReroute会根据路由的地址,和注册子应用的activeRule进行匹配。

3,匹配到对应子应用后,会调用对应的entry的配置内容,进行对子应用的加载,使用fetch进行加载对应的js和css

4,渲染对应的子应用到container: '#yourContainer'元素上

3、沙箱

其实是对window对象上属性进行备份和重置的过程。

qiankun用到了三种沙箱机制:

快照沙箱:通过对window对象的浅拷贝来处理。我们在子应用加载时会有两步操作:a、会先把window对象备份,之后的对window操作,b、会之前是否有diff内容,如果有恢复之前子应用的window属性。当移除子应用时,一是会恢复之前window上内容,二是将diff记录下来。

单例沙箱:其实和快照沙箱机制差不多,只是区别是记录变化通过proxy代理实现

proxySandbox(代理沙箱):每个子应用都会基于window主要属性生成一个单独的proxySandbox,之后子应用的更改都在proxySandbox上,所以不会影响window对象,如果一页面有多个子应用,也不会相互影响。前边种如果一个页面有多个子应用的会收到影响。而且省去了对比时间。

4、父子通信

1,在微前端中我们会经常存在子应用需要主应用拿数据的情况,这种情况我们可以使用官网提供的的initGlobalState方法,将主应用要传递给的子应用的数据通过参数传过去,子应用通过props来获取值。

当主应用

下边是官方的一个示例

主应用

import { initGlobalState, MicroAppStateActions } from 'qiankun';

// 初始化 state
const actions: MicroAppStateActions = initGlobalState(state);

actions.onGlobalStateChange((state, prev) => {
  // state: 变更后的状态; prev 变更前的状态
  console.log(state, prev);
});
actions.setGlobalState(state);
actions.offGlobalStateChange();

我们使用initGlobalState进行初始化数据,如果之后主应用数据改变了,可以通过setGlobalState,将改变的数据通知到对应的微应用里

微应用

// 从生命周期 mount 中获取通信方法,使用方式和 master 一致
export function mount(props) {
  props.onGlobalStateChange((state, prev) => {
    // state: 变更后的状态; prev 变更前的状态
    console.log(state, prev);
  });

  props.setGlobalState(state);
}

2,还有一种如果可以通过注册全局自定义事件window.dispatchEvent和addEventListener来实现子应用调用主应用的事件

5、样式隔离

qiankun实现了single-spa 推荐的两种样式隔离方案:ShadowDOM 和 Scoped CSS。

我们先来说下ShadowDOM,qiankun 的源码实现也很简单,只是添加一个 Shadow DOM 节点,伪代码如下:

if (strictStyleIsolation) {
    if (!supportShadowDOM) {
      // 报错
      // ...
    } else {
      // 清除原有的内容
      const { innerHTML } = appElement;
      appElement.innerHTML = '';

      let shadow: ShadowRoot;

      if (appElement.attachShadow) {
        // 添加 shadow DOM 节点
        shadow = appElement.attachShadow({ mode: 'open' });
      } else {
        // deprecated 的操作
        // ...
      }
      // 在 shadow DOM 节点添加内容
      shadow.innerHTML = innerHTML;
    }
  }

通过 Shadow DOM 的天然的隔离特性来实现子应用间的样式隔离。

另一个方案就是 Scoped CSS 了,说白了就是通过修改选择器来实现子应用间的样式隔离。

qiankun 会扫描子应用加载到的CSS 文本,通过正则匹配在选择器前加上子应用的名字,如果遇到元素选择器,就加一个爸爸类名给它。

比如:

.subApp.container {
  //
}

6、部署

关于生产环境部署,官网也有说明,我这边说下在企业里我们是怎么部署的。

主应用正常部署,每个子应用都有自己的构建流水线,构建完成后通过nginx代理指向不用的子应用打包后地址,主应用配置对应的url规则即可。

这种方案操作比较方便,但是每次新开一个子应用都会需要nginx配置和流水线配置。

7、遇到的问题

1,资源重复加载的问题,

2,keep-alive失效的问题,解决办法是把数据存在了localstorage里。

3,子应用之间相互通信的问题,都需要通过主应用。

8、拆分微前端的颗粒度

拆分微应用的颗粒度应该是按照业务来划分的,不应该太细。以什么为准呢,子应用之间应该没有业务交互,如果需要业务交互,那么可以合并为同一个子应用。

示例代码地址: github.com/bubucuo/mir…