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__.l
Function withscript
Label 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:
- First load main.js and inject it into HTML
- In main.js, you need to dynamically load remote
Content
andButton
Component, you need to load it firstremoteEntry.js
- in
remoteEntry.js
through__webpack_require__.e
loadingmoduleMap
In the building - The traversal is executed
__webpack_require__.f
The build dependency needs to be loaded firstwebpack_sharing_consume_default_vue_vue
- After loading
shard
After the dependency is loaded againContent
andButton
component
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.