微前端-初探

147 阅读8分钟

image.png

一种架构风格,由多个可独立交付的前端应用成功组成一个更大的整体

由简述可得出,微前端的核心就是拆、合, 一种可插拔式的架构,将拆除的互不干涉的子应用,整合到一起,这就是微前端。

image.png

上面这张图是一个微前端流程图,A,B,C三个不同的分布在不同仓库中的三个应用,进过编译及测试后,生成3个不同的产品,这三个产品可以单独部署应用,最终通过应用主体(基座)将子应用整合在一起,组成一个更大,更健全的产品。

为什么需要微前端?

image.png

业务价值

  • 比如公司有很多散落在各处的平台应用,找起来也麻烦,域名也记不住,这时我们就可以将这些应用聚合到一个主体之上,不但方便使用,还方便做一些特殊功能,例如接入统一工单通知。
  • 多应用断层:举个例子,查询订单的同时,我还想看用户访问路径,一般公司的用户访问信息记录在一个单独系统中,这时就需要打开2个窗口去查看,不方便。
  • 遗留系统迁移:历史原因,有不少之前写的应用,可能使用为老技术栈,目前稳定运行,也不需要进行更新。对于这种应用来讲,我们没必要浪费时间去重写。既然可以使用,我们直接整合到新应用中。

工程价值

  • 技术栈无关:每个开发者擅长的技术栈都不同,采用微前端可以抛开技术栈的界限,让每个子应用具备完全自主权、接入范围广、向后兼容、向前兼容。
  • 独立开发:仓库独立,每个开发者只需要维护自己的仓库,去除代码冲突、编译慢等问题。
  • 增量升级: 在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略。
  • 独立部署: 每个微应用之间状态隔离。

实现方案

实现微前端有很多种方案,目前也没有既定的规范,实现微前端不一定是新技术,也不必太复杂,选择适合自己的即可。这里我收集了几种,供各位参考

iframe

Iframe作为一个古老的HTML标签,人人都觉得普通,却一直很管用。在之前有很多中后台应用都是通过iframe拼凑而成的。它能有效的将另外一个网页/单页面嵌入到当前页面中,且两个页面中的CSS、JS相互隔离,互不干扰

  • 优点
    • 完美隔离,天然支持样式隔离及全局变量隔离
    • 对现有代码,无侵入
  • 缺点
    • 完美隔离,这也是缺点,无法共享一些数据或资源,iframe 内外系统的通信、数据同步等需求
    • 页面切换时,浏览器回退,容易出现跳转错误
    • 每次启动,都需要重新加载资源,每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程
    • 较难处理生命周期的问题。eg: 卸载后、渲染后
    • 通讯机制:在每个应用中创建postmessage事件并监听,不友好,对应用的侵入性也较强,需要定义一套ToPic规范

路由分发式

通过不同路由将业务分发至不同的前端页面上,这种实现也算微前端的一种。因为它是由多个独立可交付的应用组成的。 但这种更像是多个应用的聚合,拼凑的。

不过,这个列子也说明,微前端不一定是新技术,也不必太复杂,选择最适合自己的就好。

image.png

NPM包

使用Package集成,将每个子应用发布为NPM模块. 这种方式,更接近咱们目前的开发习惯,想使用那个业务就调用那个业务组件,算是一种友好的方式。 但这种方式会带来很多额外的开发成本。


微件化:每个业务团队将自己编写好的,编译后的代码放到指定服务器中,在运行时只加载对应的业务模块,类似VUE的Componet.

image.png

更新流程复杂

假设我们有3个管理系统,且3个管理系统都引入了同一个子业务模块,倘若子模块更新是,需要将代码发布到NPM, 3个管理系统,分别去更新NPM包,并重新部署。整个过程复杂且容易出问题。

image.png

构建速度慢

由于依赖包的增加,必然会增加编译时长

image.png

模块联邦-webpack5 Federation

官网介绍: 多个独立的构建可以组成一个应用程序,这些独立的构建之间不应该存在依赖关系,因此可以单独开发和部署它们。这通常被称为微前端,但并不仅限于此

Webpack5中引进了模块联邦机制。通过该机制,可以让构建后的代码库动态的、运行时的跑在另一个代码库中。 代码可以直接在不同项目间远程调用并共享任意内容

核心原理:在多个应用之间通过全局变量建立彩虹桥。

基本概念:Federation中,分两种角色,一种被引用方,也就是子应用,用来将自己抛出去,一种是引用方,也就是主应用/基座,用法如下

  • 被引用方

const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin')
new ModuleFederationPlugin({
  name: 'remote', // 必传值,即输出的模块名,被远程引用时路径为${name}/${expose}
  library: { type: 'var', name: 'remote' }, // 声明一个挂载在全局下的变量名,其中name即为umd的name
  filename: 'remote.js', // 构建后的Chunk名
  exposes: {
    // 作为被引用方的关键配置项,用于暴露对应提供的模块
    './Content': './src/Content'
  },
  // 可选,优先用HOST的依赖,若没有,再用自己的。
  shared: {
    vue: {
      eager: true //它不会将模块放入异步块中,而是同步提供它们。这允许我们在初始块中使用这些共享模块
    }
  }
})


  • 引用方
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin')
new ModuleFederationPlugin({
  name: 'host', // 必传值,即输出的模块名,被远程引用时路径为${name}/${expose}
  // 作为引用方最关键的配置项,用于声明需要引用的远程资源包的名称与模块名称
  remotes: {
    remote: 'remote@http://localhost:8002/remote.js'
  }
})

  • 使用场景
    • 实现微前端:通过shared和remotes提供公共依赖资源注入,减少线上体积、整合子应用到主应用。
    • 编译提速:可将node_modules资源提前打包好,通过runtime方式引用,编译时只构建项目源文件。
    • 多页应用资源复用:可以再运行时进行依赖引入,实现组件复用,页面共享,同时,也变相有了线上热更新的能力

需要解决的问题

前面都在说侵入,既然提到侵入,就会想到隔离。 在微前端中,有2个基础的隔离问题。

样式隔离

如何处理不同应用之间样式冲突问题? 一般分为两种情况,主、子应用样式冲突、子应用之间样式冲突

image.png

  • 主与子样式隔离

这里我们可以使用一些工程化的方式,就能较好的解决此类问题

image.png

  • 子应用之间隔离(Dynamic Stylesheet) - 动态样式表

image.png

  • Shadow Dom (影子Dom)

image.png

JS隔离

通过沙箱的方式处理:既sandbox,顾名思义,就是让你程序运行在一个完全隔离的环境下,不对外界的程序造成影响,避免了全局变量污染。

  • 快照沙箱

通过快照沙箱,在应用加载是激活快照,卸载时取消快照。在应用切换时依照快照恢复当前应用的环境。 缺点就是无法支持多实例。

image.png

 class SnapShotSandbox {
        constructor() {
          this.proxy = window;
          this.modifyPropMap = {}; //记录window的修改
          this.active(); // 默认记录
        }
        // 激活
        active() {
          this.windowSnapShot = {}; //记录快照
          for (const prop in window) {
            // 将属性记录在快照中
            if (window.hasOwnProperty(prop)) {
              this.windowSnapShot[prop] = window[prop];
            }
          }

          // 将修改过的属性,赋给window
          Object.keys(this.modifyPropMap).forEach((prop) => {
            window[prop] = this.modifyPropMap[prop];
          });
        }
        // 失活
        inactive() {
          for (const prop in window) {
            if (window.hasOwnProperty(prop)) {
              if (window[prop] !== this.windowSnapShot[prop]) {
                // 将变动的属性,保存到属性表中
                this.modifyPropMap[prop] = window[prop];
                // 将window还原
                window[prop] = this.windowSnapShot[prop];
              }
            }
          }
        }
      }

      let sandbox = new SnapShotSandbox();
    //  切换应用,不影响全局属性
      ((window) => {
        window.a = 1;
        window.b = 2;
        console.log(window.a, window.b); 
        sandbox.inactive(); // 失活
        console.log(window.a, window.b);
        sandbox.active(); //激活
        console.log(window.a, window.b);
      })(sandbox.proxy);
  • Proxy沙箱 image.png
 class ProxySandbox {
        constructor() {
          this.proxy = this.init();
        }
        init() {
          const rawWindow = window; //存储源window
          const fakeWindow = {}; // 伪window,用来存储沙箱内的属性
          return new Proxy(fakeWindow, {
            set(target, props, value) {
              target[props] = value;
              return true;
            },

            get(target, prop) {
              return target[prop] || rawWindow[prop];
            },
          });
        }
      }
      let sandbox1 = new ProxySandbox();
      let sandbox2 = new ProxySandbox();

      window.a = "我是全局变量a";
      console.log(window.a);

      ((window) => {
        window.a = "第一个沙箱";
        console.log(window.a);
      })(sandbox1.proxy);

      ((window) => {
        window.a = "第二个沙箱";
        console.log(window.a);
      })(sandbox2.proxy);