webpack5之Module Federation

2,692 阅读6分钟

       在webpack5中增加了一个新功能Module Federation(联邦模块),那么它到底是什么呢?我们来一探究竟 

1.常规模式

在webpack5之前开发包一般有三种模式

常规lib安装

UMD安装

micro frontend(MFE)

上述三种方式使用广泛,优缺点就不再此赘述了。

2.Module Federation是什么

        最近一个项目中,有一个业务场景,需要把每一个组件都打成包来供使用,利用UMD虽然解决了问题,但是解决起来还是很难受,机缘巧合之下发现了webpack5的这个新功能Module Federation(联邦模块)功能入下图:

       这个功能是直接将一个应用的组件包,给另外一个应用来使用,同时支持和整体应用一起打包的公共抽取能力,类似于package center的应用。简单来说就是允许运行时动态决定代码的引入和加载。

2.2 demo

官方demo的地址:module-federation-examples

我们从中选择一个最基础的项目basic-host-remote进行安装,他的核心功能就是两个app互相引用对方的一个button组件,里边的目录大致如下

app1
---index.js 入口文件
---bootstrap.js 启动文件
---Button.js react组件
---App.js react组件 
app2
---index.js 入口文件
---bootstrap.js 启动文件
---App.js react组件
---Button.js react组件

看起来一样是不是。我们看一下实际的区别APP1的webpack config如下

new ModuleFederationPlugin({
      // 必填,唯一,给其他引用,类似于alias 引用为 app1/xxx
      name: "app1",
      // 构建后的chunk名字
      filename: "remoteEntry.js",
      // 引用方配置,下方为引用第三方文件配置
      remotes: {
        app2: "app2@http://localhost:3002/remoteEntry.js",
      },
      // 被引用方配置,暴露对外的module模块
      exposes: {
        "./Button": "./src/Button",
      },
      // 共享第三方资源
      shared: {
        ...deps,
        react: {
          singleton: true,
        },
        "react-dom": {
          singleton: true,
        },
      },
    })

App2的如下

new ModuleFederationPlugin({
      name: "app2",
      filename: "remoteEntry.js",
      remotes: {
        app1: "app1@http://localhost:3001/remoteEntry.js",
      },
      exposes: {
        "./Button": "./src/Button",
      },
      shared: {
        ...deps,
        react: {
          singleton: true,
        },
        "react-dom": {
          singleton: true,
        },
      },
    })

看完了配置,我们看下其他几个文件

/*APP.js*/
import LocalButton from "./Button";
import React from "react";

const RemoteButton = React.lazy(() => import("app1/Button"));

const App = () => (
  <div>
    <h1>Bi-Directional</h1>
    <h2>App 2</h2>
    <LocalButton />
    <React.Suspense fallback="Loading Button">
      <RemoteButton />
    </React.Suspense>
  </div>
);

export default App;

/*index.js*/
import("./bootstrap");

/*bootstrap.js*/
import App from "./App";
import React from "react";
import ReactDOM from "react-dom";

ReactDOM.render(<App />, document.getElementById("root"));

/*button.js*/

import React from "react";

const style = {
  background: "#00c",
  color: "#fff",
  padding: 12,
};

const Button = () => <button style={style}>App 2 Button</button>;

export default Button;

/** APP1 END **/

App2的区别只有以下一行代码:
const RemoteButton = React.lazy(() => import("app1/Button"));

实际的运行效果

看一下生成页面的代码

我们来看下这个main.js里有什么乾坤

main.js内容:

1.modules转换成webpack_modules,

var __webpack_modules__ = ({

/***/ "webpack/container/reference/app2":
/*!************************************************************!*\
  !*** external "app2@http://localhost:3002/remoteEntry.js" ***!
  \************************************************************/
/*! unknown exports (runtime-defined) */
/*! runtime requirements: module, __webpack_require__.l, __webpack_require__.* */
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {

"use strict";
eval("var error = new Error();
module.exports = new Promise((resolve, reject) => {
if(typeof app2 !== \"undefined\") 
return resolve(); 
__webpack_require__.l(\"http://localhost:3002/remoteEntry.js\", (event) => { 
    if(typeof app2 !== \"undefined\") return resolve();   
    var errorType = event && (event.type === 'load' ? 'missing' : event.type);   
    var realSrc = event && event.target && event.target.src;   
    error.message = 'Loading script failed.\\n(' + errorType + ': ' + realSrc + ')';   
    error.name = 'ScriptExternalLoadError';   error.type = errorType;   
    error.request = realSrc;  treject(error);  }, \"app2\");\n})
    .then(() => app2)\n\n//# 
    sourceURL=webpack://@automatic-vendor-sharing/app1/external_%22app2@http://localhost:3002/remoteEntry.js%22?");

})

});

2. 动态加载src_bootstrap_js

(() => {
/*!**********************!*\  
!*** ./src/index.js ***!  
\**********************//*! unknown exports (runtime-defined) *//*! runtime requirements: __webpack_require__.e, 
__webpack_require__, __webpack_require__.* */
eval("__webpack_require__.e(/*! import() */ \"src_bootstrap_js\")
.then(__webpack_require__.bind(__webpack_require__, 
/*! ./bootstrap */ \"./src/bootstrap.js\"));\n\n
//# sourceURL=webpack://@automatic-vendor-sharing/app1/./src/index.js?");})();

3.webpack_require.e做了什么呢

/* 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) => {
    return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
            __webpack_require__.f[key](chunkId, promises);
                return promises;
            }, []));
        };
    })();

4.又引出了f这个对象我们看下是什么

分别有三个方法,4.1:remotes:远程配置相关

__webpack_require__.f.remotes = (chunkId, promises) => 
{  
  if(__webpack_require__.o(chunkMapping, chunkId)) 
  {  
    chunkMapping[chunkId].forEach((id) => 
      {  
        if(installedModules[id]) 
        return promises.push(installedModules[id]);     
        var data = idToExternalAndNameMapping[id];  
        var onError = (error) => {    
        if(!error) error = new Error("Container missing");    
        if(typeof error.message === "string")  
        error.message += '\nwhile loading "' + data[1] + '" from ' + data[2];   
         __webpack_modules__[id] = () => {
           throw error;
           }
           installedModules[id] = 0;
           };
           var handleFunction = (fn, key, data, next, first) => {
             try {  
               var promise = fn(key, data);  
               if(promise && promise.then) {    
                 var p = promise.then((result) => next(result, data), onError);    
                 if(first) promises.push(installedModules[id] = p); else return p;  
                 } else {    
                   return next(promise, data, first);  
                   }    
                   } 
                   catch(error) {  onError(error);    }  }  var onExternal = (external, _, first) => external ? 
                   handleFunction(__webpack_require__.I, data[0], external, onInitialized, first) : onError();  
                   var onInitialized = (_, external, first) => handleFunction(external.get, data[1], external, 
                   onFactory, first);  var onFactory = (factory) => {    installedModules[id] = 1;    
                   __webpack_modules__[id] = (module) => {  module.exports = factory();    }  };  
                   handleFunction(__webpack_require__, data[2], 1, onExternal, 1);  });  }  }

4.2.consumes方法。从代码得知应该和配置的 shared 有关

var chunkMapping = {
    src_bootstrap_js: [
        "webpack/sharing/consume/default/react-dom/react-dom",
        "webpack/sharing/consume/default/react/react?2849"
    ],
    webpack_sharing_consume_default_react_react: [
        "webpack/sharing/consume/default/react/react?0085"
    ]
};
__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]);
            var onFactory = factory => {
                installedModules[id] = 0;
                __webpack_modules__[id] = module => {
                    delete __webpack_module_cache__[id];
                    module.exports = factory();
                };
            };
            var onError = error => {
                delete installedModules[id];
                __webpack_modules__[id] = module => {
                    delete __webpack_module_cache__[id];
                    throw error;
                };
            };
            try {
                var promise = moduleToHandlerMapping[id]();
                if (promise.then) {
                    promises.push(
                        (installedModules[id] = promise
                            .then(onFactory)
                            .catch(onError))
                    );
                } else onFactory(promise);
            } catch (e) {
                onError(e);
            }
        });
    }
};

4.3 webpack_require.f.j,从代码得知执行的就是JSONP加载chunk

__webpack_require__.f.j = (chunkId, promises) => {    // JSONP chunk loading for javascript    var installedChunkData = __webpack_require__.o(installedChunks, chunkId)        ? installedChunks[chunkId]        : undefined;    if (installedChunkData !== 0) {        // 0 means "already installed".        // a Promise means "currently loading".        if (installedChunkData) {            promises.push(installedChunkData[2]);        } else {            if (                !/^webpack_(container_remote_app2_Button|sharing_consume_default_react_react)$/.test(                    chunkId                )            ) {                // setup Promise in chunk cache                var promise = new Promise((resolve, reject) => {                    installedChunkData = installedChunks[chunkId] = [                        resolve,                        reject                    ];                });                promises.push((installedChunkData[2] = promise));                // start chunk loading                var url =                    __webpack_require__.p + __webpack_require__.u(chunkId);                // create error before stack unwound to get useful stacktrace later                var error = new Error();                var loadingEnded = event => {                    if (__webpack_require__.o(installedChunks, chunkId)) {                        installedChunkData = installedChunks[chunkId];                        if (installedChunkData !== 0)                            installedChunks[chunkId] = undefined;                        if (installedChunkData) {                            var errorType =                                event &&                                (event.type === "load"                                    ? "missing"                                    : event.type);                            var realSrc =                                event && event.target && event.target.src;                            error.message =                                "Loading chunk " +                                chunkId +                                " failed.\n(" +                                errorType +                                ": " +                                realSrc +                                ")";                            error.name = "ChunkLoadError";                            error.type = errorType;                            error.request = realSrc;                            installedChunkData[1](error);                        }                    }                };                __webpack_require__.l(url, loadingEnded, "chunk-" + chunkId);            } else installedChunks[chunkId] = 0;        }    }};

我们再来看下app1的remoteEntry:

var app1;app1 =
/******/ (() => { // webpackBootstrap
/******/    "use strict";
/******/    var __webpack_modules__ = ({

/***/ "webpack/container/entry/app1":
/*!***********************!*\
  !*** container entry ***!
  \***********************/
/*! unknown exports (runtime-defined) */
/*! runtime requirements: __webpack_require__.d, __webpack_require__.o, __webpack_exports__, __webpack_require__.e, __webpack_require__, __webpack_require__.* */
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
// eval打包后的代码
eval("var moduleMap = {\n\t\"./Button\": () => {\n\t\treturn __webpack_require__.e(\"src_Button_js\").then(() => () => (__webpack_require__(/*! ./src/Button */ \"./src/Button.js\")));\n\t}\n};\nvar get = (module) => {\n\treturn (\n\t\t__webpack_require__.o(moduleMap, module)\n\t\t\t? moduleMap[module]()\n\t\t\t: Promise.resolve().then(() => {\n\t\t\t\tthrow new Error('Module \"' + module + '\" does not exist in container.');\n\t\t\t})\n\t);\n};\nvar init = (shareScope) => {\n\tvar oldScope = __webpack_require__.S[\"default\"];\n\tvar name = \"default\"\n\tif(oldScope && oldScope !== shareScope) throw new Error(\"Container initialization failed as it has already been initialized with a different share scope\");\n\t__webpack_require__.S[name] = shareScope;\n\treturn __webpack_require__.I(name);\n};\n\n// This exports getters to disallow modifications\n__webpack_require__.d(exports, {\n\tget: () => get,\n\tinit: () => init\n});\n\n//# sourceURL=webpack://@automatic-vendor-sharing/app1/container_entry?");

/*类似mainjs里的代码结构*/

return __webpack_require__("webpack/container/entry/app1");/******/ })()

app2的

var app2;app2 =/*基本类似app1*/

var get = (module) => {

\n\treturn (\n\t\t__webpack_require__.o(moduleMap, module)\n\t\t\t? moduleMap[module]()\n\t\t\t: Promise.resolve().then(() => {\n\t\t\t\tthrow new Error('Module \"' + module + '\" does not exist in container.');\n\t\t\t})\n\t);\n};\nvar init = (shareScope) => {\n\tvar oldScope = __webpack_require__.S[\"default\"];\n\tvar name = \"default\"\n\tif(oldScope && oldScope !== shareScope) throw new Error(\"Container initialization failed as it has already been initialized with a different share scope\");\n\t__webpack_require__.S[name] = shareScope;\n\treturn __webpack_require__.I(name);\n};\n\n// This exports getters to disallow modifications\n__webpack_require__.d(exports, {\n\tget: () => get,\n\tinit: () => init\n});\n\n//# sourceURL=webpack://@automatic-vendor-sharing/app2/container_entry?");/***/ })
return __webpack_require__("webpack/container/entry/app2");/******/ })()

加载流程:

通过以上源码的分析,我们大致就知道实现的流程了

  1. 先加载 main.js
  2. main.js 里面引用index.js
  3. index.js 这个 module 动态加载 src_bootstrap_js 这个 chunk
  4. 动态加载 src_bootstrap_js 这个 chunk 时,经过 consumes,判断依赖了 哪些文件,查看是否加载,没有加载就去加载对应的 js 文件。同时,经过 remotes,加载动态依赖
  5. 最后,经过 jsonp操作完成加载。
  6. 所有依赖以及 chunk 都加载完成后执行bootstrap.js
  7. app1和app2的remoteEntry分别在内部定义的get方法来require需要的代码(app2.get('Button') )。

细心的你一定发现indexjs和appjs之间有一个bootstrapjs。如果我们不用它的话那么会提示

Error: Shared module is not available for eager consumption

官网给出的是We strongly recommend using an asynchronous boundary. It will split out the initialization code of a larger chunk to avoid any additional round trips and improve performance in general.

通过上边我们的代码分析也能得到结论,需要依赖前置并且require进来。

结语

通过以上的分析,我们可以看出,这个功能具备了让webpack实现线上Runtime的效果,可以直接CDN进行共享,也不再需要构建发布了。

由于一些原因,笔者这次的项目没有使用该功能共享npm包来完成项目的开发上线,但是未来在版本稳定后会尝试该功能并分享踩坑等经验出来。

注:由于编写时webpack5的正式版还没发布,所以上述代码均为webpack5.0.0-beta.22版本下的情况。