Recently, I read the article “Front-end Micro-service in Bytedance’s Polishing and Application” written by bytedance technical team, and I was interested in service registration and dynamic loading module. Besides, I did some similar things before, so I spent some time to do some simple practices. I hope I can help you.

My understanding of microservices

Microservices, as I understand them, are essentially splitting a large application into many independent modules, each of which can be independently developed, debugged and launched. I understand that there are mainly the following benefits:

  • Each module is an independent entity. If a module fails, the entire application will not fail.
  • Since each module can go live separately, it goes live faster and facilitates update iterations.
  • As a result of the service registration function, so the page can be configured to load dynamically, for the function of new, roll back is particularly convenient.
  • Framework independent (this may depend on the implementation)

The purpose of this article is to briefly discuss some practices for service discovery and dynamically loading modules. Of course, here is only a simple idea for reference.

Service discovery

First, let’s think about a problem. If we split a large application into multiple modules, how will the main application know which modules there are, and the corresponding configuration information of each module (JS/CSS configuration information)? In fact, the process of finding configured module information is called service discovery.

So how do we implement service discovery?

What is the problem with hard-coding the configuration information directly into the main program? Every time you add, modify, or delete a module, you need to release the main program, which is definitely not going to work.

So, is there a better way?

At this time more intelligent students may think of, then I put the configuration information through the way of the interface call? I personally recommend this approach. Therefore, sometimes we need to return different module configuration information according to the user’s identity, permissions, through the interface, we can easily do this. I gave a simple module to configure information module:

[{
    name: 'home'.path: '/home'.js: 'https://unpkg.com/react@16/umd/react.development.js'.css: 'https://unpkg.com/react@16/umd/react.css'
}]
Copy the code

The configuration information is divided into four items. Path refers to the routing address corresponding to the module. That is, when the front end matches the route as /home, it loads the corresponding JS file and CSS file, executes the corresponding JS file, and renders the module content.

Dynamic loading module

Suppose we match the /home route and load the corresponding JS file, how do we render the corresponding module?

For dynamically loading modules, I have two schemes in mind at present, one is the new Function + CommonJs scheme used in the paper of byte Technology team, and the other is similar to AMD scheme. Next, I will briefly introduce the implementation of the two schemes.

new Function

Basic knowledge of

I don’t know whether the students have packaged the file into CommonJs format, so I will first post a paragraph of CommonJs packaging

First let’s implement a simple module function

import React from 'react';
import ReactDom from 'react-dom';

function App() {
  return React.createElement('div', null, 'hello world');
}

export const render = container => {
  ReactDom.render(React.createElement(App), container);
};
Copy the code

This code is very simple, is the normal implementation of a Hello world logic, and exported a render method.

So how does the main program load modules

// Global module management const modules = {};function loadModule() {
      const currentConfig = {
        name: 'home',
        path: '/home',
        js: './dist/main.js'}; const { name, path, js } = currentConfig; modules[name] = { exports: {}, }; const ajax = new XMLHttpRequest(); ajax.open('get', js);
      ajax.onload = function(event) {
        new Function('module'.'exports', this.responseText)(
          modules[name],
          modules[name].exports,
        );
    
        modules[name].exports.render(document.getElementById('app'));
      };
      ajax.send();
}

loadModule();
Copy the code

Implementation steps

Look at the code may be easier to understand, the main program in the loading module, mainly divided into the following steps:

  1. When developing the module, export a Render method as per convention.
  2. When the main program is loaded, create module information according to the configuration information.
  3. The js code is loaded by XHR, and the js code is executed by passing the module information to new Function, so that the js exported content is mounted to modules[name].
  4. Call the Render method of the module export to render the module content.

There are two key messages

  • Exported modules must be of CommonJs package type, otherwise we cannot pass in our own modules.
  • Use the XHR request when loading the module to get the source code of the code.

That’s the key to dynamically loading modules through new Function. Let’s talk about AMD like implementation ideas.

AMD

AMD(Asynchronous Module Definition) is a Module management solution, just like CommonJs. The feature is that every time you define a module, you need to write something like this

define('myModule', [...deps], function () {
    ...some code
});
Copy the code

By defining a module in this way, other modules can use the module through dependency injection. Of course, we don’t go into too much depth here, just a simple implementation. The hello World example is the same as before, but this time we have changed it a bit:

import React from 'react';
import ReactDom from 'react-dom';

function App() {
  return React.createElement('div', null, 'hello world');
}

const render = container => {
  ReactDom.render(React.createElement(App), container);
};

window.defineModule('home', {
  render,
});
Copy the code

The main change is that instead of exporting the module through export, we define our own module through the window.definemodule method. The implementation of the window.definemodule method is placed in the main program:

const namespace = Symbol('namespace');
window[namespace] = {};

function defineModule(name, exports) {
  window[namespace][name] = exports;
}

function getModule(name) {
  return window[namespace][name];
}

window.defineModule = defineModule;

function loadModule() {
      const currentConfig = {
        name: 'home',
        path: '/home',
        js: './dist/main.js'}; const { name, path, js } = currentConfig; const scriptEle = document.createElement('script');
      scriptEle.src = js;
      scriptEle.onload = () => {
        const module = getModule(name);
        module.render(document.getElementById('app'));
      };
    
      document.body.appendChild(scriptEle);
}

loadModule();
Copy the code

Implementation steps

The code should also be easy to understand, so let’s walk through the implementation steps

  1. The main program implements the module definition by defining a defineModule method and attaching it to the window.
  2. At development time, individual modules define their own modules through the window.definemodule method and export their render methods.
  3. The main program loads modules through the normal create script. After loading the module, you can get the exported content of the module according to the configuration information of the module.
  4. Call the Render method of the module export to render the module content.

Routing to monitor

Of course, this is actually missing the most important point, is the route listening. Because each module is bound to the route, for example, the home module is rendered when accessing the /home route. For route listening, I’m not going to expand it here, but if you’re interested, you can look at the history interface and the hashChange event. React-router-dom can also be used as a router.

conclusion

This article is a brief introduction to front-end microservices and some of my thoughts on service discovery and dynamic loading of modules. Just personal thinking, I hope to bring you some help. The main conclusions are as follows

  • Service discovery can be realized by configuring interfaces. On the one hand, it is conducive to dynamically adding, deleting, modifying and searching modules, and on the other hand, it can return different module information according to the user’s identity and permission.
  • Dynamically loaded modules can be implemented using New Function and amD-like methods, depending on personal preference. I personally think the window.definemodule approach is more elegant and probably easier to debug.

This article address in -> my blog address, welcome to give a start or follow.