module federation运行时源码分析

背景

因为公司项目中用到webapck新特性 module federation(模块联邦),所以很好奇是怎么运行的,所以去研究了下运行时代码,故本文记录下。

基础概念

webpack.docschina.org/concepts/mo…

示例代码

Host代码

webpack.config.js
new ModuleFederationPlugin({
    name: "main",
    remotes: {
        com: "com@http://localhost:3003/remoteEntry.js",
    },
    shared: {
        react: {
            singleton: true,
        },
    },
 })
 
 
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"));

App.js
import React from "react";
import A from "com/a";

const App = () => {
  return (
    <div>
      this is host
      <A />
    </div>
  );
};

export default App;
复制代码

Remote代码

webpack.config.js
new ModuleFederationPlugin({
    name: "com",
    filename: "remoteEntry.js",
    exposes: {
        "./a": "./src/a",
    },
    shared: {
        react: {
            singleton: true,
        },
    },
})

a.js
import React from "react";

const A = () => {
  return <div>remote</div>;
};

export default A;
复制代码

源码分析

首先提出几个疑惑?

1. 为什么host入口文件用动态导入?
2. 怎么实现host与remote共享模块?

我们先看Host build产物的入口js

 "./src/index.js": (
      __unused_webpack_module,
      __unused_webpack_exports,
      __webpack_require__
    ) => {
      Promise.all([
        __webpack_require__.e("vendors-node_modules_react-dom_index_js"),
        __webpack_require__.e("src_bootstrap_js"),
      ]).then(
        __webpack_require__.bind(__webpack_require__, "./src/bootstrap.js")
      );
    }
复制代码

这里使用了__webpack_require__.e去加载了react-dom和src_bootstrap_js,react-dom是因为splitChunk的缘故;而src_bootstrap_js是因为我们入口文件是import(xxx)这种写法。

webpack_require.e干了什么?

    __webpack_require__.e = (chunkId) => {
      /**
      简单理解,就是并发的执行__webpack_require__.f上面的所有方法
      */
      return Promise.all(
        Object.keys(__webpack_require__.f).reduce((promises, key) => {
          __webpack_require__.f[key](chunkId, promises);
          return promises;
        }, [])
      );
    }
复制代码

webpack_require.f有哪些方法?

  1. webpack_require.f.j:这个是利用script标签去加载其他js文件,不是本次关注对象
  2. webpack_require.f.remotes:去加载remote入口文件,然后将remote模块关联到runtime中。按照示例代码就是将com/a关联到runtime中
  3. webpack_require.f.consumes:让当前share的模块,关联到runtime中 可知remotes和consumes是实现的关键,我们一个一个分析

remotes

    // map保存了文件中引用了哪些remote
    var chunkMapping = {
      src_bootstrap_js: ["webpack/container/remote/com/a"],
    };
    // 维护remote中的模块与remote的关系
    var idToExternalAndNameMapping = {
      "webpack/container/remote/com/a": [
        "default",
        "./a",
        "webpack/container/reference/com",
      ],
    };
    __webpack_require__.f.remotes = (chunkId, promises) => {
      // 判断当前id是否引用了remote
      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);
          var onError = (error) => {···};
          var handleFunction = (fn, arg1, arg2, d, next, first) => {···};
          var onExternal = (external, _, first) => ···;
          var onInitialized = (_, external, first) => ···;
          var onFactory = (factory) => ···;
          // data[2]是webpack/container/reference/com
          // 请问webpack/container/reference/com文件,然后执行onExternal
          handleFunction(__webpack_require__, data[2], 0, 0, onExternal, 1);
        });
      }
    };
复制代码

webpack/container/reference/com是什么?

 "webpack/container/reference/com":
      (module, __unused_webpack_exports, __webpack_require__) => {
            ···
        module.exports = new Promise((resolve, reject) => {
          if (typeof com !== "undefined") return resolve();
          // __webpack_require__.l 利用script标签,去加载js文件
          __webpack_require__.l(
            "http://localhost:3003/remoteEntry.js",
            (event) => {
              if (typeof com !== "undefined") return resolve();
              ···
            },
            "com"
          );
        }).then(() => com);
      }
复制代码

可以得出webpack/container/reference/com其实就是remote的入口js文件

onExternal,onInitialized,onFactory这些方法是啥? 本质就是

graph LR
A[请求remote文件] --> B[onExternal 执行 __webpack_require__.I 下面分析];
B --> C[onInitialized调用module.get 加载对应remote];
C --> D[onFactory 将 remote结果 关联 到runtime中]

所以__webpack_require__.f.remotes其实干了2件事情

  1. 加载remote入口文件
  2. 将当前模块用到的remote 都关联 到runtime上

consumes

    var moduleToHandlerMapping = {
      // share模块的一些信息
      "webpack/sharing/consume/default/react/react": () =>
        loadSingletonVersionCheckFallback(...),
    };
    // 当前模块share了哪些模块
    var chunkMapping = {
      src_bootstrap_js: ["webpack/sharing/consume/default/react/react"],
    };
    __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) => {...};
          var onError = (error) => {...};
          try {
            var promise = moduleToHandlerMapping[id]()            
            if (promise.then) {
              promises.push(
                (installedModules[id] = promise.then(onFactory).catch(onError))
              );
            }
            ...
        });
      }
    }
复制代码

由代码可以看出,其实就是执行了loadSingletonVersionCheckFallback方法

    var loadSingletonVersionCheckFallback = /*#__PURE__*/ init(
      (scopeName, scope, key, version, fallback) => {
        if (!scope || !__webpack_require__.o(scope, key)) return fallback();
        return getSingletonVersion(scope, scopeName, key, version);
      }
    );
    
    var init = (fn) =>
      function (scopeName, a, b, c) {
        var promise = __webpack_require__.I(scopeName);
        if (promise && promise.then)
          return promise.then(
            fn.bind(fn, scopeName, __webpack_require__.S[scopeName], a, b, c)
          );
        return fn(scopeName, __webpack_require__.S[scopeName], a, b, c);
      };
      
    var getSingletonVersion = (scope, scopeName, key, requiredVersion) => {
      // 获取对应版本信息
      var version = findSingletonVersionKey(scope, key);
      // 校验是否安全
      if (!satisfy(requiredVersion, version))
        typeof console !== "undefined" &&
          console.warn &&
          console.warn(
            getInvalidSingletonVersionMessage(key, version, requiredVersion)
          );
      // 返回share模块内容
      return get(scope[key][version]);
    }
复制代码

由代码可以看出loadSingletonVersionCheckFallback其实就是调用了init高阶函数,init调用了webpack_require.I(下面讲),在执行完后,执行getSingletonVersion方法,从而得到正确版本的share模块内容。 获取完share模块内容后,执行onFactory,将share模块,关联到runtime中。

webpack_require.I

其实就是执行了
    register(
        "react",
        "17.0.2",
         () => () =>
            __webpack_require__("./node_modules/react/index.js"),1);
    initExternal("webpack/container/reference/com");
复制代码

register方法将share模块信息记录到__webpack_require__.S上

__webpack_require__.S = {
    [scopeName: 默认为default]: {
        share模块名: {
            版本号: {
                get: factory(加载函数)
            }
        }
    }
}
复制代码

initExternal方法做了

  1. 等remoteEntry文件加载完成后
  2. 调用module.init方法,将__webpack_require__.S传入,module.init方法会消费__webpack_require__.S从而实现share模块共享

remoteEntry文件

    var com;
    ...
    /***/ "webpack/container/entry/com": /***/ (
      __unused_webpack_module,
      exports,
      __webpack_require__
    ) => {
      // remote中导出模块信息
      var moduleMap = {
        "./a": () => ...,
      };
      // 暴露remote中模块
      var get = (module, getScope) => {...};
      // 注册share模块到remote中
      var init = (shareScope, initScope) => {...};

      // 给exports上绑定get和init方法
      __webpack_require__.d(exports, {
        get: () => get,
        init: () => init,
      });
    }
    ...
    var __webpack_exports__ = __webpack_require__("webpack/container/entry/com");
    // 通过往全局作用域上注册变量
    com = __webpack_exports__;
复制代码

所以其实等remoteEntry文件文件加载完成后,全局作用域上会有com变量,它有get和init方法,可以让host使用。get方法其实就是将remote中模块暴露出去,init方法就是接收scope,从而实现share模块共享。

问题解答

问题1

所以我们上面的问题1的答案就是因为需要等remoteEntry文件加载完成之后,才能获取remote信息,所以需要在入口文件import,从而通过__webpack_require__.e方法先初始化remote信息,之后在开始加载本地代码,从而可以在本地加载remote模块。

问题2

问题2的答案就是,host先通过__webpack_require__.f.consumes方法在host中注册share模块到__webpack_require__.S中,然后等remoteEntry加载完后,将__webpack_require__.S中对应模块,传给module.init方法,module.init里面将share模块注册到remote中,从而实现host与remote共享模块。

总结

webpack federation是怎么运行的?

  1. 先并发的执行__webpack_require__.f.remotes和__webpack_require__.f.consumes方法
    • 在remotes方法中加载remoteEntry,加载完成后通过module.get方法将remote中模块注册到host中
    • 在consumes方法中先在host中注册share模块,然后等remoteEntry加载完成后,通过module.init方法将share模块传入,从而实现host和remote共享模块
  2. 在加载入口文件,正常执行host代码

第一次写文章,如果有不足之处,麻烦指出,谢谢。如果有收获,麻烦👍~~~

分类:
前端
标签: