Tapable
这是webpack里的核心工具,webpack里的很多对象都继承了它(比如compiler)。它暴露了tap,tapAsync,tapPromise方法,插件可以用这些方法,在整个webpack的构建周期注入自定义构建。
Hook类
tapable导出的钩子类:
const {
SyncHook,
SyncBailHook,
AsyncParallelHook,
AsyncSeriesHook
} = require("tapable");
事件回调的运行逻辑:
类型 | 描述 |
---|---|
Basic | 基础类型,单纯的调用注册的事件回调,不关心内部运行逻辑 |
Bail | 保险类型,当一个事件回调在运行时,返回值不是undefined ,停止后面事件回调的执行 |
Waterfall | 瀑布类型,如果当前执行的事件回调的返回值不为undefined ,那么就把下一个事件回调的第一个参数,替换为当前返回值 |
Loop | 循环类型,如果当前执行的事件回调的返回值不为undefined ,递归调用注册事件直到没有返回值 |
触发事件的方式:
类型 | 描述 |
---|---|
Sync | Sync开头的钩子,只能用tap 方法注册事件回调,这类事件回调会同步执行。 |
AsyncParallel | Async开头的钩子,只能用callAsync 或promise 方法触发回调,AsyncParallel并行执行回调 |
AsyncSeries | 串行执行回调 |
注册事件回调
tap,tapAsync,tapPromise
const {SyncHook} = require('tapable')
const hooks = {
aaa: new SyncHook()
}
// 注册事件回调的方法,例如 tap,它们的第一个参数可以是事件回调的名字,也可以是配置对象
hooks.aaa.tap('hookname', () => {console.log(1)})
// stage:这个属性的类型是数字,数字越大事件回调执行的越晚
hooks.aaa.tap({name: 'secendHookName', stage: 20}, () => {console.log(2)})
// before:在哪个钩子名称之前执行
hooks.aaa.tap({name: 'thirdHookName', before: 'secendHookName'}, () => {console.log(3)})
hooks.aaa.call()
// 打印顺序就是:1 3 2
触发事件
call,callAsync,promise.then
拦截器
可以对钩子的注册、调用的触发进行监听
const { SyncHook } = require('tapable');
const hook = new SyncHook();
hook.intercept({
// 注册时执行
register(tap) {
console.log('register', tap);
return tap;
},
// 触发事件时执行
call(...args) {
console.log('call', args);
},
// 在 call 拦截器之后执行
loop(...args) {
console.log('loop', args);
},
// 事件回调调用前执行
tap(tap) {
console.log('tap', tap);
},
});
流程源码解析
从webpack的main入口进去,会看到webpack要求必须要安装webpack-cli,因为,webpack把关于命令行的version、help等命令都交给cli处理,关键的build和watch命令,才真正的在webpack代码中处理。
处理build或watch都需要createCompiler
,生成compiler
实例。然后运行compiler.watch()
或compiler.run()
。
compiler = createCompiler(webpackOptions);
// 创建compiler实例
const { compiler, watch, watchOptions } = create();
if (watch) {
compiler.watch(watchOptions, callback);
} else {
// 运行compiler.run,来call所有注册的钩子回调
compiler.run((err, stats) => {
compiler.close(err2 => {
callback(err || err2, stats);
});
});
}
createCompiler流程解析
先上图,
在createCompiler中,
- 首先webpack会整合配置参数。
- 优化node环境,比如node的fs模块,webpack专门用了
graceful-fs
包封装,以及日志打印,文件监听,文件读取缓存等。 - 然后,webpack便开始注册插件的钩子函数了,包括用户传入的插件,以及自己的内置插件。此处要结合我上文所说的
tabpable
一起来看。 - 调用了compiler的几个钩子函数。
此处比较重要的地方就是
new WebpackOptionsApply().process(options, compiler);
,这几乎贯穿了webpack打包的整个生命周期,因为内置插件都在这里注册了,后面讲到整个编译流程还会详细讲到这里。
const createCompiler = rawOptions => {
// 获取webpack配置信息
const options = getNormalizedWebpackOptions(rawOptions);
applyWebpackOptionsBaseDefaults(options);
// 创建compiler实例
const compiler = new Compiler(options.context);
// 给实例的options赋值
compiler.options = options;
//
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).apply(compiler);
// 执行webpack插件的apply方法
if (Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
// 给用户没有配置的options,赋上默认值
applyWebpackOptionsDefaults(options);
// 执行与配置环境相关钩子
compiler.hooks.environment.call();
// 执行after环境钩子
compiler.hooks.afterEnvironment.call();
// 这是个关键函数,开始根据用户配置,处理webapck的内置插件逻辑
// 插件都是注册回调事件,真正调用要到compiler.run函数
// 也就是说,到此所有的钩子函数都注册好了
new WebpackOptionsApply().process(options, compiler);
// 执行初始化钩子
compiler.hooks.initialize.call();
return compiler;
};
构建逻辑
compiler.run
其实,如下run函数结束后,整个编译流程就结束了,但是里面的逻辑真的是山路十八弯,这可以说是灵活的代码,就难免会丧失一些可读性吧。此处盗用一张别人的图,感觉把逻辑总结的很到位。webpack编译大概就是如下流程。我会针对这个图,再展开讲,它到底是出现在webpack源码中的哪个地方,以及是如何调用的整个流程。
run(callback) {
if (this.running) {
return callback(new ConcurrentCompilationError());
}
let logger;
const finalCallback = (err, stats) => {
if (logger) logger.time("beginIdle");
this.idle = true;
this.cache.beginIdle();
this.idle = true;
if (logger) logger.timeEnd("beginIdle");
this.running = false;
if (err) {
this.hooks.failed.call(err);
}
if (callback !== undefined) callback(err, stats);
this.hooks.afterDone.call(stats);
};
const startTime = Date.now();
this.running = true;
// 编译结束后的回调
const onCompiled = (err, compilation) => {
if (err) return finalCallback(err);
// 同步执行(SyncBailHook)shouldEmit钩子注册的所有回调函数
//(保险类型,当一个事件回调在运行时,返回值不是`undefined`,停止后面事件回调的执行)
// 不需要输出文件
if (this.hooks.shouldEmit.call(compilation) === false) {
compilation.startTime = startTime;
compilation.endTime = Date.now();
const stats = new Stats(compilation);
// 执行done(AsyncSeriesHook)钩子
this.hooks.done.callAsync(stats, err => {
if (err) return finalCallback(err);
return finalCallback(null, stats);
});
return;
}
process.nextTick(() => {
logger = compilation.getLogger("webpack.Compiler");
logger.time("emitAssets");
// 输出文件
this.emitAssets(compilation, err => {
logger.timeEnd("emitAssets");
if (err) return finalCallback(err);
if (compilation.hooks.needAdditionalPass.call()) {
compilation.needAdditionalPass = true;
compilation.startTime = startTime;
compilation.endTime = Date.now();
logger.time("done hook");
const stats = new Stats(compilation);
this.hooks.done.callAsync(stats, err => {
logger.timeEnd("done hook");
if (err) return finalCallback(err);
this.hooks.additionalPass.callAsync(err => {
if (err) return finalCallback(err);
this.compile(onCompiled);
});
});
return;
}
logger.time("emitRecords");
this.emitRecords(err => {
logger.timeEnd("emitRecords");
if (err) return finalCallback(err);
compilation.startTime = startTime;
compilation.endTime = Date.now();
logger.time("done hook");
const stats = new Stats(compilation);
this.hooks.done.callAsync(stats, err => {
logger.timeEnd("done hook");
if (err) return finalCallback(err);
this.cache.storeBuildDependencies(
compilation.buildDependencies,
err => {
if (err) return finalCallback(err);
return finalCallback(null, stats);
}
);
});
});
});
});
};
const run = () => {
// 串行执行beforeRun(AsyncSeriesHook)钩子注册的所有回调函数,传入complier对象作为参数
this.hooks.beforeRun.callAsync(this, err => {
// 出错执行错误回调钩子
if (err) return finalCallback(err);
// 串行执行run(AsyncSeriesHook)钩子注册的所有回调函数,传入complier对象作为参数
this.hooks.run.callAsync(this, err => {
if (err) return finalCallback(err);
this.readRecords(err => {
if (err) return finalCallback(err);
this.compile(onCompiled);
});
});
});
};
if (this.idle) {
this.cache.endIdle(err => {
if (err) return finalCallback(err);
this.idle = false;
run();
});
} else {
run();
}
}
addEntry入口逻辑
也就是在我上文说到的new WebpackOptionsApply().process(options, compiler);
里面,有一个EntryOptionPlugin
里有EntryPlugin
,在compiler.hooks.make.tapAsync
钩子(真正编译开始的钩子)里调用了compilation.addEntry(context, dep, options,...
。
除此之外,还有DllEntryPlugin
,DynamicEntryPlugin
,DynamicEntryPlugin
,ContainerPlugin
这四个插件都在compiler.hooks.make
钩子调用了addEntry
方法。
先上图,好像有点小,可能需要放大看,后面还会给出更详细的流程图,这个是看个大概。
此处再盗用一张图,可以结合我的中文描述一起看看,便于理解:
大致流程是:
- 入口文件调用了
addModuleTree
,根据要处理的依赖的dependency.constructor
,从webpack的dependencyFactories
中匹配相应的模块工厂函数,dependencyFactories
在很多模块插件中set
了值,基本上都是set的normalModuleFactory
。 - 接着调用了
handleModuleCreation
函数 - 之后在
this.factorizeModule(options, (err, newModule) => {})
中解析引入的模块。
下面来详细聊一下解析模块(factorizeModule
)的步骤:
- 首先调用模块工厂函数的
create
方法:factory.create
-->normalModuleFactory,创建模块实例NormalModule
- 添加模块
- 构建模块
- 处理模块依赖
- 循环调用:handleModuleCreation函数,直到解析完所有依赖为止。
构建逻辑中,用到了webpack的loader,大家可以看下面的loader处理逻辑,来进一步理解构建流程。
loader处理逻辑
1、初始化ruleSet
在创建normalModuleFactory
时,通过webpack默认的rules,以及用户传入的rules配置,得到一个ruleSet
this.ruleSet = ruleSetCompiler.compile([
{
rules: options.defaultRules
},
{
rules: options.rules
}
]);
比如,css就是需要用户配置rule的,不然解析css文件,wenpack会报错。
ERROR in ./xxx.css 1:5
Module parse failed: Unexpected token (1:5)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file.
默认的webpack的rule配置有:
{mimetype: "application/node", type: "javascript/auto"}
{test: /\.json$/i, type: "json"}
{mimetype: "application/json", type: "json"}
{test: /\.mjs$/i, type: "javascript/esm", resolve: {…}}
{test: /\.js$/i, descriptionData: {…}, type: "javascript/esm", resolve: {…}}
{test: /\.cjs$/i, type: "javascript/dynamic"}
{test: /\.js$/i, descriptionData: {…}, type: "javascript/dynamic"}
{mimetype: ["text/javascript","application/javascript"], type: "javascript/esm", resolve: {…}}
{dependency: "url", oneOf: [{scheme: /^data$/, type: "asset/inline"},{type: "asset/resource"}]
除此之外的,css,vue等文件都需要配置相应的loader。
module: {
rules: [
{
test: /\.css$/,
loader: "css-loader"
}
]
}
2、执行ruleSet,收集模块信息
在normalModuleFactory
的resolve
钩子下,会调用this.ruleSet.exec
得到module的type,value等信息。如果是css文件会得到如下result信息:
1. type: "use"
1. value:
1. 1. ident: undefined
1. loader: "css-loader"
1. options: undefined
如果type
为use
,则会在useLoaders
中push
相应的loader,后续runLoaders
的时候就会用到
if (r.type === "use") {
if (!noAutoLoaders && !noPrePostAutoLoaders) {
useLoaders.push(r.value);
}
}
3、调用runLoaders,处理模块资源
在NormalModule
中的module.build
,调用了doBuild
,里面调用了runLoaders
。loader处理后的结果,最终所有的资源都会处理成webpack的parser能够parse的类型。
例如,如下是css文件的处理结果,也是处理成了JavascriptParser
能够处理的类型:
// Imports
import ___CSS_LOADER_API_IMPORT___ from "../../node_modules/css-loader/dist/runtime/api.js";
var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(function(i){return i[1]});
// Module
___CSS_LOADER_EXPORT___.push([module.id, "html {
height: 100%;
}
", ""]);
// Exports
export default ___CSS_LOADER_EXPORT___;
4、模块资源解析
用相应的parser解析文件:this.parser.parse(this._ast || this._source.source()
,这里parser的类型与上面提到的type
的值相联系,具体可看如下图:
以JavascriptParser
为例,会对资源进行AST解析。当检测到是import或者export的声明时,就会在parser.hooks.import
钩子里,给dependency
添加依赖:parser.state.module.addDependency(sideEffectDep);
。
5、处理解析依赖
在handleParseResult
函数中,会调用module.build
传进来的callback
函数,最终会走到compilation
的processModuleDependencies
函数。然后该函数会针对之前AST解析时,添加在module
中的dependency
,循环调用processDependency
,最终得到一个sortedDependencies
。后续就是遍历依赖循环调用了,直到解析完所有依赖为止。
asyncLib.forEach(
sortedDependencies,
(item, callback) => {
this.handleModuleCreation(item, err => {
// In V8, the Error objects keep a reference to the functions on the stack. These warnings &
// errors are created inside closures that keep a reference to the Compilation, so errors are
// leaking the Compilation object.
if (err && this.bail) {
// eslint-disable-next-line no-self-assign
err.stack = err.stack;
return callback(err);
}
callback();
});
},
err => {
this.processDependenciesQueue.decreaseParallelism();
return callback(err);
}
);
文件生成逻辑
文件生成逻辑围绕chunks展开,从compilation.seal
开始。注意的节点有:
hooks.optimizeChunks
:splitChunksPlugin
就是在这里处理chunkscreateChunkAssets
:里面调用了getRenderManifest
,在compilation.hooks.renderManifest
钩子中,针对js模块,图片等assets模块,生成一个assets对象,用于后来生成打包后的文件
seal
原意密封、上锁,我个人理解在 webpack 语境下接近于 “将模块装进蜜罐” 。seal
函数主要完成从 module
到 chunks
的转化,核心流程由盗图一张:
至于打包出来的js代码是怎么生成的,一方面是
__webpack_modules__
(const chunkModules = Template.renderChunkModules
),一方面是webpack启动代码,都是通过webpack-sources
(renderBootstrap
)来拼接的。看如下逻辑:
webpack-sources
webpack的代码拼接以及生成sourceMap都是依赖的这个包。
Source(基类)
Source是所有webpack-sources实现类的基类,是一个抽象类。
名称 | 类型 | 参数 | 备注 |
---|---|---|---|
source | () => source | 获得传入实例中的字符串代码 | |
buffer | () => BufferString | 获得 Buffer 字符串代码 | |
size | () => BufferString.length | 获得 source 转换为 BufferString 的长度 | |
map | (options) => (options) => SourceMapObj | {columns: false} | 获取传入代码的 sourcemap 信息,参数为生成的 sourcemap 是否包含列信息 |
sourceAndMap | (options) => ({source, map}) | {columns: false} | 此方法整合了 source 和 map 方法的返回值,将其一并返回 |
updateHash | (hash) => void | 要更新替换的 hash 值 | 更新当前 Source 的 hash 值 |
OriginalSource(可以生成代码的 sourcemap 对象)
const header = Template.asString(bootstrap.header) + "\n";
new PrefixSource(
prefix,
useSourceMap
? new OriginalSource(header, "webpack/bootstrap")
: new RawSource(header)
)
ConcatSource(组合资源片段)
let source = new ConcatSource();
if (
chunkModules ||
runtimeRequirements.has(RuntimeGlobals.moduleFactories) ||
runtimeRequirements.has(RuntimeGlobals.moduleFactoriesAddOnly)
) {
source.add(prefix + "var __webpack_modules__ = (");
source.add(chunkModules || "{}");
source.add(");\n");
source.add(
"/************************************************************************/\n"
);
}
最终生成的代码如下:
/******/ var __webpack_modules__ = {
/***/ "./example.js":
/*!********************************!*\
!*** ./example.js + 3 modules ***!
\********************************/
/***/ (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
) => {
eval(
'\n// EXPORTS\n__webpack_require__.d(__webpack_exports__, {\n "add": () => (/* reexport */ add),\n "addMore": () => (/* reexport */ addMore)\n});\n\n;// CONCATENATED MODULE: ./add.js\nfunction add(a, b) {\n\treturn a + b;\n}\n\n// EXTERNAL MODULE: ./images/file.png\nvar file = __webpack_require__("./images/file.png");\n;// CONCATENATED MODULE: ./addMore.js\n\n\nfunction addMore(a, b) {\n\treturn add(a, b) + 22;\n}\n\n;// CONCATENATED MODULE: ./index.json\nconst index_namespaceObject = {};\n;// CONCATENATED MODULE: ./example.js\n// ImportDeclaration\n// ./add --> statement.source.value\n// parser.hooks.import.call() --> HarmonyImportDependencyParserPlugin\n\n\n// ImportDeclaration\n\nconsole.log(file);\naddMore(1, 2);\nadd(1, 2);\n// HarmonyImportSideEffectDependency\n\n// CommonJsRequireDependency\n__webpack_require__(/*! ./index.css */ "./index.css");\n// "ExpressionStatement" "ExpressionStatement" "ExportNamedDeclaration" --> HarmonyExportDependencyParserPlugin\n// harmonyNamedExports.add(\'add\') -- HarmonyExportSpecifierDependency\n\n\n\n//# sourceURL=webpack:///./example.js_+_3_modules?'
);
/***/
},
/***/ "./images/file.png":
/*!*************************!*\
!*** ./images/file.png ***!
\*************************/
/***/ (module, __unused_webpack_exports, __webpack_require__) => {
eval(
'module.exports = __webpack_require__.p + "89a353e9c515885abd8e.png";\n\n//# sourceURL=webpack:///./images/file.png?'
);
/***/
},
/***/ "./index.css":
/*!*******************!*\
!*** ./index.css ***!
\*******************/
/***/ (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 */ });\n/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../node_modules/css-loader/dist/runtime/api.js */ "../../node_modules/css-loader/dist/runtime/api.js");\n/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_0__);\n// Imports\n\nvar ___CSS_LOADER_EXPORT___ = _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_0___default()(function(i){return i[1]});\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, "html {\\n\\theight: 100%;\\n}\\n", ""]);\n// Exports\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (___CSS_LOADER_EXPORT___);\n\n\n//# sourceURL=webpack:///./index.css?'
);
/***/
},
/***/ "../../node_modules/css-loader/dist/runtime/api.js":
/*!*********************************************************!*\
!*** ../../node_modules/css-loader/dist/runtime/api.js ***!
\*********************************************************/
/***/ module => {
eval(
'\n\n/*\n MIT License http://www.opensource.org/licenses/mit-license.php\n Author Tobias Koppers @sokra\n*/\n// css base code, injected by the css-loader\n// eslint-disable-next-line func-names\nmodule.exports = function (cssWithMappingToString) {\n var list = []; // return the list of modules as css string\n\n list.toString = function toString() {\n return this.map(function (item) {\n var content = cssWithMappingToString(item);\n\n if (item[2]) {\n return "@media ".concat(item[2], " {").concat(content, "}");\n }\n\n return content;\n }).join("");\n }; // import a list of modules into the list\n // eslint-disable-next-line func-names\n\n\n list.i = function (modules, mediaQuery, dedupe) {\n if (typeof modules === "string") {\n // eslint-disable-next-line no-param-reassign\n modules = [[null, modules, ""]];\n }\n\n var alreadyImportedModules = {};\n\n if (dedupe) {\n for (var i = 0; i < this.length; i++) {\n // eslint-disable-next-line prefer-destructuring\n var id = this[i][0];\n\n if (id != null) {\n alreadyImportedModules[id] = true;\n }\n }\n }\n\n for (var _i = 0; _i < modules.length; _i++) {\n var item = [].concat(modules[_i]);\n\n if (dedupe && alreadyImportedModules[item[0]]) {\n // eslint-disable-next-line no-continue\n continue;\n }\n\n if (mediaQuery) {\n if (!item[2]) {\n item[2] = mediaQuery;\n } else {\n item[2] = "".concat(mediaQuery, " and ").concat(item[2]);\n }\n }\n\n list.push(item);\n }\n };\n\n return list;\n};\n\n//# sourceURL=webpack:///../../node_modules/css-loader/dist/runtime/api.js?'
);
/***/
}
/******/
};