在webpack中模块联邦的实现主要依赖于两个插件ContainerReferencePlugin和ContainerPlugin,ContainerPlugin是用来添加入口依赖并给当前依赖添加异步依赖,ContainerReferencePlugin用来添加解析用户的请求并分析是否是远程模块,然后加载远程模块。接下来我们看看源码是如何实现的。
const path = require('path');
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
mode: 'development',
plugins: [
new ModuleFederationPlugin({
// 指定导出的容器名称
name: "app_exposes",
library:{ type: "var", name: "app_exposes" },
// 打包生成的文件名称
filename: "remoteEntry.js",
// 导出对应的模块
exposes: {
"./math": "./src/math.js",
},
})
],
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
clean:true
},
};
const ContainerPlugin = require('./ContainerPlugin')
class TestPlugin {
constructor(options) {
this._options = options
}
apply(compiler) {
const options = this._options
// 添加当前允许的变量挂载类型window,var,self,this去接收__webpack_exports__
compiler.options.output.enabledLibraryTypes.push(options.library.type)
// 在初始化内部插件集合完成设置之后调用
compiler.hooks.afterPlugins.tap("TestPlugin", () => {
new ContainerPlugin({
name: options.name,
library:options.library,
filename: options.filename,
runtime: options.runtime,
exposes: options.exposes
}).apply(compiler);
});
}
}
module.exports = TestPlugin
主要执行了ContainerPlugin插件:
class ContainerPlugin {
/**
* @param {ContainerPluginOptions} options options
*/
constructor(options) {
this._options = {
name: options.name,
shareScope: options.shareScope || "default",
// 全局变量接收__webpack_exports__
library: options.library || {
type: "var",
name: options.name
},
runtime: options.runtime,
filename: options.filename || undefined,
exposes: parseOptions(
options.exposes,
item => ({
import: Array.isArray(item) ? item : [item],
name: undefined
}),
item => ({
import: Array.isArray(item.import) ? item.import : [item.import],
name: item.name || undefined
})
)
};
}
/**
* Apply the plugin
* @param {Compiler} compiler the compiler instance
* @returns {void}
*/
apply(compiler) {
const { name, exposes, shareScope, filename, library, runtime } =
this._options;
compiler.options.output.enabledLibraryTypes.push(library.type);
compiler.hooks.make.tapAsync(PLUGIN_NAME, (compilation, callback) => {
const dep = new ContainerEntryDependency(name, exposes, shareScope);
dep.loc = { name };
compilation.addEntry(
compilation.options.context,
dep,
{
name,
filename,// 输出的文件名
runtime,
library
},
error => {
if (error) return callback(error);
callback();
}
);
});
compiler.hooks.thisCompilation.tap(
PLUGIN_NAME,
(compilation, { normalModuleFactory }) => {
// 设置remoteEntry.js的依赖和创造remoteEntry的工厂
compilation.dependencyFactories.set(
ContainerEntryDependency,
new ContainerEntryModuleFactory()// ContainerEntryModule 依赖ContainerExposedDependency
);
// 设置依赖的模块创造工厂
compilation.dependencyFactories.set(
ContainerExposedDependency,// math
normalModuleFactory // 普通模块
);
}
);
}
}
可以看出通过addEntry方法添加了一个入口,并设置了依赖ContainerEntryDependency,然后在thisCompilation中设置了ContainerEntryDependency依赖的创造模块工厂ContainerEntryModuleFactory。也就意味着会先调用ContainerEntryModuleFactory函数的ContainerEntryModule去生成模块。
module.exports = class ContainerEntryModuleFactory extends ModuleFactory {
/**
* @param {ModuleFactoryCreateData} data data object
* @param {function(Error=, ModuleFactoryResult=): void} callback callback
* @returns {void}
*/
create({ dependencies: [dependency] }, callback) {
const dep = /** @type {ContainerEntryDependency} */ (dependency);
callback(null, {
module: new ContainerEntryModule(dep.name, dep.exposes, dep.shareScope)
});
}
};
我们重点看ContainerEntryModule方法的build方法和codeGeneration方法。
/** 给当前模块添加依赖,设置变量
* @param {WebpackOptions} options webpack options
* @param {Compilation} compilation the compilation
* @param {ResolverWithOptions} resolver the resolver
* @param {InputFileSystem} fs the file system
* @param {function(WebpackError=): void} callback callback function
* @returns {void}
*/
build(options, compilation, resolver, fs, callback) {
this.buildMeta = {};
// 设置三个变量
this.buildInfo = {
strict: true,
topLevelDeclarations: new Set(["moduleMap", "get", "init"])
};
this.buildMeta.exportsType = "namespace";
this.clearDependenciesAndBlocks();
// remoteEntry.js依赖于main.js,此时AsyncDependenciesBlock表示公开模块来创建一个额外的容器入口。客户端会通过jsonp加载
for (const [name, options] of this._exposes) {
// 添加异步模块
const block = new AsyncDependenciesBlock(
{
name: options.name
},
{ name },
options.import[options.import.length - 1]
);
let idx = 0;
for (const request of options.import) {
// name=math.js
const dep = new ContainerExposedDependency(name, request);
dep.loc = {
name,
index: idx++
};
// build过程中设置异步模块的依赖(ContainerExposedDependency)
block.addDependency(dep);
}
// 添加异步模块,会把math.js单独打包出来,同时会在remoteEntry.js文件中引入运行时的代码
this.addBlock(block);
}
// 设置导出依赖
this.addDependency(new StaticExportsDependency(["get", "init"], false));
callback();
}
在执行build方法过程中会给remoteEntry模块添加一个异步依赖math.js。再来看看codeGeneration执行过程:
/**
* @param {CodeGenerationContext} context context for code generation
* @returns {CodeGenerationResult} result
*/
codeGeneration({ moduleGraph, chunkGraph, runtimeTemplate }) {
const sources = new Map();
const runtimeRequirements = new Set([
// __webpack_require__.d
RuntimeGlobals.definePropertyGetters,
// __webpack_require__.o
RuntimeGlobals.hasOwnProperty,
// __webpack_exports__
RuntimeGlobals.exports
]);
const getters = [];
// 给webpack/container/entry/app_exposes模块新增'"./math": () => {\n\treturn __webpack_require__.e("src_math_js").then(() => (() => ((__webpack_require__(/*! ./src/math.js */ "./src/math.js")))));\n}'
for (const block of this.blocks) {
const { dependencies } = block;
// 取出异步模块
const modules = dependencies.map(dependency => {
const dep = /** @type {ContainerExposedDependency} */ (dependency);
return {
name: dep.exposedName,
module: moduleGraph.getModule(dep),
request: dep.userRequest
};
});
let str;
// 获取要请求的模块
if (modules.some(m => !m.module)) {
str = runtimeTemplate.throwMissingModuleErrorBlock({
request: modules.map(m => m.request).join(", ")
});
} else {
// runtimeTemplate.blockPromise=>__webpack_require__.e
str = `return ${runtimeTemplate.blockPromise({
block,
message: "",
chunkGraph,
runtimeRequirements
// runtimeTemplate.returningFunction () => ()
})}.then(${runtimeTemplate.returningFunction(
runtimeTemplate.returningFunction(
`(${modules
.map(({ module, request }) =>
// runtimeTemplate.moduleRaw =>__webpack_require__(/*! ./src/math.js */ "./src/math.js")
runtimeTemplate.moduleRaw({
module,
chunkGraph,
request,
weak: false,
runtimeRequirements
})
)
.join(", ")})`
)
)});`;
}
getters.push(
`${JSON.stringify(modules[0].name)}: ${runtimeTemplate.basicFunction(
"",
[str]
)}`
);
}
// remoteEntry.js文件的内容
const source = Template.asString([
`var moduleMap = {`,
Template.indent(getters.join(",\n")),
"};",
// 定义remotEntry.js的get
`var get = ${runtimeTemplate.basicFunction("module, getScope", [
`${RuntimeGlobals.currentRemoteGetScope} = getScope;`,
// reusing the getScope variable to avoid creating a new var (and module is also used later)
"getScope = (",
Template.indent([
`${RuntimeGlobals.hasOwnProperty}(moduleMap, module)`,
Template.indent([
"? moduleMap[module]()",
`: Promise.resolve().then(${runtimeTemplate.basicFunction(
"",
"throw new Error('Module \"' + module + '\" does not exist in container.');"
)})`
])
]),
");",
`${RuntimeGlobals.currentRemoteGetScope} = undefined;`,
"return getScope;"
])};`,
// 定义remotEntry.js的init
`var init = ${runtimeTemplate.basicFunction("shareScope, initScope", [
`if (!${RuntimeGlobals.shareScopeMap}) return;`,
`var name = ${JSON.stringify(this._shareScope)}`,
`var oldScope = ${RuntimeGlobals.shareScopeMap}[name];`,
`if(oldScope && oldScope !== shareScope) throw new Error("Container initialization failed as it has already been initialized with a different share scope");`,
`${RuntimeGlobals.shareScopeMap}[name] = shareScope;`,
`return ${RuntimeGlobals.initializeSharing}(name, initScope);`
])};`,
"",
"// This exports getters to disallow modifications",
`${RuntimeGlobals.definePropertyGetters}(exports, {`,
Template.indent([
`get: ${runtimeTemplate.returningFunction("get")},`,
`init: ${runtimeTemplate.returningFunction("init")}`
]),
"});"
]);
sources.set(
"javascript",
this.useSourceMap || this.useSimpleSourceMap
? new OriginalSource(source, "webpack/container-entry")
: new RawSource(source)
);
return {
sources,
// remotentry添加运行时函数
runtimeRequirements
};
}
这里主要是设置remoteEntry.js中的webpack/container/entry/app_exposes模块内容:
var moduleMap = {
"./math": () => {
return __webpack_require__
.e("src_math_js")
.then(
() => () => __webpack_require__(/*! ./src/math.js */ "./src/math.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 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);
};
// This exports getters to disallow modifications
__webpack_require__.d(exports, {
get: () => get,
init: () => init,
});
//# sourceURL=webpack://webpack/container_entry?
此时我们打包一下:
会生成三个文件,bundle.js文件就是我们配置的entry文件入口。
remoteEntry.js文件是我们新增加的入口,webpack/container/entry/app_exposes
依赖就是此处的dep.。
math.js文件是remoteEntry.js文件中webpack/container/entry/app_exposes
的依赖。该依赖的添加是在webpack/container/entry/app_exposes
模块的build阶段:
此时ContainerPlugin插件做的事情我们就分析清楚了。接下来分析ContainerReferencePlugin插件。
const path = require('path');
module.exports = {
mode: 'development',
plugins: [
new ModuleFederationPlugin({
// 引用的外部资源列表
remotes: {
app_exposes: 'app_exposes@http://localhost:8081/remoteEntry.js',
}
})
],
entry: './src/index2.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true
},
};
// index2.js
import('app_exposes/math')
主要执行的是ModuleFederationPlugin插件:
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra and Zackary Jackson @ScriptedAlchemy
*/
"use strict";
const ContainerReferencePlugin = require("./ContainerReferencePlugin");
class ModuleFederationPlugin {
/**
* @param {ModuleFederationPluginOptions} options options
*/
constructor(options) {
this._options = options;
}
/**
* Apply the plugin
* @param {Compiler} compiler the compiler instance
* @returns {void}
*/
apply(compiler) {
const { _options: options } = this;
const library = { type: "var", name: options.name };
const remoteType = "script";
compiler.options.output.enabledLibraryTypes.push(library.type);
compiler.hooks.afterPlugins.tap("ModuleFederationPlugin", () => {
new ContainerReferencePlugin({
remoteType,
remotes: options.remotes
}).apply(compiler);
});
}
}
module.exports = ModuleFederationPlugin;
该插件主要执行ContainerReferencePlugin插件,我们继续看:
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra and Zackary Jackson @ScriptedAlchemy
*/
"use strict";
const ExternalsPlugin = require("webpack/lib/ExternalsPlugin");
const RuntimeGlobals = require("webpack/lib/RuntimeGlobals");
const FallbackDependency = require("webpack/lib/container/FallbackDependency");
const FallbackItemDependency = require("webpack/lib/container/FallbackItemDependency");
const FallbackModuleFactory = require("webpack/lib/container/FallbackModuleFactory");
const RemoteModule = require("webpack/lib/container/RemoteModule");
const RemoteRuntimeModule = require("webpack/lib/container/RemoteRuntimeModule");
const RemoteToExternalDependency = require("webpack/lib/container/RemoteToExternalDependency");
const { parseOptions } = require("webpack/lib/container/options");
const slashCode = "/".charCodeAt(0);
class ContainerReferencePlugin {
/**
* @param {ContainerReferencePluginOptions} options options
*/
constructor(options) {
this._remoteType = options.remoteType;
this._remotes = parseOptions(
options.remotes,
item => ({
external: Array.isArray(item) ? item : [item],
shareScope: options.shareScope || "default"
}),
item => ({
external: Array.isArray(item.external)
? item.external
: [item.external],
shareScope: item.shareScope || options.shareScope || "default"
})
);
}
/** 1. 定义了普通模块
* Apply the plugin
* @param {Compiler} compiler the compiler instance
* @returns {void}
*/
apply(compiler) {
const { _remotes: remotes, _remoteType: remoteType } = this;
/** @type {Record} */
const remoteExternals = {};
for (const [key, config] of remotes) {
let i = 0;
for (const external of config.external) {
if (external.startsWith("internal ")) continue;
remoteExternals[
`webpack/container/reference/${key}${i ? `/fallback-${i}` : ""}`
] = external;
i++;
}
}
// ExternalModuleFactoryPlugin=>ExternalModule最终返回webpack/container/reference/app_exposes的模块内容
new ExternalsPlugin(remoteType, remoteExternals).apply(compiler);
compiler.hooks.compilation.tap(
"ContainerReferencePlugin",
(compilation, { normalModuleFactory }) => {
//
compilation.dependencyFactories.set(
RemoteToExternalDependency,
normalModuleFactory
);
// 定义了普通模块
compilation.dependencyFactories.set(
FallbackItemDependency,
normalModuleFactory
);
// 当前模块依赖于FallbackDependency
compilation.dependencyFactories.set(
FallbackDependency,
new FallbackModuleFactory() // FallbackModule依赖于FallbackItemDependency
);
// 4. 第一次解析(type=module)'./src/index2.js' 第二次'app_exposes/math'
normalModuleFactory.hooks.factorize.tap(
"ContainerReferencePlugin",
data => {
if (!data.request.includes("!")) {
for (const [key, config] of remotes) {
// 在解析依赖的时候如果以app_exposes开头并且有'\'
if (
data.request.startsWith(`${key}`) &&
(data.request.length === key.length ||
data.request.charCodeAt(key.length) === slashCode)
) {
// app_exposes/math依赖于RemoteToExternalDependency
return new RemoteModule(
data.request,
config.external.map((external, i) =>
external.startsWith("internal ")
? external.slice(9)
: `webpack/container/reference/${key}${i ? `/fallback-${i}` : ""
}`
),
`.${data.request.slice(key.length)}`,
config.shareScope
);
}
}
}
}
);
// 添加运行时代码
compilation.hooks.runtimeRequirementInTree
.for(RuntimeGlobals.ensureChunkHandlers)
.tap("ContainerReferencePlugin", (chunk, set) => {
set.add(RuntimeGlobals.module);
set.add(RuntimeGlobals.moduleFactoriesAddOnly);
set.add(RuntimeGlobals.hasOwnProperty);
set.add(RuntimeGlobals.initializeSharing);
set.add(RuntimeGlobals.shareScopeMap);
compilation.addRuntimeModule(chunk, new RemoteRuntimeModule());
});
}
);
}
}
module.exports = ContainerReferencePlugin;
首先执行的是ExternalsPlugin插件,其次执行的compiler.hooks.compilation.tap
回调方法。compiler.hooks.compilation.tap
主要设置了FallbackDependency依赖的模块创造函数,其次监听了normalModuleFactory.hooks.factorize
钩子,该钩子是在解析依赖路径的时候会触发。我们在来看看ExternalsPlugin插件干啥了。
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
"use strict";
const ExternalModuleFactoryPlugin = require("./ExternalModuleFactoryPlugin");
/** @typedef {import("../declarations/WebpackOptions").Externals} Externals */
/** @typedef {import("./Compiler")} Compiler */
class ExternalsPlugin {
/**
* @param {string | undefined} type default external type
* @param {Externals} externals externals config
*/
constructor(type, externals) {
this.type = type;
this.externals = externals;
}
/** compile先于compilation执行
* Apply the plugin
* @param {Compiler} compiler the compiler instance
* @returns {void}
*/
apply(compiler) {// beforeCompile 之后立即调用,但在一个新的 compilation 创建之前
compiler.hooks.compile.tap("ExternalsPlugin", ({ normalModuleFactory }) => {
new ExternalModuleFactoryPlugin(this.type, this.externals).apply(
normalModuleFactory
);
});
}
}
module.exports = ExternalsPlugin;
主要执行ExternalModuleFactoryPlugin插件:
...
class ExternalModuleFactoryPlugin {
/**
* @param {string | undefined} type default external type
* @param {Externals} externals externals config
*/
constructor(type, externals) {
this.type = type;
this.externals = externals;
}
/**
* @param {NormalModuleFactory} normalModuleFactory the normal module factory
* @returns {void}
*/
apply(normalModuleFactory) {
const globalType = this.type;// 2.第一次解析(type=module)'./src/index2.js',3.第二次解析(type=module)'app_exposes/math',6.第三次解析(type=module)'webpack/container/reference/app_exposes' 第四次解析(type=script)'webpack/container/reference/app_exposes'
normalModuleFactory.hooks.factorize.tapAsync(
"ExternalModuleFactoryPlugin",
(data, callback) => {
const context = data.context;
const contextInfo = data.contextInfo;
const dependency = data.dependencies[0];
const dependencyType = data.dependencyType;
/** value 'app_exposes@http://localhost:8081/remoteEntry.js'
* @param {string|string[]|boolean|Record} value the external config
* @param {string|undefined} type type of external
* @param {function(Error=, ExternalModule=): void} callback callback
* @returns {void}
*/
const handleExternal = (value, type, callback) => {
if (value === false) {
// Not externals, fallback to original factory
return callback();
}
/** @type {string | string[] | Record} */
let externalConfig;
if (value === true) {
externalConfig = dependency.request;
} else {
externalConfig = value;
}
// When no explicit type is specified, extract it from the externalConfig
if (type === undefined) {
if (
typeof externalConfig === "string" &&
UNSPECIFIED_EXTERNAL_TYPE_REGEXP.test(externalConfig)
) {
const idx = externalConfig.indexOf(" ");
type = externalConfig.slice(0, idx);
externalConfig = externalConfig.slice(idx + 1);
} else if (
Array.isArray(externalConfig) &&
externalConfig.length > 0 &&
UNSPECIFIED_EXTERNAL_TYPE_REGEXP.test(externalConfig[0])
) {
const firstItem = externalConfig[0];
const idx = firstItem.indexOf(" ");
type = firstItem.slice(0, idx);
externalConfig = [
firstItem.slice(idx + 1),
...externalConfig.slice(1)
];
}
}
callback(
null,
new ExternalModule(// 8.最终执行
externalConfig,
type || globalType,
dependency.request
)
);
};
/**
* @param {Externals} externals externals config
* @param {function((Error | null)=, ExternalModule=): void} callback callback
* @returns {void}
*/
const handleExternals = (externals, callback) => {
if (typeof externals === "string") {
if (externals === dependency.request) {
return handleExternal(dependency.request, undefined, callback);
}
} else if (Array.isArray(externals)) {
let i = 0;
const next = () => {
let asyncFlag;
const handleExternalsAndCallback = (err, module) => {
if (err) return callback(err);
if (!module) {
if (asyncFlag) {
asyncFlag = false;
return;
}
return next();
}
callback(null, module);
};
do {
asyncFlag = true;
if (i >= externals.length) return callback();
handleExternals(externals[i++], handleExternalsAndCallback);
} while (!asyncFlag);
asyncFlag = false;
};
next();
return;
} else if (externals instanceof RegExp) {
if (externals.test(dependency.request)) {
return handleExternal(dependency.request, undefined, callback);
}
} else if (typeof externals === "function") {
const cb = (err, value, type) => {
if (err) return callback(err);
if (value !== undefined) {
handleExternal(value, type, callback);
} else {
callback();
}
};
if (externals.length === 3) {
// TODO webpack 6 remove this
callDeprecatedExternals(
externals,
context,
dependency.request,
cb
);
} else {
const promise = externals(
{
context,
request: dependency.request,
dependencyType,
contextInfo,
getResolve: options => (context, request, callback) => {
const resolveContext = {
fileDependencies: data.fileDependencies,
missingDependencies: data.missingDependencies,
contextDependencies: data.contextDependencies
};
let resolver = normalModuleFactory.getResolver(
"normal",
dependencyType
? cachedSetProperty(
data.resolveOptions || EMPTY_RESOLVE_OPTIONS,
"dependencyType",
dependencyType
)
: data.resolveOptions
);
if (options) resolver = resolver.withOptions(options);
if (callback) {
resolver.resolve(
{},
context,
request,
resolveContext,
callback
);
} else {
return new Promise((resolve, reject) => {
resolver.resolve(
{},
context,
request,
resolveContext,
(err, result) => {
if (err) reject(err);
else resolve(result);
}
);
});
}
}
},
cb
);
if (promise && promise.then) promise.then(r => cb(null, r), cb);
}
return;// {webpack/container/reference/app_exposes: 'app_exposes@http://localhost:8081/remoteEntry.js'}
} else if (typeof externals === "object") {
const resolvedExternals = resolveLayer(
externals,
contextInfo.issuerLayer
);
if (
Object.prototype.hasOwnProperty.call(
resolvedExternals,
dependency.request
)
) {// 7.最终执行
return handleExternal(
resolvedExternals[dependency.request],
undefined,
callback
);
}
}
callback();
};
handleExternals(this.externals, callback);
}
);
}
}
module.exports = ExternalModuleFactoryPlugin;
该插件主要执行normalModuleFactory.hooks.factorize
监听解析路径,主要是用来处理webpack/container/reference/app_exposes
依赖,最终执行ExternalModule模块。我们看看ExternalModule模块返回了什么内容。
const getSourceForScriptExternal = (urlAndGlobal, runtimeTemplate) => {
if (typeof urlAndGlobal === "string") {
urlAndGlobal = extractUrlAndGlobal(urlAndGlobal);
}// 'http://localhost:8081/remoteEntry.js'
const url = urlAndGlobal[0];
const globalName = urlAndGlobal[1];
return {
init: "var __webpack_error__ = new Error();",
expression: `new Promise(${runtimeTemplate.basicFunction(
"resolve, reject",
[
`if(typeof ${globalName} !== "undefined") return resolve();`,
`${RuntimeGlobals.loadScript}(${JSON.stringify(
url
)}, ${runtimeTemplate.basicFunction("event", [
`if(typeof ${globalName} !== "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__);"
])}, ${JSON.stringify(globalName)});`
]
)}).then(${runtimeTemplate.returningFunction(
`${globalName}${propertyAccess(urlAndGlobal, 2)}`
)})`,
runtimeRequirements: RUNTIME_REQUIREMENTS_FOR_SCRIPT
};
};
打包后生成:
/***/ "webpack/container/reference/app_exposes":
/*!*******************************************************************!*\
!*** external "app_exposes@http://localhost:8081/remoteEntry.js" ***!
\*******************************************************************/
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {
"use strict";
var __webpack_error__ = new Error();
module.exports = new Promise((resolve, reject) => {
// 已有全局变量app_exposes说明已加载
if(typeof app_exposes !== "undefined") return resolve();
__webpack_require__.l("http://localhost:8081/remoteEntry.js", (event) => {
if(typeof app_exposes !== "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__);
}, "app_exposes");
}).then(() => (app_exposes));
/***/ })
可以看出是请求远程的remoteEntry.js模块。我们再来看看解析app_exposes/math
的时候是怎么处理的。当解析app_expose/math路径的时候会执行返回RemoteModule模块内容。
我们重点看RemoteModule模块生成内容:
build(options, compilation, resolver, fs, callback) {
this.buildMeta = {};
this.buildInfo = {
strict: true
};
// 5. 添加'webpack/container/reference/app_exposes'依赖会触发normalModuleFactory.hooks.factorize
this.clearDependenciesAndBlocks();
if (this.externalRequests.length === 1) {
this.addDependency(
new RemoteToExternalDependency(this.externalRequests[0])
);
} else {
this.addDependency(new FallbackDependency(this.externalRequests));
}
callback();
}
该模块添加了webpack/container/reference/app_exposes
依赖。再来看看codeGeneration返回:
codeGeneration({ runtimeTemplate, moduleGraph, chunkGraph }) {
const module = moduleGraph.getModule(this.dependencies[0]);
const id = module && chunkGraph.getModuleId(module);// 'webpack/container/reference/app_exposes'
const sources = new Map();
sources.set("remote", new RawSource(""));
const data = new Map();//shareScope = 'default' init=initExternal("webpack/container/reference/app_exposes")
data.set("share-init", [
{
shareScope: this.shareScope,// 作为ShareRuntimeModule的switch的default
initStage: 20,
init: id === undefined ? "" : `initExternal(${JSON.stringify(id)});`
}
]);
return { sources, data, runtimeRequirements: RUNTIME_REQUIREMENTS };
}
设置了data的share-init。重点看init方法,initExternal(${JSON.stringify(id)})主要是用来初始化模块内容。会被应用到__webpack_require__.I
的switch方法中。
至此该插件实现的过程完毕。
模块联邦功能实现主要依赖两个插件:
normalModuleFactory.hooks.factorize
监听依赖解析,当解析app_exposes/math
时会返回执行RemoteModule模块的build方法添加依赖webpack/container/reference/app_exposes
,在code生成时设置initExternal方法。在解析webpack/container/reference/app_exposes
依赖的时候会触发ExternalsPlugin插件的normalModuleFactory.hooks.factorize
回调方法。主要返回ExternalModule模块内容(获取需要的远端模块内容)。