1. 回顾
上文我们介绍了webpack在代码生成阶段做的事情。
我们知道,webpack调用了compiler.hooks.make
加载资源,
它会先加载loader,然后用loader加载源文件,
对于js而言,babel-loader会返回转换后的es5代码,而不是AST。
加载完资源之后,webpack就会调用compilation.seal
来生成代码,
compilation.seal
中调用了一大堆hooks,
其中最重要的两件事情是,createChunkAssets
和optimizeChunkAssets
。
(1)createChunkAssets
会填充compilation.assets
对象,
compilation.assets
中保存了待生成的目标文件名,和文件内容。
(2)optimizeChunkAssets
会调用uglifyjs-webpack-plugin进行代码压缩,
而uglifyjs-webpack-plugin则引用了uglify-es,worker-farm协助完成工作。
本文就从uglifyjs-webpack-plugin开始介绍,
其中worker-farm还涉及了Node.js内置模块child_process。
2. 进入uglifyjs-wepack-plugin
2.1 compilation.hooks.optimizeChunkAssets
上一篇中我们介绍了,之所以会用到uglifyjs-wepack-plugin,
是因为compilation.seal
中调用了compilation.hooks.optimizeChunkAssets
。
而compilation.hooks.optimizeChunkAssets的实现,位于 uglifyjs-webpack-plugin/src/index.js 第339行。
compilation.hooks.optimizeChunkAssets.tapAsync(plugin, optimizeFn.bind(this, compilation));
该hooks的代码逻辑主要位于optimizeFn
中,它在 index.js 第138行。
const optimizeFn = (compilation, chunks, callback) => {
...
runner.runTasks(tasks, (tasksError, results) => {
...
callback();
});
};
2.2 runner.runTasks
我们看到,optimizeFn
函数调用了runner.runTasks
,
其中runner
是由 uglifyjs-webpack-plugin/src/uglify/Runner.js 导出的。
export default class Runner {
...
runTasks(tasks, callback) {
...
}
...
}
runTasks
方法在 Runner.js 第25行。
runTasks(tasks, callback) {
...
if (this.maxConcurrentWorkers > 1) {
...
this.workers = workerFarm(workerOptions, workerFile);
this.boundWorkers = (options, cb) => this.workers(serialize(options), cb);
} else {
this.boundWorkers = (options, cb) => {
...
cb(null, minify(options));
};
}
...
const step = (index, data) => {
...
callback(null, results);
};
tasks.forEach((task, index) => {
const enqueue = () => {
this.boundWorkers(task, (error, data) => {
...
const done = () => step(index, result);
...
done();
});
};
...
cacache.get(this.cacheDir, serialize(task.cacheKeys)).then(({ data }) => step(index, JSON.parse(data)), enqueue);
});
}
下面我们仔细分析一下这个函数,这是一个关键点。
(1)parallel 模式
首先,if (this.maxConcurrentWorkers > 1) {
,这个条件,
是判断 uglifyjs-webpack-plugin是否开启了parallel
,可参考github仓库中的文档,README.md #Options。
值得注意的是,这里虽然文档上写了parallel
默认为false
,
但是webpack内部集成uglifyjs-webpack-plugin的时候,显式传入了true
,
代码位于,webpack/lib/WebpackOptionsDefaulter.js 第310行。
this.set("optimization.minimizer", "make", options => [
{
apply: compiler => {
// Lazy load the uglifyjs plugin
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
const SourceMapDevToolPlugin = require("./SourceMapDevToolPlugin");
new UglifyJsPlugin({
cache: true,
parallel: true,
sourceMap:
(options.devtool && /source-?map/.test(options.devtool)) ||
(options.plugins &&
options.plugins.some(p => p instanceof SourceMapDevToolPlugin))
}).apply(compiler);
}
}
]);
所以,我们使用示例工程进行调试的时候,if (this.maxConcurrentWorkers > 1) {
为真,
表示启用了parallel模式进行压缩。
注:
如果我们在webpack.config.js中,手动引入uglifyjs-webpack-plugin,并设置parallel
为false
,
...
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
...
plugins: [
new UglifyJsPlugin({
parallel: false,
}),
],
...
};
就会关闭parallel模式,逻辑走到这里,
else {
this.boundWorkers = (options, cb) => {
...
cb(null, minify(options));
};
}
this.boundWorkers
的值绑定为不同的值。
this.boundWorkers
什么时候被调用,我们之后再详细介绍(本文第2.3节)。
(2)worker-farm
下面我们再回到parallel模式,parallel模式中会调用workerFarm
创建workers
,
最后,this.boundWorkers
的调用会导致workers
被调用。
if (this.maxConcurrentWorkers > 1) {
...
this.workers = workerFarm(workerOptions, workerFile);
this.boundWorkers = (options, cb) => this.workers(serialize(options), cb);
}
可是workers
是什么呢?workerFarm
又是什么?
workerFarm实际上是引用了一个独立的代码库,worker-farm(v1.6.0)。
它内部会调用Node.js内置模块child_process来完成任务。
具体用法如下,
首先我们要新建一个child.js文件,子进程中会执行这些代码,
module.exports = function (inp, callback) {
callback(null, inp + ' BAR (' + process.pid + ')')
}
然后我们在main.js文件中,使用worker-farm开启子进程,
var workerFarm = require('worker-farm')
, workers = workerFarm(require.resolve('./child'))
, ret = 0
for (var i = 0; i < 10; i++) {
workers('#' + i + ' FOO', function (err, outp) {
console.log(outp)
if (++ret == 10)
workerFarm.end(workers)
})
}
以上代码,只有到了第6
行workers被调用的时候,
Node.js才会加载并执行子进程中的代码,
被加载的子进程文件,我们可以查看下workerFile
,
~/Test/debug-webpack/node_modules/_uglifyjs-webpack-plugin@1.3.0@uglifyjs-webpack-plugin/dist/uglify/worker.js
源代码位置,位于uglifyjs-webpack-plugin/src/uglify/worker.js,
其中,第17行,调用了minify
来进行代码压缩。
callback(null, minify(options));
worker-farm的内部逻辑,我们这里暂且略过,
唯一会引起我们困扰的是,child.js中无法打断点,使用vscode调试的时候,也不会跳进去。
所以,我们只能在里面写log来确定child.js执行了。
child.js中代码执行完了之后,调用callback
,会触发main.js 中workers
的回调。
因此对于uglifyjs-webpack-plugin而言,
this.boundWorkers = (options, cb) => this.workers(serialize(options), cb);
this.workers
中工作做完后,会导致this.boundWorkers
的回调cb
被触发。
(3)一路callback
我们再来看下runTasks
的代码,
runTasks(tasks, callback) {
...
if (this.maxConcurrentWorkers > 1) {
...
this.workers = workerFarm(workerOptions, workerFile);
this.boundWorkers = (options, cb) => this.workers(serialize(options), cb);
} else {
this.boundWorkers = (options, cb) => {
...
cb(null, minify(options));
};
}
...
const step = (index, data) => {
...
callback(null, results);
};
tasks.forEach((task, index) => {
const enqueue = () => {
this.boundWorkers(task, (error, data) => {
...
const done = () => step(index, result);
...
done();
});
};
...
cacache.get(this.cacheDir, serialize(task.cacheKeys)).then(({ data }) => step(index, JSON.parse(data)), enqueue);
});
}
以上代码第6
行,this.workers
完成后回调,会导致this.boundWorkers
返回,
this.boundWorkers
返回,在第23行,会调用done();
done();
会调用step
,step
会调用callback
。
step
中的callback
,就是runTasks
的callback
。
这样runTasks
就结束了,回到了optimizeFn
中,继而完成了compilation.hooks.optimizeChunkAssets
,
最终回到了 Compilation.js 中,第1283行。
this.hooks.optimizeChunkAssets.callAsync(this.chunks, err => {
// 这里
});
这样就完成compilation.hooks.optimizeChunkAssets
调用了。
2.3 enqueue
上文中我们留下了一个疑问,this.boundWorkers
到底是什么时候触发的呢?
答案是,它是在runTasks中enqueue
函数里触发的。
源码位于,Runner.js 第59行,
tasks.forEach((task, index) => {
const enqueue = () => {
this.boundWorkers(task, (error, data) => {
...
});
};
if (this.cacheDir) {
cacache.get(this.cacheDir, serialize(task.cacheKeys)).then(({ data }) => step(index, JSON.parse(data)), enqueue);
} else {
enqueue();
}
});
而enqueue
可能会由cacahe.get
调用,也可能在else
语句中直接调用。
(1)cacache缓存
通过调试,我们发现,cacheDir
总是有值的,默认开启了缓存,
~/Test/debug-webpack/node_modules/.cache/uglifyjs-webpack-plugin
我们进入该目录查看一下文件结构,
uglifyjs-webpack-plugin
├── content-v2
│ └── sha512
│ └── d9
│ └── 62
│ └── a6889cb7fcb5f74679b4995fc488b42058fa7d1974386c75e566747c31bff92b1690152d887937e411caa9618019c796801b4879f3c927bff72da41e4080
├── index-v5
│ └── f9
│ └── 62
│ └── ab8974c954e9577998f607845e6ff5dc6acf034140aaf851b6a3fbf93ead
└── tmp
其中,content-v2/sha512/d9/62/... 那个长文件的内容如下,
{"code":"!function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){\"undefined\"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:\"Module\"}),Object.defineProperty(e,\"__esModule\",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&\"object\"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,\"default\",{enumerable:!0,value:e}),2&t&&\"string\"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,\"a\",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p=\"\",r(r.s=0)}([function(e,t){}]);","extractedComments":[]}
index-v5/f9/62/... 这个长文件的内容如下,
3f12b2ef5f09ba45b021cbeb26a3b0356b1e42df {
"key": "{\"uglify-es\":\"3.3.9\",\"uglifyjs-webpack-plugin\":\"1.3.0\",\"uglifyjs-webpack-plugin-options\":{\"test\":/\\.js(\\?.*)?$/i,\"warningsFilter\":function () {\n return true;\n },\"extractComments\":false,\"sourceMap\":false,\"cache\":true,\"cacheKeys\":function (defaultCacheKeys) {\n return defaultCacheKeys;\n },\"parallel\":true,\"uglifyOptions\":{\"compress\":{\"inline\":1},\"output\":{\"comments\":/^\\**!|@preserve|@license|@cc_on/}}},\"path\":\"\\u002FUsers\\u002Fthzt\\u002FTest\\u002Fdebug-webpack\\u002Fdist\\u002Findex.js\",\"hash\":\"27c9fda4f852c4a1e09c203bd9f77a56\"}",
"integrity": "sha512-2WKmiJy3/LX3Rnm0mV/EiLQgWPp9GXQ4bHXlZnR8Mb/5KxaQFS2IeTfkEcqpYYAZx5aAG0h588knv/ctpB5AgA==",
"time": 1540290187493,
"size": 980
}
它们分别存储了缓存的key
和value
,value
就是uglifyjs minify
后的代码。
(2)then(..., enqueue)
如果有缓存,就不会触发enqueue
,也就不会触发this.boundWorkers
,继而不会触发this.workers
被调用,
代码就不会再次被minify
了。
而如果没有缓存,会发生什么呢?
我们把缓存目录删掉,
~/Test/debug-webpack/node_modules/.cache/uglifyjs-webpack-plugin
通过逐行调试,我们发现最终在 cacache/get.js 第38行 抛了一个异常,
return (
...
).then(entry => {
if (...) {
throw new index.NotFoundError(cache, key)
}
...
})
这个异常是在promise.then
中抛出的,
因此,Runner.js 第72行,调用cacahe.get
的地方,就会触发then
的第二个回调参数,
cacache.get(this.cacheDir, serialize(task.cacheKeys)).then(({ data }) => step(index, JSON.parse(data)), enqueue);
这个回调正好是enqueue
。
欲知后事如何,且待我下回分解。
参考
uglifyjs-webpack-plugin v1.3.0
worker-farm v1.6.0
cacache v10.0.4