webpack 版本: ^5.24.2
npm init -y
npm i webpack webpack-cli -D
Development:
(() => { // webpackBootstrap
var __webpack_modules__ = ({
"./src/index.js":
(() => {
eval("console.log('duanxl.com')\n\n//# sourceURL=webpack://webpack5-demo/./src/index.js?");
})
});
var __webpack_exports__ = {};
__webpack_modules__["./src/index.js"]();
})();
因为 scope hositing 的原因,所以 Production 并没有闭包,有声明变量,才会打成闭包
Production:
console.log('duanxl.com')
简单写入代码
// sync.js
const num = 99;
export default num;
// index.js
import num from './sync'
console.log(num + 1)
Dev 打包后
(() => { // webpackBootstrap
"use strict";
var __webpack_modules__ = ({
"./src/index.js":
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _sync__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./sync */ \"./src/sync.js\");\n\n\nconsole.log(_sync__WEBPACK_IMPORTED_MODULE_0__.default + 1)\n\n//# sourceURL=webpack://webpack5-demo/./src/index.js?");
}),
"./src/sync.js":
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\nconst num = 99;\n\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (num);\n\n//# sourceURL=webpack://webpack5-demo/./src/sync.js?");
})
});
// The module cache
var __webpack_module_cache__ = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
if (__webpack_module_cache__[moduleId]) {
return __webpack_module_cache__[moduleId].exports;
}
var module = __webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {}
};
// Execute the module function
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
// Return the exports of the module
return module.exports;
}
(() => {
// define getter functions for harmony exports
__webpack_require__.d = (exports, definition) => {
for (var key in definition) {
if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
}
}
};
})();
(() => {
__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
})();
(() => {
// define __esModule on exports
__webpack_require__.r = (exports) => {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
})();
var __webpack_exports__ = __webpack_require__("./src/index.js");
})();
流程: - 首先,一进来,定义了一些变量,开始执行函数,函数内部有几个自执行函数,分别定义了webpack_require.d、webpack_require.o、webpack_require.r 函数,下面再说定义的这些函数做了什么 - 判断webpack_module_cache是否存在 moduleId,存在即返回,不存在新建对象 - 接着开始执行webpack_require,并以入口文名作为参数执行 - webpack_require函数内部,可以看到,判断webpack_module_cache是否存在 moduleId(即文件名),不存在,直接赋值,并添加了 exports 空对象,接着,开始执行文件对象(即 module 对象,内部存储了文件名做未 key,函数作为 value 的对象),最后,return module.exports;module.exports(即webpack_module_cache中对应的) - 接着,我们看一下接下来执行了什么
```
// __unused_webpack_module : { exports: {}}
// __webpack_exports__ : {}
// __webpack_require__ : 函数自身
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval(`__webpack_require__.r(__webpack_exports__);
var _sync__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/sync.js");
console.log(_sync__WEBPACK_IMPORTED_MODULE_0__.default + 1)`);
})
```
> 传入空对象还有对象属性干了什么,接着看
```
// exports : {}
__webpack_require__.r = (exports) => {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
// 可以看到,最终创建了一个新的类型标签对象
// Symbol.toStringTag 用于标记该对象的自定义类型标签
// 可以看到,目前的module "./src/index.js":{exports:Module {Symbol(Symbol.toStringTag): "Module"}}
```
> 可以看到,把 module 入口对象中的 exports 改成了空的类型标签对象,先不管,紧接着看是如何调用了另一个 module 对象 __webpack_require__("./src/sync.js")
```
// "./src/sync.js":
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval(`__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__,
"default": () => (__WEBPACK_DEFAULT_EXPORT__)});
const num = 99;
const __WEBPACK_DEFAULT_EXPORT__ = (num);`);
})
```
- 和上面一样,把 module 入口对象中的 exports 改成了空的类型标签对象
- 执行了__webpack_require__.d
```
// 可以看到,挂载属性至module对象上,并添加了get方法
__webpack_require__.d = (exports, definition) => {
for (var key in definition) {
if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
}
}
};
```
```
// 这里不解,为啥不直接用obj.hasOwnProperty(prop),大佬的世界我还没理解
(() => {
__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
})();
```
- 再看接下来的声明变量,以及打印,就可以理解是如何得到的值了
- > 可以看到,对于module,相比于webpack4,减少了commonjs
的实现(需手动配置target),即传参以commonjs参数传入闭包,然后赋值到__webpack_require__等变量上
Pro 打包后
(()=>{"use strict";console.log(100)})();
emmmmm….,就这一句话,对,你没有看错
这是因为 Webpack5 集成了 prepack,prepack 编译的代码比较激进,对于一些简单的值,会直接找到并进行运算,现在我们可以知道,为什么 Webpack5 的体积小了很多,当然,还可以更小,可以去看 prepack 官网,编写符合 prepack 编译优化的代码,这样既可以装 X,又可以优化项目~~~
prepack 地址 prepack.io
编写简单异步
// async.js
const num = 99;
export default num;
// index.js
import('./async')
.then(() => {
console.log(num + 1)
}).catch((e) => { console.log(e) })
Development:
// src_async_js.js
(self["webpackChunkwebpack5_demo"] = self["webpackChunkwebpack5_demo"] || []).push([["src_async_js"], {
/***/ "./src/async.js":
/*!**********************!*\
!*** ./src/async.js ***!
\**********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
"use strict";
eval(`
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, { "default": () => (__WEBPACK_DEFAULT_EXPORT__)});
const num = 99;
const __WEBPACK_DEFAULT_EXPORT__ = (num);`);
/***/
})
}]);
可以看到,异步模块打包后,是挂载在 window 属性上的二维数组,其中 eval 中的代码和 module 打包后的差不多,有一点区别,就是声明变量及赋值跑到了”./src/async.js”属性上。还是用了新的 api—globalThis
(() => { // webpackBootstrap
var __webpack_modules__ = ({
"./src/index.js":
((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
eval(`
__webpack_require__.e("src_async_js")
.then(__webpack_require__.bind(__webpack_require__,"./src/async.js"))
.then(() => {console.log(num + 1)})
.catch((e) => { console.log(e) })`);
})
});
// The module cache
var __webpack_module_cache__ = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
if (__webpack_module_cache__[moduleId]) {
return __webpack_module_cache__[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = __webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {}
};
// Execute the module function
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
// Return the exports of the module
return module.exports;
}
// expose the modules object (__webpack_modules__)
__webpack_require__.m = __webpack_modules__;
/* webpack/runtime/define property getters */
(() => {
// define getter functions for harmony exports
__webpack_require__.d = (exports, definition) => {
for (var key in definition) {
if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
}
}
};
})();
/* 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;
}, []));
};
})();
/* webpack/runtime/get javascript chunk filename */
(() => {
// This function allow to reference async chunks
__webpack_require__.u = (chunkId) => {
// return url for filenames based on template
return "" + chunkId + ".js";
};
})();
/* webpack/runtime/global */
(() => {
__webpack_require__.g = (function () {
if (typeof globalThis === 'object') return globalThis;
try {
return this || new Function('return this')();
} catch (e) {
if (typeof window === 'object') return window;
}
})();
})();
/* webpack/runtime/hasOwnProperty shorthand */
(() => {
__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
})();
/* webpack/runtime/load script */
(() => {
var inProgress = {};
var dataWebpackPrefix = "webpack5-demo:";
// loadScript function to load a script via script tag
__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/runtime/make namespace object */
(() => {
// define __esModule on exports
__webpack_require__.r = (exports) => {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
})();
/* webpack/runtime/publicPath */
(() => {
var scriptUrl;
if (__webpack_require__.g.importScripts) scriptUrl = __webpack_require__.g.location + "";
var document = __webpack_require__.g.document;
if (!scriptUrl && document) {
if (document.currentScript)
scriptUrl = document.currentScript.src
if (!scriptUrl) {
var scripts = document.getElementsByTagName("script");
if (scripts.length) scriptUrl = scripts[scripts.length - 1].src
}
}
// When supporting browsers where an automatic publicPath is not supported you must specify an output.publicPath manually via configuration
// or pass an empty string ("") and set the __webpack_public_path__ variable from your code to use your own logic.
if (!scriptUrl) throw new Error("Automatic publicPath is not supported in this browser");
scriptUrl = scriptUrl.replace(/#.*$/, "").replace(/\?.*$/, "").replace(/\/[^\/]+$/, "/");
__webpack_require__.p = scriptUrl;
})();
/* webpack/runtime/jsonp chunk loading */
(() => {
// no baseURI
// object to store loaded and loading chunks
// undefined = chunk not loaded, null = chunk preloaded/prefetched
// Promise = chunk loading, 0 = chunk loaded
var installedChunks = {
"main": 0
};
__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 (true) { // all chunks have JS
// 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, chunkId);
} else installedChunks[chunkId] = 0;
}
}
};
// no prefetching
// no preloaded
// no HMR
// no HMR manifest
// no deferred startup
// install a JSONP callback for chunk loading
var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
var [chunkIds, moreModules, runtime] = data;
// add "moreModules" to the modules object,
// then flag all "chunkIds" as loaded and fire callback
var moduleId, chunkId, i = 0, resolves = [];
for (; i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if (__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
resolves.push(installedChunks[chunkId][0]);
}
installedChunks[chunkId] = 0;
}
for (moduleId in moreModules) {
if (__webpack_require__.o(moreModules, moduleId)) {
__webpack_require__.m[moduleId] = moreModules[moduleId];
}
}
if (runtime) runtime(__webpack_require__);
if (parentChunkLoadingFunction) parentChunkLoadingFunction(data);
while (resolves.length) {
resolves.shift()();
}
}
var chunkLoadingGlobal = self["webpackChunkwebpack5_demo"] = self["webpackChunkwebpack5_demo"] || [];
chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
// no deferred startup
})();
/************************************************************************/
// startup
// Load entry module and return exports
// This entry module can't be inlined because the eval devtool is used.
var __webpack_exports__ = __webpack_require__("./src/index.js");
})();
先不管闭包中的自执行函数(即变量赋值),可以看到,和之前一样,先执行__webpack_require__,和module流程也一样,不一样的,就是webpack_modules__这个模块对象,里面执行了__webpack_require.e
var __webpack_modules__ = ({
"./src/index.js":
((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
eval(`
__webpack_require__.e("src_async_js")
.then(__webpack_require__.bind(__webpack_require__,"./src/async.js"))
.then(() => {console.log(num + 1)})
.catch((e) => { console.log(e) })`);
})
});
> Production:
> 生产环境就不看了,和 dev 出入不大,只是代码进行了压缩,变量名变得精简,为了更小的内存空间
loader 是 webpack 的核心之一,loader 可以理解为一个导出的函数,是对模块源代码进行转换
下面先看一个简单的 loader,实现 const 转换为 var
'use strict';
const loaderUtils = require('loader-utils');
const acorn = require('acorn');
const walk = require('acorn-walk');
const MagicString = require('magic-string');
module.exports = function (content) {
const options = loaderUtils.getOptions(this);
console.log('前置钩子', this.data.value);
console.log('🍌配置文件', options)
const ast = acorn.parse(content, { ecmaVersion: 2020 });
const code = new MagicString(content);
walk.simple(ast, {
VariableDeclarator(node) {
console.log('节点', node);
const { start } = node;
code.overwrite(start, start + 5, 'var')
}
})
return code.toString();
};
module.exports.pitch = function (r, preRequest, data) {
// r:loader链中排在后面的loader以及资源文件路径组成的字符串
// preRequest:loader链中排在前面的loader以及资源文件路径组成的字符串
// data:每个loader中存放在上下文中的数据,可用于pitch传递给loader数据
data.value = "段鑫磊";
}
使用 this.async 来获取 callback 函数
通过配置 raw,loader 可以接收 Buffer
会阻断 loader,即在 loader 运行前会触发,在执行 loader 之前,会按照顺序调用 loader 上的 pitch 方法,而 loader 的执行顺序是从右向左,也可以理解成从下至上
plugin,针对 loader 结束后,在 webpack 打包的过程中,基于事件机制,不直接操作文件,执行的任务
// plugin.js
const pluginName = 'TestPlugin';
class TestPlugin {
apply(compiler) {
compiler.hooks.run.tap(pluginName, compilation => {
console.log('lalalal', compilation);
})
}
}
module.exports = TestPlugin;
// webpack.config.js
const TestPlugin = require('./plugin/plugin')
module.exports = {
plugins: [
new TestPlugin()
]
}
简单看一下 webpack 的流程,基于上面我们自己编写的 plugin,下面代码展示部分代码,用于清晰流程
"main": "lib/index.js",
"bin": {
"webpack": "bin/webpack.js"
},
runCommand(packageManager, installOptions.concat(cli.package))
.then(() => {
runCli(cli);
})
.catch(error => {
console.error(error);
process.exitCode = 1;
});
const { promptInstallation, logger, colors } = utils;
const runCLI = require('../lib/bootstrap');
promptInstallation('webpack', () => {
utils.logger.error(`It looks like ${colors.bold('webpack')} is not installed.`);
})
.then(() => {
logger.success(`${colors.bold('webpack')} was installed successfully.`);
runCLI(process.argv, originalModuleCompile);
})
.catch(() => {
logger.error(`Action Interrupted, Please try once again or install ${colors.bold('webpack')} manually.`);
process.exit(2);
});
const WebpackCLI = require('./webpack-cli');
const utils = require('./utils');
const runCLI = async (args, originalModuleCompile) => {
try {
// Create a new instance of the CLI object
const cli = new WebpackCLI();
cli._originalModuleCompile = originalModuleCompile;
await cli.run(args);
} catch (error) {
utils.logger.error(error);
process.exit(2);
}
};
module.exports = runCLI;
// 省略99%代码。。。
compiler = await this.createCompiler(options, callback);
// 获取配置,this.webpack,才真正调用了webpack中的createCompiler
async createCompiler(options, callback) {
this.applyNodeEnv(options);
let config = await this.resolveConfig(options);
config = await this.applyOptions(config, options);
config = await this.applyCLIPlugin(config, options);
let compiler;
try {
compiler = this.webpack(
config.options,
callback
? (error, stats) => {
if (error && this.isValidationError(error)) {
this.logger.error(error.message);
process.exit(2);
}
callback(error, stats);
}
: callback,
);
} catch (error) {
if (this.isValidationError(error)) {
this.logger.error(error.message);
} else {
this.logger.error(error);
}
process.exit(2);
}
// TODO webpack@4 return Watching and MultiWatching instead Compiler and MultiCompiler, remove this after drop webpack@4
if (compiler && compiler.compiler) {
compiler = compiler.compiler;
}
return compiler;
}
// webpack/lib/webpack.js
const createCompiler = rawOptions => {
const options = getNormalizedWebpackOptions(rawOptions);
applyWebpackOptionsBaseDefaults(options);
const compiler = new Compiler(options.context);
compiler.options = options;
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).apply(compiler);
if (Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
applyWebpackOptionsDefaults(options);
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
new WebpackOptionsApply().process(options, compiler);
compiler.hooks.initialize.call();
return compiler;
};
class Compiler {
/**
* @param {string} context the compilation path
*/
constructor(context) {
this.hooks = Object.freeze({
/** @type {SyncHook<[]>} */
initialize: new SyncHook([]),
/** @type {SyncBailHook<[Compilation], boolean>} */
shouldEmit: new SyncBailHook(["compilation"]),
/** @type {AsyncSeriesHook<[Stats]>} */
done: new AsyncSeriesHook(["stats"]),
/** @type {SyncHook<[Stats]>} */
afterDone: new SyncHook(["stats"]),
/** @type {AsyncSeriesHook<[]>} */
additionalPass: new AsyncSeriesHook([]),
/** @type {AsyncSeriesHook<[Compiler]>} */
beforeRun: new AsyncSeriesHook(["compiler"]),
/** @type {AsyncSeriesHook<[Compiler]>} */
run: new AsyncSeriesHook(["compiler"]),
/** @type {AsyncSeriesHook<[Compilation]>} */
emit: new AsyncSeriesHook(["compilation"]),
/** @type {AsyncSeriesHook<[string, AssetEmittedInfo]>} */
assetEmitted: new AsyncSeriesHook(["file", "info"]),
/** @type {AsyncSeriesHook<[Compilation]>} */
afterEmit: new AsyncSeriesHook(["compilation"]),
/** @type {SyncHook<[Compilation, CompilationParams]>} */
thisCompilation: new SyncHook(["compilation", "params"]),
/** @type {SyncHook<[Compilation, CompilationParams]>} */
compilation: new SyncHook(["compilation", "params"]),
/** @type {SyncHook<[NormalModuleFactory]>} */
normalModuleFactory: new SyncHook(["normalModuleFactory"]),
/** @type {SyncHook<[ContextModuleFactory]>} */
contextModuleFactory: new SyncHook(["contextModuleFactory"]),
/** @type {AsyncSeriesHook<[CompilationParams]>} */
beforeCompile: new AsyncSeriesHook(["params"]),
/** @type {SyncHook<[CompilationParams]>} */
compile: new SyncHook(["params"]),
/** @type {AsyncParallelHook<[Compilation]>} */
make: new AsyncParallelHook(["compilation"]),
/** @type {AsyncParallelHook<[Compilation]>} */
finishMake: new AsyncSeriesHook(["compilation"]),
/** @type {AsyncSeriesHook<[Compilation]>} */
afterCompile: new AsyncSeriesHook(["compilation"]),
/** @type {AsyncSeriesHook<[Compiler]>} */
watchRun: new AsyncSeriesHook(["compiler"]),
/** @type {SyncHook<[Error]>} */
failed: new SyncHook(["error"]),
/** @type {SyncHook<[string | null, number]>} */
invalid: new SyncHook(["filename", "changeTime"]),
/** @type {SyncHook<[]>} */
watchClose: new SyncHook([]),
/** @type {AsyncSeriesHook<[]>} */
shutdown: new AsyncSeriesHook([]),
/** @type {SyncBailHook<[string, string, any[]], true>} */
infrastructureLog: new SyncBailHook(["origin", "type", "args"]),
// TODO the following hooks are weirdly located here
// TODO move them for webpack 5
/** @type {SyncHook<[]>} */
environment: new SyncHook([]),
/** @type {SyncHook<[]>} */
afterEnvironment: new SyncHook([]),
/** @type {SyncHook<[Compiler]>} */
afterPlugins: new SyncHook(["compiler"]),
/** @type {SyncHook<[Compiler]>} */
afterResolvers: new SyncHook(["compiler"]),
/** @type {SyncBailHook<[string, Entry], boolean>} */
entryOption: new SyncBailHook(["context", "entry"])
});
// 省略无数code。。。
}
// /tapable/index.js
"use strict";
exports.__esModule = true;
exports.SyncHook = require("./SyncHook");
exports.SyncBailHook = require("./SyncBailHook");
exports.SyncWaterfallHook = require("./SyncWaterfallHook");
exports.SyncLoopHook = require("./SyncLoopHook");
exports.AsyncParallelHook = require("./AsyncParallelHook");
exports.AsyncParallelBailHook = require("./AsyncParallelBailHook");
exports.AsyncSeriesHook = require("./AsyncSeriesHook");
exports.AsyncSeriesBailHook = require("./AsyncSeriesBailHook");
exports.AsyncSeriesLoopHook = require("./AsyncSeriesLoopHook");
exports.AsyncSeriesWaterfallHook = require("./AsyncSeriesWaterfallHook");
exports.HookMap = require("./HookMap");
exports.MultiHook = require("./MultiHook");
看个简单的 SyncHook 如何实现
function SyncHook(args = [], name = undefined) {
const hook = new Hook(args, name);
hook.constructor = SyncHook;
hook.tapAsync = TAP_ASYNC;
hook.tapPromise = TAP_PROMISE;
hook.compile = COMPILE;
return hook;
}
// hook.js
class Hook {
constructor(args = [], name = undefined) {
this._args = args;
this.name = name;
this.taps = [];
this.interceptors = [];
this._call = CALL_DELEGATE;
this.call = CALL_DELEGATE;
this._callAsync = CALL_ASYNC_DELEGATE;
this.callAsync = CALL_ASYNC_DELEGATE;
this._promise = PROMISE_DELEGATE;
this.promise = PROMISE_DELEGATE;
this._x = undefined;
this.compile = this.compile;
this.tap = this.tap;
this.tapAsync = this.tapAsync;
this.tapPromise = this.tapPromise;
}
compile(options) {
throw new Error("Abstract: should be overridden");
}
_createCall(type) {
return this.compile({
taps: this.taps,
interceptors: this.interceptors,
args: this._args,
type: type
});
}
_tap(type, options, fn) {
if (typeof options === "string") {
options = {
name: options.trim()
};
} else if (typeof options !== "object" || options === null) {
throw new Error("Invalid tap options");
}
if (typeof options.name !== "string" || options.name === "") {
throw new Error("Missing name for tap");
}
if (typeof options.context !== "undefined") {
deprecateContext();
}
options = Object.assign({ type, fn }, options);
options = this._runRegisterInterceptors(options);
this._insert(options);
}
tap(options, fn) {
this._tap("sync", options, fn);
}
tapAsync(options, fn) {
this._tap("async", options, fn);
}
tapPromise(options, fn) {
this._tap("promise", options, fn);
}
_runRegisterInterceptors(options) {
for (const interceptor of this.interceptors) {
if (interceptor.register) {
const newOptions = interceptor.register(options);
if (newOptions !== undefined) {
options = newOptions;
}
}
}
return options;
}
withOptions(options) {
const mergeOptions = opt =>
Object.assign({}, options, typeof opt === "string" ? { name: opt } : opt);
return {
name: this.name,
tap: (opt, fn) => this.tap(mergeOptions(opt), fn),
tapAsync: (opt, fn) => this.tapAsync(mergeOptions(opt), fn),
tapPromise: (opt, fn) => this.tapPromise(mergeOptions(opt), fn),
intercept: interceptor => this.intercept(interceptor),
isUsed: () => this.isUsed(),
withOptions: opt => this.withOptions(mergeOptions(opt))
};
}
isUsed() {
return this.taps.length > 0 || this.interceptors.length > 0;
}
intercept(interceptor) {
this._resetCompilation();
this.interceptors.push(Object.assign({}, interceptor));
if (interceptor.register) {
for (let i = 0; i < this.taps.length; i++) {
this.taps[i] = interceptor.register(this.taps[i]);
}
}
}
_resetCompilation() {
this.call = this._call;
this.callAsync = this._callAsync;
this.promise = this._promise;
}
_insert(item) {
this._resetCompilation();
let before;
if (typeof item.before === "string") {
before = new Set([item.before]);
} else if (Array.isArray(item.before)) {
before = new Set(item.before);
}
let stage = 0;
if (typeof item.stage === "number") {
stage = item.stage;
}
let i = this.taps.length;
while (i > 0) {
i--;
const x = this.taps[i];
this.taps[i + 1] = x;
const xStage = x.stage || 0;
if (before) {
if (before.has(x.name)) {
before.delete(x.name);
continue;
}
if (before.size > 0) {
continue;
}
}
if (xStage > stage) {
continue;
}
i++;
break;
}
this.taps[i] = item;
}
}
简单实现上面的事件订阅
class SyncHook {
constructor() {
this.taps = [];
}
tap(name, fn) {
this.taps.push(fn)
}
call() {
this.taps.map(tap => tap(...arguments))
}
}
我们可以看到,webpack 核心就是使用 tapable 来实现 plugins 的 building,tapable 是一个用于事件发布订阅的插件
module:{
rules:[
{
test:/\.(png|jps|svg))/i,
type:'asset'
}
]
}