【webpack】Externals(外部扩展)浅析 - webpack 5
本篇主要介绍 webpack 中的 externals 如何使用,顺带浅析一下 webpack 内部实现。
Externals
使用 Externals
项目目录
在 codesandbox 中预览项目,run build or build-externals
|--pages/
| |--A.js
|--App.js
|--index.js
|--webpack.config.js
// Pages/A.js
import React from "react";
import { get } from "lodash";
import { Button } from "antd";
const A = () => {
const obj = { a: 1 };
return (
<div>
<Button
onClick={() => {
console.log(get(obj, "a"), "use loadsh-es");
}}
>
click
</Button>
</div>
);
};
export default A;
// webpack.config.js
{
externals: {
lodash: "_",
},
}
<!-- public/index.html -->
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
产物对比
[{"url":"https://image.baidu.com/search/down?url=https://gzw.sinaimg.cn/mw2000/0085UwQ9gy1heagl58mc6j30zk0kcnic.jpg","alt":""},{"url":"https://image.baidu.com/search/down?url=https://gzw.sinaimg.cn/mw2000/0085UwQ9gy1heagl3e932j30zk0kb7q5.jpg","alt":""}]
[{"url":"https://image.baidu.com/search/down?url=https://gzw.sinaimg.cn/mw2000/0085UwQ9gy1heagl45hc3j30q4160qmy.jpg","alt":""},{"url":"https://image.baidu.com/search/down?url=https://gzw.sinaimg.cn/mw2000/0085UwQ9gy1heagl0fzd6j30rg19g4pw.jpg","alt":""}]
API 介绍
// webpack.config.js
{
externalsType: 'var'
externals: {
// key is the module name, value is the global variable name
"module-name": "global-variable-name"
}
}
externalsType - 模块类型
externals
externals: {
// key is the module name, value is the global variable name
"module-name": "global-variable-name",
"lodash": "_",
}
key => module-name
value => global-variable-name
CDN / 资源文件
Features:
externalsType = script
写法1:
// webpack.config.js
externalsType: 'script',
externals: {
"module-name": ["library", "global-variable-name"],
react: ['https://cdn.jsdelivr.net/npm/react@18.2.0/umd/react.production.min.js', 'React'],
'react-dom': ['https://cdn.jsdelivr.net/npm/react-dom@18.2.0/umd/react-dom.profiling.min.js', 'ReactDOM'],
lodash: ['https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js', '_'],
},
写法2:
// webpack.config.js
externals: {
"module-name": ["${externalsType} ${libraryName}", "global-variable-name"],
react: ['script https://cdn.jsdelivr.net/npm/react@18.2.0/umd/react.production.min.js', 'React'],
'react-dom': ['script https://cdn.jsdelivr.net/npm/react-dom@18.2.0/umd/react-dom.profiling.min.js', 'ReactDOM'],
lodash: ['script https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js', '_'],
}
// lodash in test
const Test = React.lazy(() => import(/* webpackChunkName: "test" */'./Test'));
<React.Suspense fallback={<div>loading...</div>}>
{this.state.visible && <Test/>}
</React.Suspense>
externalsType = module
浅析
externals 是如何替换模块的?
Webpack 大致流程
Source code
ExternalsPlugin
class ExternalsPlugin {
// ...
apply(compiler): {
compiler.hooks.compile.tap("ExternalsPlugin", ({ normalModuleFactory }) => {
new ExternalModuleFactoryPlugin(this.type, this.externals).apply(
normalModuleFactory
);
});
}
}
ExternalModuleFactoryPlugin
class ExternalModuleFactoryPlugin {
// ...
apply(normalModuleFactory) {
// ...
/**
* 订阅 normalModuleFactory.hooks.factorize 后,会将重新定义后的 module callback 透传下去
*/
normalModuleFactory.hooks.factorize.tapAsync(
"ExternalModuleFactoryPlugin",
(resolveData, callback) => {
const dependency = resolveData.dependencies[0];
// ...
if (typeof externals === "object") {
// ...
/**
* resolvedExternals: config 内的 externals 配置
* request: request 一般为 import 的路径,例:import lodash from "lodash/get"; request: 'lodash/get'
*/
if (
Object.prototype.hasOwnProperty.call(
resolvedExternals,
dependency.request
)
) {
callback(
null,
new ExternalModule(
externalConfig,
type || globalType,
dependency.request
)
);
}
}
}
)
}
}
ExternalModule
class ExternalModule extends Module {
// ...
codeGeneration({
runtimeTemplate,
moduleGraph,
chunkGraph,
runtime,
concatenationScope
}) {
const { request, externalType } = this._getRequestAndExternalType();
switch (externalType) {
default: {
// 根据 ExternalsType 生成对应模版代码
const sourceData = this._getSourceData(
request,
externalType,
runtimeTemplate,
moduleGraph,
chunkGraph,
runtime
);
let sourceString = sourceData.expression;
// ...
if (sourceData.init)
sourceString = `${sourceData.init}\n${sourceString}`;
let data = undefined;
if (sourceData.chunkInitFragments) {
data = new Map();
data.set("chunkInitFragments", sourceData.chunkInitFragments);
}
const sources = new Map();
if (this.useSourceMap || this.useSimpleSourceMap) {
sources.set(
"javascript",
new OriginalSource(sourceString, this.identifier())
);
} else {
sources.set("javascript", new RawSource(sourceString));
}
let runtimeRequirements = sourceData.runtimeRequirements;
if (!concatenationScope) {
if (!runtimeRequirements) {
runtimeRequirements = RUNTIME_REQUIREMENTS;
} else {
const set = new Set(runtimeRequirements);
// "module"
set.add(RuntimeGlobals.module);
runtimeRequirements = set;
}
}
return {
sources,
runtimeRequirements:
runtimeRequirements || EMPTY_RUNTIME_REQUIREMENTS,
data
};
}
}
}
build(options, compilation, resolver, fs, callback) {
this.buildMeta = {
async: false,
exportsType: undefined
};
this.buildInfo = {
strict: true,
topLevelDeclarations: new Set(),
module: compilation.outputOptions.module
};
const { request, externalType } = this._getRequestAndExternalType();
this.buildMeta.exportsType = "dynamic";
let canMangle = false;
this.clearDependenciesAndBlocks();
// ...
switch (externalType) {
// ....
case "script":
case "promise":
this.buildMeta.async = true;
break;
}
this.addDependency(new StaticExportsDependency(true, canMangle));
callback();
}
// ...
}
// ExternalModule
const getSourceForScriptExternal = (urlAndGlobal, runtimeTemplate) => {
if (typeof urlAndGlobal === "string") {
urlAndGlobal = extractUrlAndGlobal(urlAndGlobal);
}
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)}`
)})`,
// 在运行时代码内插入
// RUNTIME_REQUIREMENTS_FOR_SCRIPT = __webpack_require__.l
// __webpack_require__.l :基于 JSONP 实现的异步模块加载函数
runtimeRequirements: RUNTIME_REQUIREMENTS_FOR_SCRIPT
};
};
// 编译后
var __webpack_error__ = new Error();
module.exports = new Promise((resolve, reject) => {
if(typeof _ !== "undefined") return resolve();
__webpack_require__.l("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js", (event) => {
if(typeof _ !== "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__);
}, "_");
// rerutn Module 变量,通过 module.exports 暴露
}).then(() => (_));
const getSourceForDefaultCase = (optional, request, runtimeTemplate) => {
// ...
const variableName = request[0]; // 模块全局变量,例如 lodash 为 _
const objectLookup = propertyAccess(request, 1);
return {
init: optional
? checkExternalVariable(variableName, request.join("."), runtimeTemplate)
: undefined,
// _
expression: `${variableName}${objectLookup}`
};
};
/* 1 */ Module id
/*!********************!*\
!*** external "_" ***!
\********************/
/*! dynamic exports */
/*! exports [maybe provided (runtime-defined)] [no usage info] */
/*! runtime requirements: module */
/***/ ((module) => {
// type 为 var 时,Externals 只关注全局变量,所以需要确保变量的存在
module.exports = _;
/***/ })
NormalModule
- 调用
handleModuleCreate
,根据文件类型构建module
子类 - 调用 loader-runner 仓库的
runLoaders
转译module
内容,通常是从各类资源类型转译为 JavaScript 文本 - 调用 acorn 将 JS 文本解析为AST
- 遍历 AST,触发各种钩子
a. 在HarmonyExportDependencyParserPlugin
插件监听exportImportSpecifier
钩子,解读 JS 文本对应的资源依赖
b. 调用module
对象的addDependency
将依赖对象加入到module
依赖列表中 - AST 遍历完毕后,调用
module.handleParseResult
处理模块依赖 - 对于
module
新增的依赖,调用handleModuleCreate
,控制流回到第一步 - 所有依赖都解析完毕后,构建阶段结束
class NormalModule extends Module {
_doBuild(..., callback) {
// ...
runLoaders(...);
}
// 在build时,会根据该文件匹配到的loader,执行runLoaders转换文件内容。
// 根据 resolve 解析文件
build(options, compilation, resolver, fs, callback) {
// ...
return this._doBuild(...)
}
}
// NormalModuleFactory.js
class NormalModuleFactory extends ModuleFactory {
// ...
constructor() {
// ...
this.hooks.factorize.tapAsync(
{
name: "NormalModuleFactory",
// 执行权重,优先执行其他 factorize
stage: 100
},
() => {
// ...
this.hooks.resolve.callAsync(..., () => {
createdModule = new NormalModule(
/** @type {NormalModuleCreateData} */ (createData)
);
})
}
)
}
// ...
create() {
// ...
this.hooks.factorize.callAsync(
...,
(err, module) => {
// ...
const factoryResult = {
module,
// ...
};
callback(null, factoryResult);
})
}
}
// Compilation.js
class Compilation {
// ...
_factorizeModule({ factory }) {
// ...
factory.create({...}, (err, result) => {
// ...
callback(null, factoryResult ? result : result.module);
})
// ...
}
}
externals 和 alias 之间有影响吗?
Resolve 阶段
import lodash from "我被Alias了";
console.log(lodash);
// webpack.config.js
resolve: {
alias: {
我被Alias了: 'lodash',
},
},
Resolve 后
// in AliasPlugin.js
class AliasTest {
apply(compiler) {
normalModuleFactory.hooks.afterResolve.tap('AliasTest-afterResolve', (resolveData) => {
if (resolveData.request.includes('Alias')) {
console.log(`${resolveData.request}(request) | -- afterResolve.resolveData -- | ${resolveData.context}(context)`);
console.log(resolveData.createData?.request);
console.log('----- afterResolve --- \n');
}
})
normalModuleFactory.hooks.createModule.tap('AliasTest-createModule', (createData, resolveData) => {
if (resolveData.request.includes('Alias')) {
console.log(`${createData.request}(request) | -- createModule.createData -- | ${createData.context}(context)`);
console.log(`${resolveData.request}(request) | -- createModule.resolveData -- | ${resolveData.context}(context)`);
console.log('----- createModule ---- \n');
}
})
});
}
}
Resolve
class NormalModuleFactory extends ModuleFactory {
constructor() {
// ...
this.hooks.resolve.tapAsync(..., (resolveData) => {
//...
// createData 包含 alias 解析后路径
Object.assign(resolveData.createData, {
// ...
request: stringifyLoadersAndResource(
allLoaders,
resourceData.resource
),
userRequest,
rawRequest: request,
});
})
}
}
Module
class NormalModuleFactory extends ModuleFactory {
constructor() {
// ...
this.hooks.afterResolve.callAsync(resolveData, (err, result) => {
// ...
const createData = resolveData.createData;
this.hooks.createModule.callAsync(
createData,
resolveData,
(err, createdModule) => {
if (!createdModule) {
// ...
// 创建 module
createdModule = new NormalModule(createData);
}
createdModule = this.hooks.module.call(
createdModule,
createData,
resolveData
);
return callback(null, createdModule);
}
);
});
}
}
class IgnorePlugin {
checkIgnore(resolveData) {
if (
this.options.checkResource(resolveData.request, resolveData.context)
) {
return false;
}
}
apply(compiler) {
compiler.hooks.normalModuleFactory.tap("IgnorePlugin", nmf => {
nmf.hooks.beforeResolve.tap("IgnorePlugin", this.checkIgnore);
});
// ...
}
}
// webpack.config.js plugins
new webpack.IgnorePlugin({
resourceRegExp: /我被Alias了/,
})
https://mp.weixin.qq.com/s/SbJNbSVzSPSKBe2YStn2Zw
https://mp.weixin.qq.com/s?__biz=Mzg3OTYwMjcxMA==&mid=2247484088&idx=1&sn=41bf509a72f2cbcca1521747bf5e28f4&chksm=cf00bfc1f87736d76681e1e1db39deb1fa3121686bcd45a42709fa58aaba6d7ffba6d7fb4975&scene=178&cur_album_id=1856066636768722949#rd
https://mp.weixin.qq.com/s/tXkGx6Ckt9ucT2o8tNM-8w
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 kshao-blog-前端知识记录!
评论