Skip to content

模块联邦打包后代码分析

一、案例

1. 远程模块

20221021151844

项目中分别导出两个工具方法

  • add.js
  • sub.js

模块联邦配置:

js
new ModuleFederationPlugin({
  name: "lib_app",
  filename: "remoteEntry.js",
  exposes: {
    "./add":"./src/add.js",
    "./sub":"./src/sub.js"
  }
})
new ModuleFederationPlugin({
  name: "lib_app",
  filename: "remoteEntry.js",
  exposes: {
    "./add":"./src/add.js",
    "./sub":"./src/sub.js"
  }
})

最后多产生了三个文件:

20221021152032

2. 本地项目

模块联邦配置如下:

js
new ModuleFederationPlugin({
  name: "main_app",
  remotes: {
    'lib-app': 'lib_app@http://localhost:3000/remoteEntry.js'
  }
})
new ModuleFederationPlugin({
  name: "main_app",
  remotes: {
    'lib-app': 'lib_app@http://localhost:3000/remoteEntry.js'
  }
})

示例代码:

js
import add from 'lib-app/add'

console.log(add(1, 2));
import add from 'lib-app/add'

console.log(add(1, 2));

二、remoteEntry 远程模块做了什么?

整个文件最后将导出的内容赋值给了lib_app

js
var lib_app;
(() => {
  // ...各种代码
  lib_app = __webpack_exports__;
})()
var lib_app;
(() => {
  // ...各种代码
  lib_app = __webpack_exports__;
})()

里面的内容重点有如下几部分

1. ensure chunk

用于加载chunk

js
(() => {
	__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;
		}, []));
	};
})();
(() => {
	__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;
		}, []));
	};
})();

2. 模块映射表

这里是模块联邦定义,需要暴露出去的模块

js
var moduleMap = {
	"./add": () => {
		return __webpack_require__.e("src_add_js").then(() => (() => ((__webpack_require__(/*! ./src/add.js */ "./src/add.js")))));
	},
	"./sub": () => {
		return __webpack_require__.e("src_sub_js").then(() => (() => ((__webpack_require__(/*! ./src/sub.js */ "./src/sub.js")))));
	}
};
var moduleMap = {
	"./add": () => {
		return __webpack_require__.e("src_add_js").then(() => (() => ((__webpack_require__(/*! ./src/add.js */ "./src/add.js")))));
	},
	"./sub": () => {
		return __webpack_require__.e("src_sub_js").then(() => (() => ((__webpack_require__(/*! ./src/sub.js */ "./src/sub.js")))));
	}
};

3. get加载某个模块

js
var get = (module, getScope) => {
	__webpack_require__.R = getScope;
	getScope = (
		__webpack_require__.o(moduleMap, module)
			? moduleMap[module]()
			: Promise.resolve().then(() => {
				throw new Error('Module "' + module + '" does not exist in container.');
			})
	);
	__webpack_require__.R = undefined;
	return getScope;
};
var get = (module, getScope) => {
	__webpack_require__.R = getScope;
	getScope = (
		__webpack_require__.o(moduleMap, module)
			? moduleMap[module]()
			: Promise.resolve().then(() => {
				throw new Error('Module "' + module + '" does not exist in container.');
			})
	);
	__webpack_require__.R = undefined;
	return getScope;
};
  1. 初始化共享模块
js
var init = (shareScope, initScope) => {
	if (!__webpack_require__.S) return;
	var name = "default"
	var oldScope = __webpack_require__.S[name];
	if(oldScope && oldScope !== shareScope) throw new Error("Container initialization failed as it has already been initialized with a different share scope");
	__webpack_require__.S[name] = shareScope;
	return __webpack_require__.I(name, initScope);
};
var init = (shareScope, initScope) => {
	if (!__webpack_require__.S) return;
	var name = "default"
	var oldScope = __webpack_require__.S[name];
	if(oldScope && oldScope !== shareScope) throw new Error("Container initialization failed as it has already been initialized with a different share scope");
	__webpack_require__.S[name] = shareScope;
	return __webpack_require__.I(name, initScope);
};

三、exposes的模块做了什么

我们发现,其实exposes中的模块,都被单独打包成了chunk

js
(self["webpackChunkremote"] = self["webpackChunkremote"] || []).push([["src_add_js"],{

/***/ "./src/add.js":
/*!********************!*\
  !*** ./src/add.js ***!
  \********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "default": () => (/* binding */ add)
/* harmony export */ });
function add(a, b) {
  return a + b
}

/***/ })

}]);
(self["webpackChunkremote"] = self["webpackChunkremote"] || []).push([["src_add_js"],{

/***/ "./src/add.js":
/*!********************!*\
  !*** ./src/add.js ***!
  \********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "default": () => (/* binding */ add)
/* harmony export */ });
function add(a, b) {
  return a + b
}

/***/ })

}]);

四、本地模块是如何引入远程模块的

先来看下打包后的代码,下面只列出关键点

js
var __webpack_modules__ = ({
  "webpack/container/reference/lib-app": ((module, __unused_webpack_exports, __webpack_require__) => {
      // 创建一个error对象
      var __webpack_error__ = new Error();
      module.exports = new Promise((resolve, reject) => {
        if(typeof lib_app !== "undefined") return resolve();
        __webpack_require__.l("http://localhost:3000/remoteEntry.js", (event) => {
          if(typeof lib_app !== "undefined") return resolve();
          var errorType = event && (event.type === 'load' ? 'missing' : event.type);
          var realSrc = event && event.target && event.target.src;
          __webpack_error__.message = 'Loading script failed.\n(' + errorType + ': ' + realSrc + ')';
          __webpack_error__.name = 'ScriptExternalLoadError';
          __webpack_error__.type = errorType;
          __webpack_error__.request = realSrc;
          reject(__webpack_error__);
        }, "lib_app");
      }).then(() => (lib_app));
    })
})

// ...其他代码
__webpack_require__(/*! lib-app/add */ "webpack/container/remote/lib-app/add")
// ...其他代码
var __webpack_modules__ = ({
  "webpack/container/reference/lib-app": ((module, __unused_webpack_exports, __webpack_require__) => {
      // 创建一个error对象
      var __webpack_error__ = new Error();
      module.exports = new Promise((resolve, reject) => {
        if(typeof lib_app !== "undefined") return resolve();
        __webpack_require__.l("http://localhost:3000/remoteEntry.js", (event) => {
          if(typeof lib_app !== "undefined") return resolve();
          var errorType = event && (event.type === 'load' ? 'missing' : event.type);
          var realSrc = event && event.target && event.target.src;
          __webpack_error__.message = 'Loading script failed.\n(' + errorType + ': ' + realSrc + ')';
          __webpack_error__.name = 'ScriptExternalLoadError';
          __webpack_error__.type = errorType;
          __webpack_error__.request = realSrc;
          reject(__webpack_error__);
        }, "lib_app");
      }).then(() => (lib_app));
    })
})

// ...其他代码
__webpack_require__(/*! lib-app/add */ "webpack/container/remote/lib-app/add")
// ...其他代码

从上面的代码中,不难发现lib_app其实是, 远程模块暴露出的变量。这一点从上面的分析就可以看到。

js
var lib_app;
(() => {
  // ...各种代码
  lib_app = __webpack_exports__;
})()
var lib_app;
(() => {
  // ...各种代码
  lib_app = __webpack_exports__;
})()

关键点在这一行, 最后返回了lib_app

js
__webpack_require__.l("http://localhost:3000/remoteEntry.js", (event) => {
  // 一些错误处理

}, 'lib_app').then(() => (lib_app));
__webpack_require__.l("http://localhost:3000/remoteEntry.js", (event) => {
  // 一些错误处理

}, 'lib_app').then(() => (lib_app));

loadJs加载远程模块

下面的代码就很简单了,直接创建了script标签,最后触发onload事件,执行最终的回调.

比如我们导入了lib-app/add, 就加载add.js的chunk

js
__webpack_require__.l = (url, done, key, chunkId) => {
		if(inProgress[url]) { inProgress[url].push(done); return; }
		var script, needAttach;
		if(key !== undefined) {
			var scripts = document.getElementsByTagName("script");
			for(var i = 0; i < scripts.length; i++) {
				var s = scripts[i];
				if(s.getAttribute("src") == url || s.getAttribute("data-webpack") == dataWebpackPrefix + key) { script = s; break; }
			}
		}
		if(!script) {
			needAttach = true;
			script = document.createElement('script');
	
			script.charset = 'utf-8';
			script.timeout = 120;
			if (__webpack_require__.nc) {
				script.setAttribute("nonce", __webpack_require__.nc);
			}
			script.setAttribute("data-webpack", dataWebpackPrefix + key);
			script.src = url;
		}
		inProgress[url] = [done];
		var onScriptComplete = (prev, event) => {
			// avoid mem leaks in IE.
			script.onerror = script.onload = null;
			clearTimeout(timeout);
			var doneFns = inProgress[url];
			delete inProgress[url];
			script.parentNode && script.parentNode.removeChild(script);
			doneFns && doneFns.forEach((fn) => (fn(event)));
			if(prev) return prev(event);
		}
		;
		var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000);
		script.onerror = onScriptComplete.bind(null, script.onerror);
		script.onload = onScriptComplete.bind(null, script.onload);
		needAttach && document.head.appendChild(script);
	};
})();
__webpack_require__.l = (url, done, key, chunkId) => {
		if(inProgress[url]) { inProgress[url].push(done); return; }
		var script, needAttach;
		if(key !== undefined) {
			var scripts = document.getElementsByTagName("script");
			for(var i = 0; i < scripts.length; i++) {
				var s = scripts[i];
				if(s.getAttribute("src") == url || s.getAttribute("data-webpack") == dataWebpackPrefix + key) { script = s; break; }
			}
		}
		if(!script) {
			needAttach = true;
			script = document.createElement('script');
	
			script.charset = 'utf-8';
			script.timeout = 120;
			if (__webpack_require__.nc) {
				script.setAttribute("nonce", __webpack_require__.nc);
			}
			script.setAttribute("data-webpack", dataWebpackPrefix + key);
			script.src = url;
		}
		inProgress[url] = [done];
		var onScriptComplete = (prev, event) => {
			// avoid mem leaks in IE.
			script.onerror = script.onload = null;
			clearTimeout(timeout);
			var doneFns = inProgress[url];
			delete inProgress[url];
			script.parentNode && script.parentNode.removeChild(script);
			doneFns && doneFns.forEach((fn) => (fn(event)));
			if(prev) return prev(event);
		}
		;
		var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000);
		script.onerror = onScriptComplete.bind(null, script.onerror);
		script.onload = onScriptComplete.bind(null, script.onload);
		needAttach && document.head.appendChild(script);
	};
})();

五、本地模块如何执行远程模块的

回顾一下示例代码:

js
import add from 'lib-app/add'

console.log(add(1, 2));
import add from 'lib-app/add'

console.log(add(1, 2));

被编译成了:

var lib_app_add__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! lib-app/add */ "webpack/container/remote/lib-app/add");
var lib_app_add__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(lib_app_add__WEBPACK_IMPORTED_MODULE_0__);

console.log(lib_app_add__WEBPACK_IMPORTED_MODULE_0___default()(1, 2));

最后直接调用了方法

拉取远程代码

  1. 获取当前chunk依赖的远程模块
  2. 根据远程模块,获取远程应用的ID, 然后webpack_require对应的模块代码,此时会去加载远程应用的入口文件

消费公共模块 获取拉取公共模块,并注册到modules中

拉取入口chunk代码