乐趣区

关于前端:从零开始写一个微前端框架数据通信篇

前言

自从微前端框架 micro-app 开源后,很多小伙伴都十分感兴趣,问我是如何实现的,但这并不是几句话能够说明确的。为了讲清楚其中的原理,我会从零开始实现一个繁难的微前端框架,它的外围性能包含:渲染、JS 沙箱、款式隔离、数据通信。因为内容太多,会依据性能分成四篇文章进行解说,这是系列文章的最终篇:数据通信篇。

通过这些文章,你能够理解微前端框架的具体原理和实现形式,这在你当前应用微前端或者本人写一套微前端框架时会有很大的帮忙。如果这篇文章对你有帮忙,欢送点赞留言。

相干举荐

  • micro-app 仓库地址
  • simple-micro-app 仓库地址
  • 从零开始写一个微前端框架 - 渲染篇
  • 从零开始写一个微前端框架 - 沙箱篇
  • 从零开始写一个微前端框架 - 款式隔离篇
  • 从零开始写一个微前端框架 - 数据通信篇
  • micro-app 介绍

开始

架构设计

微前端各个利用自身是独立运行的,通信零碎不应该对利用侵入太深,所以咱们采纳公布订阅零碎。然而因为子利用封装在 micro-app 标签内,作为一个类 webComponents 的组件,公布订阅零碎的弱绑定和它心心相印。

最好的形式是像一般属性一样通过 micro-app 元素传递数据。但自定义元素无奈反对对象类型的属性,只能传递字符串,例如 <micro-app data={x: 1}></micro-app> 会转换为 <micro-app data='[object Object]'></micro-app>,想要以组件化模式进行数据通信必须让元素反对对象类型属性,为此咱们须要重写 micro-app 原型链上 setAttribute 办法解决对象类型属性。

流程图

代码实现

创立文件 data.js,数据通信的性能次要在这里实现。

公布订阅零碎

实现公布订阅零碎的形式很多,咱们简略写一个,满足根本的需要即可。

// /src/data.js

// 公布订阅零碎
class EventCenter {
  // 缓存数据和绑定函数
  eventList = new Map()
  /**
   * 绑定监听函数
   * @param name 事件名称
   * @param f 绑定函数
   */
  on (name, f) {let eventInfo = this.eventList.get(name)
    // 如果没有缓存,则初始化
    if (!eventInfo) {
      eventInfo = {data: {},
        callbacks: new Set(),}
      // 放入缓存
      this.eventList.set(name, eventInfo)
    }

    // 记录绑定函数
    eventInfo.callbacks.add(f)
  }

  // 解除绑定
  off (name, f) {const eventInfo = this.eventList.get(name)
    // eventInfo 存在且 f 为函数则卸载指定函数
    if (eventInfo && typeof f === 'function') {eventInfo.callbacks.delete(f)
    }
  }

  // 发送数据
  dispatch (name, data) {const eventInfo = this.eventList.get(name)
    // 当数据不相等时才更新
    if (eventInfo && eventInfo.data !== data) {
      eventInfo.data = data
      // 遍历执行所有绑定函数
      for (const f of eventInfo.callbacks) {f(data)
      }
    }
  }
}

// 创立公布订阅对象
const eventCenter = new EventCenter()

公布订阅零碎很灵便,但太过于灵便可能会导致数据传输的凌乱,必须定义一套清晰的数据流。所以咱们要进行数据绑定,基座利用一次只能向指定的子利用发送数据,子利用只能发送数据到基座利用,至于子利用之间的数据通信则通过基座利用进行管制,这样数据流就会变得清晰

通过格式化订阅名称来进行数据的绑定通信。

// /src/data.js
/**
 * 格式化事件名称,保障基座利用和子利用的绑定通信
 * @param appName 利用名称
 * @param fromBaseApp 是否从基座利用发送数据
 */
 function formatEventName (appName, fromBaseApp) {if (typeof appName !== 'string' || !appName) return ''
  return fromBaseApp ? `__from_base_app_${appName}__` : `__from_micro_app_${appName}__`
}

因为基座利用和子利用的数据通信形式不同,咱们离开定义。

// /src/data.js

// 基座利用的数据通信办法汇合
export class EventCenterForBaseApp {
  /**
   * 向指定子利用发送数据
   * @param appName 子利用名称
   * @param data 对象数据
   */
  setData (appName, data) {eventCenter.dispatch(formatEventName(appName, true), data)
  }

  /**
   * 清空某个利用的监听函数
   * @param appName 子利用名称
   */
  clearDataListener (appName) {eventCenter.off(formatEventName(appName, false))
  }
}

// 子利用的数据通信办法汇合
export class EventCenterForMicroApp {constructor (appName) {this.appName = appName}

  /**
   * 监听基座利用发送的数据
   * @param cb 绑定函数
   */
  addDataListener (cb) {eventCenter.on(formatEventName(this.appName, true), cb)
  }

  /**
   * 解除监听函数
   * @param cb 绑定函数
   */
  removeDataListener (cb) {if (typeof cb === 'function') {eventCenter.off(formatEventName(this.appName, true), cb)
    }
  }

  /**
   * 向基座利用发送数据
   * @param data 对象数据
   */
  dispatch (data) {const app = appInstanceMap.get(this.appName)
    if (app?.container) {
      // 子利用以自定义事件的模式发送数据
      const event = new CustomEvent('datachange', {
        detail: {data,}
      })

      app.container.dispatchEvent(event)
    }
  }

  /**
   * 清空以后子利用绑定的所有监听函数
   */
  clearDataListener () {eventCenter.off(formatEventName(this.appName, true))
  }
}

在入口文件中创立基座利用通信对象。

// /src/index.js

+ import {EventCenterForBaseApp} from './data'
+ const BaseAppData = new EventCenterForBaseApp()

在沙箱中创立子利用的通信对象,并在沙箱敞开时清空所有绑定的事件。

// /src/sandbox.js

import {EventCenterForMicroApp} from './data'

export default class SandBox {constructor (appName) {
    // 创立数据通信对象
    this.microWindow.microApp = new EventCenterForMicroApp(appName)
    ...
  }

  stop () {
    ...
    // 清空所有绑定函数
    this.microWindow.microApp.clearDataListener()}
}

到这里,数据通信大部分性能都实现了,但还短少一点,就是对 micro-app 元素对象类型属性的反对。

咱们重写 Element 原型链上 setAttribute 办法,当 micro-app 元素设置 data 属性时进行非凡解决。

// /src/index.js

// 记录原生办法
const rawSetAttribute = Element.prototype.setAttribute

// 重写 setAttribute
Element.prototype.setAttribute = function setAttribute (key, value) {
  // 指标为 micro-app 标签且属性名称为 data 时进行解决
  if (/^micro-app/i.test(this.tagName) && key === 'data') {if (toString.call(value) === '[object Object]') {
      // 克隆一个新的对象
      const cloneValue = {}
      Object.getOwnPropertyNames(value).forEach((propertyKey) => {
        // 过滤 vue 框架注入的数据
        if (!(typeof propertyKey === 'string' && propertyKey.indexOf('__') === 0)) {cloneValue[propertyKey] = value[propertyKey]
        }
      })
      // 发送数据
      BaseAppData.setData(this.getAttribute('name'), cloneValue)
    }
  } else {rawSetAttribute.call(this, key, value)
  }
}

功败垂成,咱们验证一下是否能够失常运行,在 vue2 我的项目中向子利用发送数据,并承受来自子利用的数据。

// vue2/pages/page1.vue
<template>
  ...
  <micro-app
    name='app'
    url='http://localhost:3001/'
    v-if='showapp'
    id='micro-app-app1'
    :data='data'
    @datachange='handleDataChange'
  ></micro-app>
</template>

<script>
export default {
  ...
  mounted () {setTimeout(() => {
      this.data = {name: '来自基座利用的数据'}
    }, 2000)
  },
  methods: {handleDataChange (e) {console.log('承受数据:', e.detail.data)
    }
  }
}
</script>

在 react17 我的项目中监听来自基座利用的数据并向基座利用发送数据。

// react17/index.js

// 数据监听
window.microApp?.addDataListener((data) => {console.log("承受数据:", data)
})

setTimeout(() => {window.microApp?.dispatch({ name: '来自子利用的数据'})
}, 3000);

查看管制抬的打印信息:

数据失常打印,数据通信性能失效。

结语

从这些文章中能够看出,微前端的实现并不难,真正难的是开发、生产环境中遇到的各种问题,没有完满的微前端框架,无论是 Module Federation、qiankun。micro-app 以及其它微前端解决方案,都会在某些场景下呈现问题,理解微前端原理能力疾速定位和解决问题,让本人立于不败之地。

退出移动版