preface

Since the micro front-end framework micro-App became open source, many friends are very interested and ask me how to achieve it, but it is not a few words can be understood. To illustrate how this works, I’m going to implement a simple micro front-end framework from scratch. Its core features include rendering, JS sandbox, style isolation, and data communication. This is the final part of the series: Data Communication.

Through these articles, you will learn how micro front end frameworks work and how they are implemented, which will be of great help if you use them later or write your own. If this post helped you, feel free to like it and leave a comment.

Related to recommend

  • Micro-app Warehouse address
  • Simple-micro-app Warehouse address
  • Write a micro front end frame – render from scratch
  • Write a micro front end frame – sandbox from scratch
  • Write a micro front end frame – style isolation section from scratch
  • Write a micro front end framework from scratch – data communication article
  • Micro – app is introduced

start

Architecture design

The micro front applications themselves run independently, and the communication system should not penetrate the application too deeply, so we use a publish-subscribe system. But because the subapplication is encapsulated within the Micro-app tag, as a WebComponent-like component, the weak bindings of the publish-subscribe system are incompatible with it.

The best way to do this is to pass the data through a micro-app element like a normal attribute. However, custom elements cannot support attributes of object types and can only be passed strings, such as
will be converted to
. To do this we need to override the setAttribute method on the micro-App prototype chain to handle object type attributes.

The flow chart

Code implementation

Create file data.js, data communication function is mainly implemented here.

Publish and subscribe system

There are many ways to implement a publish-subscribe system, but let’s simply write one that meets the basic requirements.

// /src/data.js

// Publish a subscription system
class EventCenter {
  // Cache data and binding functions
  eventList = new Map(a)/** * bind the listening function *@param Name Event name *@param F Binding function */
  on (name, f) {
    let eventInfo = this.eventList.get(name)
    // If there is no cache, initialize
    if(! eventInfo) { eventInfo = {data: {},
        callbacks: new Set(),}// Put it in the cache
      this.eventList.set(name, eventInfo)
    }

    // Record the binding function
    eventInfo.callbacks.add(f)
  }

  // Unbind
  off (name, f) {
    const eventInfo = this.eventList.get(name)
    // eventInfo exists and f is a function
    if (eventInfo && typeof f === 'function') {
      eventInfo.callbacks.delete(f)
    }
  }

  // Send data
  dispatch (name, data) {
    const eventInfo = this.eventList.get(name)
    // Update only when the data is not equal
    if(eventInfo && eventInfo.data ! == data) { eventInfo.data = data// Iterate over all binding functions
      for (const f of eventInfo.callbacks) {
        f(data)
      }
    }
  }
}

// Create publish subscribe objects
const eventCenter = new EventCenter()
Copy the code

Publish-subscribe systems are flexible, but being too flexible can lead to a chaotic flow of data, and a clear set of data flows must be defined. So we’re going to do data binding, where the base application can only send data to the specified child application at one time, and the child application can only send data to the base application, and the data communication between the children is controlled by the base application, so that the data flow is clear

Data binding communication is done by formatting subscription names.

// /src/data.js
/** * Format the event name to ensure binding communication between the base application and the child application *@param AppName Application name *@param FromBaseApp Whether to send data from the base application */
 function formatEventName (appName, fromBaseApp) {
  if (typeofappName ! = ='string'| |! appName)return ' '
  return fromBaseApp ? `__from_base_app_${appName}__ ` : `__from_micro_app_${appName}__ `
}
Copy the code

Because the data communication between the base application and the sub-application is different, we define it separately.

// /src/data.js

// Set of data communication methods applied by the base
export class EventCenterForBaseApp {
  /** * send data to the specified subapplication *@param AppName Subapplication name *@param Data Object data */
  setData (appName, data) {
    eventCenter.dispatch(formatEventName(appName, true), data)
  }

  /** * Clear an application's listener function *@param AppName Name of the sub-application */
  clearDataListener (appName) {
    eventCenter.off(formatEventName(appName, false))}}// Set of data communication methods for the subapplication
export class EventCenterForMicroApp {
  constructor (appName) {
    this.appName = appName
  }

  /** * listen for data sent by the base application *@param Cb binding function */
  addDataListener (cb) {
    eventCenter.on(formatEventName(this.appName, true), cb)
  }

  /** * Unlisten function *@param Cb binding function */
  removeDataListener (cb) {
    if (typeof cb === 'function') {
      eventCenter.off(formatEventName(this.appName, true), cb)
    }
  }

  /** * send data to the base application *@param Data Object data */
  dispatch (data) {
    const app = appInstanceMap.get(this.appName)
    if(app? .container) {// The child application sends data as a custom event
      const event = new CustomEvent('datachange', {
        detail: {
          data,
        }
      })

      app.container.dispatchEvent(event)
    }
  }

  /** * Clear all listening functions bound to the current child application */
  clearDataListener () {
    eventCenter.off(formatEventName(this.appName, true))}}Copy the code

Create the base application communication object in the entry file.

// /src/index.js

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

Copy the code

Create communication objects for the child application in the sandbox and clear all bound events when the sandbox is closed.

// /src/sandbox.js

import { EventCenterForMicroApp } from './data'

export default class SandBox {
  constructor (appName) {
    // Create a data communication object
    this.microWindow.microApp = new EventCenterForMicroApp(appName)
    ...
  }

  stop () {
    ...
    // Clear all binding functions
    this.microWindow.microApp.clearDataListener()
  }
}
Copy the code

At this point, most of the data communication is complete, but one thing that is missing is support for object type attributes of micro-App elements.

We override the setAttribute method on the Element prototype chain to make it special when the micro-app Element sets the data attribute.

// /src/index.js

// Record the native method
const rawSetAttribute = Element.prototype.setAttribute

/ / rewrite setAttribute
Element.prototype.setAttribute = function setAttribute (key, value) {
  // When the target is a Micro-app tag and the attribute name is data
  if (/^micro-app/i.test(this.tagName) && key === 'data') {
    if (toString.call(value) === '[object Object]') {
      // Clone a new object
      const cloneValue = {}
      Object.getOwnPropertyNames(value).forEach((propertyKey) = > {
        // Filter the data injected by the Vue framework
        if(! (typeof propertyKey === 'string' && propertyKey.indexOf('__') = = =0)) {
          cloneValue[propertyKey] = value[propertyKey]
        }
      })
      // Send data
      BaseAppData.setData(this.getAttribute('name'), cloneValue)
    }
  } else {
    rawSetAttribute.call(this, key, value)
  }
}
Copy the code

With that done, let’s verify that it works properly by sending data to and receiving data from the child application in 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: 'Data from base applications'}},2000)},methods: {
    handleDataChange (e) {
      console.log('Accept data:', e.detail.data)
    }
  }
}
</script>
Copy the code

In the REact17 project, listen for data from and send data to the dock application.

// react17/index.js

// Data listening
window.microApp? .addDataListener((data) = > {
  console.log("Receiving data:", data)
})

setTimeout(() = > {
  window.microApp? .dispatch({name: 'Data from sub-applications'})},3000);
Copy the code

View the printed information of the control lift:

The data is printed normally and the data communication function takes effect.

conclusion

From these articles, we can see that micro front-end implementation is not difficult, the real problem is the development and production environment encountered various problems, there is no perfect micro front-end framework, whether it is Module Federation, Qiankun. Micro-app and other micro front end solutions will encounter problems in certain scenarios. Only by understanding the principle of micro front end can we quickly locate and deal with problems and make ourselves invincible.