The current landing scheme of micro front-end can be divided into self-organization mode, base mode and module loading mode.

In contrast to pedestal mode, module-loading mode has no central container (decentralized mode), which means that any single microapplication can act as a module entry point, and the entire project’s microapplications are connected in series with each other. The example is Qiankun VS EMP.

Implementing the Module loading mode relies on webpackage 5’s Module Federation feature.

What is Module Federation?

Multiple independent builds can make up an application. There should be no dependencies between these independent builds, so they can be developed and deployed separately, often referred to as a microfront end, but there’s more to it than that! In layman’s terms, Module Federation provides the ability to load other applications within the current application.

So, if the current module wants to load other modules, it needs an import action, and if it wants to be used by other modules, it needs an export action.

Thus, two concepts of webapck configuration are introduced:

Expose: Exports the application and imports it to other applications

Remote: Imports other applications

This is quite different from the dock model, where single-SPA and Qiankun require a dock (central container) to load other sub-applications. With Module Federation, any Module can reference other applications and export to be used by other applications. This eliminates the concept of container center.

Module Federation configuration resolution

Use the Module Federation need to introduce plug-ins ModuleFederationPlugin, exposes parameter to specify which modules need to export, the distribution of the remotes to the application of import.

Example code, vue3 as an example:

comsumer

const ModuleFederationPlugin = require('webpack').container.ModuleFederationPlugin
module.exports = {
    plugins: [
         new ModuleFederationPlugin({
          // Unique ID, the name of the current micro application
          name: "comsumer".filename: "remoteEntry.js".// Import the module
          remotes: {
          // After the import, give the module a different name: "micro-application name @address/exported file name"
            home: "home@http://localhost:3002/remoteEntry.js",},exposes: {},
          shared: ['vue']]}})Copy the code

home

const ModuleFederationPlugin = require('webpack').container.ModuleFederationPlugin
module.exports = {
    plugins: [
      new ModuleFederationPlugin({
          // Unique ID, the name of the current micro application
          name: "home".// The packaged file name provided externally (used when importing)
          filename: "remoteEntry.js".// Exposed application specific modules
          exposes: {
          // Name: code path
            "./Content": "./src/components/Content"."./Button": "./src/components/Button",},shared: ['vue']})],devServer: {
        port: 3002,}}Copy the code

Use imported content in microapplications

Reference microapplications return a Promise that eventually returns the result of a “module object,” and default is the result of the exported content by default. The comsumer app loads the home app as follows:

import { createApp, defineAsyncComponent } from "vue";
import Layout from "./Layout.vue";
// Load the remote Content component
const Content = defineAsyncComponent(() = > import("home/Content"));
// Load the remote Buttom component
const Button = defineAsyncComponent(() = > import("home/Button"));

const app = createApp(Layout);

app.component("content-element", Content);
app.component("button-element", Button);

app.mount("#app");

Copy the code

Build resolution of Module Federation

What does webpack’s MF configuration do when Webpack is packaged? How does the resulting packaged code load the remote module? How do you export your own modules for other application imports?

Comsumer imports two remote components of the Home app. Comsumer loads the 148 module remoteentry.js:

app.component("content-element", Content);
app.component("button-element", Button);
Copy the code

main.js

var chunkMapping = {
  "186": [
    186]."190": [
    190]};var idToExternalAndNameMapping = {
  "186": [
    "default"."./Content".148]."190": [
    "default"."./Button".148]}; __webpack_require__.f.remotes =(chunkId, promises) = > {
    if (__webpack_require__.o(chunkMapping, chunkId)) {
        chunkMapping[chunkId].forEach((id) = > {
            var getScope = __webpack_require__.R;
            if(! getScope) getScope = [];var data = idToExternalAndNameMapping[id];
            if (getScope.indexOf(data) >= 0) return;
            getScope.push(data);
            if (data.p) return promises.push(data.p);
            // First load the 148 module, remoteentry.js
            handleFunction(__webpack_require__, data[2].0.0, onExternal, 1); }); }};148: ((module, __unused_webpack_exports, __webpack_require__) = > {
    "use strict";
    var __webpack_error__ = new Error(a);module.exports = new Promise((resolve, reject) = > {
        if(typeofhome ! = ="undefined") return resolve();
        __webpack_require__.l("http://localhost:3002/remoteEntry.js".(event) = > {}, "home");
    }).then(() = > (home));
 })
Copy the code

The home module remoteentry. js contains the following code:

var moduleMap = {
	"./Content": () = > {
		return Promise.all([__webpack_require__.e("webpack_sharing_consume_default_vue_vue"), __webpack_require__.e("src_components_Content_vue-_56df0")]).then(() = > (() = > ((__webpack_require__(/ *! ./src/components/Content */ "./src/components/Content.vue")))));
	},
	"./Button": () = > {
		return Promise.all([__webpack_require__.e("webpack_sharing_consume_default_vue_vue"), __webpack_require__.e("src_components_Button_js-_e56a0")]).then(() = > (() = > ((__webpack_require__(/ *! ./src/components/Button */ "./src/components/Button.js"))))); }};Copy the code

__webpack_require__. E executes the methods in __webpack_require__. F in parallel:

/* webpack/runtime/ensure chunk */
(() = > {
	__webpack_require__.f = {};
	// This file contains only the entry chunk.
	// The chunk loading function for additional chunks
	__webpack_require__.e = (chunkId) = > {	
        // All methods in __webpack_require__.f are executed in parallel
            return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) = > {
                    __webpack_require__.f[key](chunkId, promises);
                    return promises;
            }, []));
	};
})();
Copy the code

We look at what are the functions on __webpack_require__.f and discover that there are three: remotes, Consumes and j.

__webpack_require__. O, that is, to refer to the Object. The prototype. HasOwnProperty.

  • Remotes: Loads related to remotes. In this example, the home module does not have Remotes
  • Consumes: It is used to load things related to shared. In this example, there is the Vue module
// no consumes in initial chunks
var chunkMapping = {
    webpack_sharing_consume_default_vue_vue: [
        "webpack/sharing/consume/default/vue/vue",]}; __webpack_require__.f.consumes =(chunkId, promises) = > {
    if (__webpack_require__.o(chunkMapping, chunkId)) {
        chunkMapping[chunkId].forEach((id) = > {
            if (__webpack_require__.o(installedModules, id))
                return promises.push(installedModules[id]);
            try {
                var promise = moduleToHandlerMapping[id]();
                if (promise.then) {
                    promises.push(
                        (installedModules[id] = promise.then(onFactory).catch(onError))
                    );
                } else onFactory(promise);
            } catch(e) { onError(e); }}); }};Copy the code
  • J: Chunk to load JSONP. If the chunk has been loaded, it will not be loaded. If the chunk has not been loaded, it will be called__webpack_require__.lFunction withscriptLabel loading. Here is the simplified code:
var installedChunks = {
    home: 0}; __webpack_require__.f.j =(chunkId, promises) = > {
    // JSONP chunk load
    var installedChunkData = __webpack_require__.o(installedChunks, chunkId)
            ? installedChunks[chunkId]
            : undefined;
    // 0 indicates that the file has been loaded
    if(installedChunkData ! = =0) {
        // Promise indicates loading
        if (installedChunkData) {
                promises.push(installedChunkData[2]);
        } else {
            // Load chunk, but exclude webpack_sharing_consume_default_vue_vue, the shared package
            if ("webpack_sharing_consume_default_vue_vue"! = chunkId) { __webpack_require__.l(url, loadingEnded,"chunk-" + chunkId, chunkId);
            } else installedChunks[chunkId] = 0; }}};Copy the code

To sum up:

  1. First load main.js and inject it into HTML
  2. In main.js, you need to dynamically load remoteContentandButtonComponent, you need to load it firstremoteEntry.js
  3. inremoteEntry.jsthrough__webpack_require__.eloadingmoduleMapIn the building
  4. The traversal is executed__webpack_require__.fThe build dependency needs to be loaded firstwebpack_sharing_consume_default_vue_vue
  5. After loadingshardAfter the dependency is loaded againContentandButtoncomponent

My understanding of Module Federarion

At present, the official documents give several use cases, WHICH I think will definitely become the trend of front-end development in the future. Module Federation can be applied to a micro front end, but there is more to it than that.

You can imagine it when we can

  • Deploy each page of a single-page application independently
  • A large component library can be broken up into independently deployed components, and component updates only need to deploy their own components

It’s exciting to think about it.

Module Federation is a more elegant solution to the problem of common dependencies loading and sharing, which is something pedestal mode doesn’t handle very well.

There are also many Module Federation demo cases, such as

  • Implement vUE component loading in vuE3 project
  • Realize the SSR

You can see the implementation in the code repository.

But there are so many more questions

Try module Federation and you’ll get all kinds of errors that make you crash. I had to find a variety of solutions. Here are some of the problems I encountered:

Vue3 + vue CLI + webpack ^5.61.0

Uncaught (in promise) ScriptExternalLoadError: Loading script failed.
(missing: http://localhost:8000/remote.js)
while loading "./HelloWorld" from webpack/container/reference/app1
Copy the code

From the loaded screenshot, remote remote.js is loaded, but not subcontracted from remote.jssrc_components_HelloWorld_vue.js. However, it is not a problem to write the configuration directly without vue CLI. It is estimated that the vue CLI supports the problem.

The solution is to remove the subcontracting configuration, but this is also a temporary solution, which is definitely not good, but it is ok to give people a taste of the functionality.

chainWebpack: (config) = > {
        config.optimization.delete("splitChunks");
},
Copy the code

Shared libraries

For example, if we do not load the contents of the entry file asynchronously, an error is reported as follows:

Uncaught Error: Shared module is not available for eager consumption: webpack/sharing/consume/default/vue/vue
Copy the code

The solution is to create a new bootstrap.js file that contains the contents of the original entry JS file, and then asynchronously load the bootstrap.js file in the entry file, so that the code can run normally.

The specific code is as follows:

Entry file: main.js

// The load must be asynchronous
import("./bootstrap"); 
Copy the code

Bootstrap. js: The contents are the contents of the original entry file

import { createApp, defineAsyncComponent } from "vue";
import Layout from "./Layout.vue";

const Content = defineAsyncComponent(() = > import("home/Content"));
const Button = defineAsyncComponent(() = > import("home/Button"));

const app = createApp(Layout);

app.component("content-element", Content);
app.component("button-element", Button);

app.mount("#app");

Copy the code

This is because the remote remoteentry. js file needs to take precedence over the content loading execution in src_bootSTRap_js.js. If bootstrap.js is not asynchronously loaded and the original entry code is executed directly, but the original entry code depends on the remote JS code, and the remote JS code has not been loaded, an error is reported. As you can see from the size of the js file in the figure above, the code is moved from main.js to src_bootstrap_js.js, and the remoteentry.js is executed before src_bootstrap_js.js.