1. 精彩回顾
1.1 历史篇章
本系列文章已经进展到了第六篇,
在第一篇中,我们介绍了一个小游戏,游戏的主角穿越到了游戏中,
只有集齐金庸的14天书,才能回到现实世界中。
这非常类似于我们学习webpack源码的过程,不同的是,我们穿越到了代码世界中。
第二篇我们做了一些准备工作,先用nvm管理Node.js版本,
然后创建了一个称为debug-webpack的示例应用,
以此为入口,打开了webpack世界的大门。
为了跟踪程序的执行过程,第三篇中我介绍了自己常用的两个方法,
一个是写log,另一个是使用vscode对程序进行调试。
在这一篇中,我们还简要分析了webpack命令行工具,和webpack-cli的代码逻辑。
接下来的第四篇,是非常烧脑的一篇,
在这篇中,我们理清了完整的资源加载过程。
webpack会递归的加载每个入口文件loader,然后再用loader加载文件。
我们的示例中,涉及了webpack,loader-runner和babel-loader。
第五篇,我们介绍了hooks的原理,
webpack使用一个名为tapable的代码库,实现了强大的切面功能,
我们可以编写插件,利用webpack预留的各个切面进行编程。
到目前为止,我们已经对webpack资源加载和webpack hooks,有了一定的了解了,
所以从本文开始,让我们继续往下进行吧。
看看加载的这些文件,是怎样生成最终代码的,
这有点类似于通常的编译器优化和后端做的事情。
在此之前,我们再回顾一下debug-webpack和debug.js
1.2 debug-webpack示例应用
还记得我们的debug-webpack示例应用么?
这是第二篇中我们创建的一个小项目,简单配置了一下webpack.config.js。
const path = require('path');
const fs = require('fs');
module.exports = {
entry: {
index: path.resolve(__dirname, 'src/index.js'),
},
output: {
path: path.resolve(__dirname, 'dist/'),
},
module: {
rules: [
{ test: /\.js$/, use: { loader: 'babel-loader', query: { presets: ['@babel/preset-env'] } } },
]
}
};
没有使用任何插件,只是指定了entry
为 ./src/index.js,
然后借助babel-loader加载它,最后将目标代码生成到 ./dist/ 中。
1.3 debug.js
再来看看为了调试webpack,我们在第三篇中新建的 ./debug.js 吧,
const webpack = require('webpack');
const options = require('./webpack.config');
const compiler = webpack(options);
compiler.run((...args) => {
console.log(args);
});
还记得么,根据第三篇介绍的方法,我们将程序停在了断点处。
1.4 资源加载过程
详细的资源加载过程,我们已经在第四篇中介绍了,
这里我们来简单概括下,一图胜千言,
(1)compiler.run
方法在 Compiler.js 第268行 调用了compiler.compile
run(callback) {
...
this.hooks.beforeRun.callAsync(this, err => {
...
this.hooks.run.callAsync(this, err => {
...
this.readRecords(err => {
...
this.compile(onCompiled);
});
});
});
}
(2)compiler.compile
在Compiler.js 第536行 调用了compiler.hooks.make
compile(callback) {
...
this.hooks.beforeCompile.callAsync(params, err => {
...
this.hooks.make.callAsync(compilation, err => {
...
});
});
}
(3)compiler.hooks.make
是在 SingleEntryPlugin.js 第40行 实现的,它调用了compilation.addEntry
并且,将compiler.hooks.make
的callback
传递给了compilation.addEntry
compiler.hooks.make.tapAsync(
"SingleEntryPlugin",
(compilation, callback) => {
...
compilation.addEntry(context, dep, name, callback);
}
);
(4)compilation.addEntry
位于 Compilation.js 第1027行,它调用了compilation._addModuleChain
完了之后回调到compiler.hooks.make
addEntry(context, entry, name, callback) {
...
this._addModuleChain(
...,
(err, module) => {
...
return callback(null, module);
}
);
}
(5)compilation._addModuleChain
在 Compilation.js 第943行 调用了 moduleFactory.create
然后调用compilation.buildModule
进行构建,buildModule
完了之后,调用了afterBuild
,
afterBuild
调用compilation.processModuleDependencies
处理模块的依赖,最后回调callback
。
_addModuleChain(context, dependency, onModule, callback) {
...
this.semaphore.acquire(() => {
moduleFactory.create(
...,
(err, module) => {
...
const afterBuild = () => {
...
if (addModuleResult.dependencies) {
this.processModuleDependencies(module, err => {
...
callback(null, module);
});
} else {
return callback(null, module);
}
};
...
this.buildModule(module, false, null, null, err => {
...
afterBuild();
});
}
);
});
}
afterBuild
的回调,会导致this._addModuleChain
返回,
compilation._addModuleChain
返回会导致compiler.hooks.make
返回,
最后回到了Compiler.js 第537行。
compile(callback) {
...
this.hooks.beforeCompile.callAsync(params, err => {
...
this.hooks.make.callAsync(compilation, err => {
// 这里
});
});
}
到此为止,我们又回到了Compiler.js中,完成了compiler.hooks.make
调用。
2. compilation.seal
我们来到了Compiler.js 第537行,
...
this.hooks.make.callAsync(compilation, err => {
...
compilation.seal(err => {
...
});
});
发现webpack在完成compiler.hooks.make
之后,调用了compilation.seal
,位于Compilation.js 第1159行。
这是一个长达143
行的函数,从1159-1301行,
在其中进行了大量的hooks操作。
下面我们来挑选两个重要的环节进行说明,一图胜千言,
2.1 createChunkAssets
第一个比较重要的环节是,createChunkAssets
,
它会得到所有待生成的文件名,以及未优化的文件内容。
(1)调用
对createChunkAssets
的调用,位于 Compilation.js 第1270行,
在调用之前,触发了compilation.hooks.beforeChunkAssets
。
if (this.hooks.shouldGenerateChunkAssets.call() !== false) {
this.hooks.beforeChunkAssets.call();
this.createChunkAssets();
}
(2)实现
createChunkAssets
是compilation
实例的一个方法,位于Compilation.js 第2313行,
代码流程大体如下,
createChunkAssets() {
...
for (let i = 0; i < this.chunks.length; i++) {
...
try {
...
for (const fileManifest of manifest) {
...
file = this.getPath(filenameTemplate, fileManifest.pathOptions);
...
if (
...
) {
...
} else {
source = fileManifest.render();
...
}
...
this.assets[file] = source;
...
this.hooks.chunkAsset.call(chunk, file);
...
}
} catch (err) {
...
}
}
}
调用getPath
得到文件名file
,然后调用fileManifest.render
得到文件内容source
。
最后保存到compilation.assets
中。
我们来看一下示例工程debug-webpack中,file
和source
分别是什么。
在 Compilation.js 第2393行 打个断点。
this.assets[file] = source;
file
的值是,
index.js
source
的值是,
{
"_source": {
"children": [
"/******/ (function(modules) { // webpackBootstrap\n",
{
"_source": {
"_value": " \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 0);\n",
"_name": "webpack/bootstrap"
},
"_prefix": "/******/"
},
"/******/ })\n",
"/************************************************************************/\n",
"/******/ (",
"[\n",
"/* 0 */",
"\n",
"/***/ (function(module, exports) {\n\n",
{
"_source": {
"_source": {
"_value": "alert();",
"_name": "~/Test/debug-webpack/node_modules/[email protected]@babel-loader/lib/index.js??ref--4!~/Test/debug-webpack/src/index.js"
},
"replacements": []
},
"_cachedMaps": {}
},
"\n\n/***/ })",
"\n/******/ ]",
")",
";"
]
},
"_cachedMaps": {}
}
其中,source._source.children
数组中包含了待优化的源码。
我们的源文件,在以上代码的第22行,
"_value": "alert();"
(3)修改一下源文件
我们修改一下 ./src/index.js 的内容,
const a = 1;
发现source
中其他内容都没变,只有第22行改变了,
"_value": "var a = 1;",
这里有一点值得注意,我们的源文件是使用babel-loader加载的,
babel-loader加载后的文件,在内存中并不是AST,而是转换后的代码。
在第四篇中,我们也提到了这一点。
因此,我们分析的createChunkAssets
,与载入资源的时间点,并没有相距太远,
这里是直接拿到了babel-loader返回的结果,再增加了一些辅助代码,最后放入到source
变量中。
(4)compilation.hooks.chunkAssets
source
被保存到compilation.assets
中之后,
webpack就调用了compilation.hooks.chunkAsset
。
this.assets[file] = source;
...
this.hooks.chunkAsset.call(chunk, file);
因此在写webpack插件时,我们可以利用这个hooks,获取compilation.assets
。
在这个hooks之前,compilation.assets
还没有值。
2.2 compilatin.hooks.optimizeChunkAssets
获取了待生成的文件名file
和文件内容source
之后,剩下的另一个重要环节就是压缩了,
生成的代码会经过uglify-es进行压缩,
我们来看看这个过程是怎么进行的。
(1)optimizeChunkAssets
上文中,我们讨论了,
compilation.seal
方法中,webpack在Compilation.js 第1270行 调用了createChunkAssets
,
这件事完成之后,compilation.assets
就有值了,
其中包含了待生成的文件名file
和文件内容source
。
于是紧接着,后面仍然还是在compilation.seal
中,
webpack在Compilation.js 第1282行 调用了 compilatin.hooks.optimizeChunkAssets
,
this.hooks.optimizeChunkAssets.callAsync(this.chunks, err => {
...
});
这里调试起来很困难,要跟进到tapable代码库里面,
最终我们找到了,这个hooks是由 uglifyjs-webpack-plugin(v1.3.0) 实现的。
本地路径在这里,
~/Test/debug-webpack/node_modules/[email protected]@uglifyjs-webpack-plugin/dist/index.js
源码位置,在 uglifyjs-webpack-plugin/src/index.js 第339行,
compilation.hooks.optimizeChunkAssets.tapAsync(plugin, optimizeFn.bind(this, compilation));
注:
在进行调试的时候,我们只能跟进node_modules中uglifyjs-webpack-plugin的安装包内,
这些代码都是编译后的,存在于dist文件夹中,
为了便于理解,我们后面贴代码的话,都像上面这样,贴uglifyjs-webpack-plugin github仓库的源码。
(2)uglifyjs-webpack-plugin(v1.3.0)
uglifyjs-webpack-plugin 实现了compilation.hooks.optimizeChunkAssets
,
具体是代码逻辑在optimizeFn
中,位于 index.js 第130行。
const optimizeFn = (compilation, chunks, callback) => {
...
runner.runTasks(tasks, (tasksError, results) => {
...
callback();
});
};
这又是一个长函数,从index.js 第130行-第328行,总共199
行。
它先创建了一些tasks
,然后用runner
去运行这些tasks
。
tasks
执行完之后,调用callback
,会导致compilation.hooks.optimizeChunkAssets
返回。
其中,runner
由 uglifyjs-webpack-plugin/src/uglify/Runner.js 导出。
export default class Runner {
...
runTasks(tasks, callback) {
...
}
...
}
runTasks
的实现位于,Runner.js 第25行。
runTasks
主要做了两件重要的事情,
一个是使用Node.js内置模块 child_process,对代码进行压缩(minify)
另一个,则是对uglifyjs minify的结果进行缓存。
我们下一篇中再详细介绍。
(3)回到compilation.seal
上文提到,uglifyjs-webpack-plugin 中optimizeFn
执行完后调用callback
,
会导致tasks
执行完之后,调用callback
,会导致compilation.hooks.optimizeChunkAssets
返回。
const optimizeFn = (compilation, chunks, callback) => {
...
runner.runTasks(tasks, (tasksError, results) => {
...
callback();
});
};
执行流程就回到了 Compilation.js 第1283行,它位于compilation.seal
函数中,
this.hooks.optimizeChunkAssets.callAsync(this.chunks, err => {
// 这里
});
至于uglifyjs-webpack-plugin中,到底是如何进行压缩和缓存的,
compilation.seal后续又做了哪些事情,
且听我下回分解。
参考
webpack v4.20.2
uglifyjs-webpack-plugin v1.3.0