1. 回顧

上文我們介紹了webpack在代碼生成階段做的事情。
我們知道,webpack調(diào)用了compiler.hooks.make加載資源,
它會(huì)先加載loader,然后用loader加載源文件,
對(duì)于js而言,babel-loader會(huì)返回轉(zhuǎn)換后的es5代碼,而不是AST。
加載完資源之后,webpack就會(huì)調(diào)用compilation.seal來(lái)生成代碼,
compilation.seal中調(diào)用了一大堆hooks,
其中最重要的兩件事情是,createChunkAssets和optimizeChunkAssets。
(1)createChunkAssets會(huì)填充compilation.assets對(duì)象,
compilation.assets中保存了待生成的目標(biāo)文件名,和文件內(nèi)容。
(2)optimizeChunkAssets會(huì)調(diào)用uglifyjs-webpack-plugin進(jìn)行代碼壓縮,
而uglifyjs-webpack-plugin則引用了uglify-es,worker-farm協(xié)助完成工作。
本文就從uglifyjs-webpack-plugin開(kāi)始介紹,
其中worker-farm還涉及了Node.js內(nèi)置模塊child_process。
2. 進(jìn)入uglifyjs-wepack-plugin

2.1 compilation.hooks.optimizeChunkAssets
上一篇中我們介紹了,之所以會(huì)用到uglifyjs-wepack-plugin,
是因?yàn)?code>compilation.seal中調(diào)用了compilation.hooks.optimizeChunkAssets。

而compilation.hooks.optimizeChunkAssets的實(shí)現(xiàn),位于 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 函數(shù)調(diào)用了runner.runTasks,
其中runner是由 uglifyjs-webpack-plugin/src/uglify/Runner.js 導(dǎo)出的。
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);
});
}
下面我們仔細(xì)分析一下這個(gè)函數(shù),這是一個(gè)關(guān)鍵點(diǎn)。
(1)parallel 模式
首先,if (this.maxConcurrentWorkers > 1) { ,這個(gè)條件,
是判斷 uglifyjs-webpack-plugin是否開(kāi)啟了parallel,可參考github倉(cāng)庫(kù)中的文檔,README.md #Options。
值得注意的是,這里雖然文檔上寫了parallel默認(rèn)為false,
但是webpack內(nèi)部集成uglifyjs-webpack-plugin的時(shí)候,顯式傳入了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);
}
}
]);
所以,我們使用示例工程進(jìn)行調(diào)試的時(shí)候,if (this.maxConcurrentWorkers > 1) { 為真,
表示啟用了parallel模式進(jìn)行壓縮。
注:
如果我們?cè)趙ebpack.config.js中,手動(dòng)引入uglifyjs-webpack-plugin,并設(shè)置parallel為false,
...
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
...
plugins: [
new UglifyJsPlugin({
parallel: false,
}),
],
...
};
就會(huì)關(guān)閉parallel模式,邏輯走到這里,
else {
this.boundWorkers = (options, cb) => {
...
cb(null, minify(options));
};
}
this.boundWorkers的值綁定為不同的值。
this.boundWorkers什么時(shí)候被調(diào)用,我們之后再詳細(xì)介紹(本文第2.3節(jié))。
(2)worker-farm

下面我們?cè)倩氐絧arallel模式,parallel模式中會(huì)調(diào)用workerFarm創(chuàng)建workers,
最后,this.boundWorkers的調(diào)用會(huì)導(dǎo)致workers被調(diào)用。
if (this.maxConcurrentWorkers > 1) {
...
this.workers = workerFarm(workerOptions, workerFile);
this.boundWorkers = (options, cb) => this.workers(serialize(options), cb);
}
可是workers 是什么呢?workerFarm又是什么?
workerFarm實(shí)際上是引用了一個(gè)獨(dú)立的代碼庫(kù),worker-farm(v1.6.0)。
它內(nèi)部會(huì)調(diào)用Node.js內(nèi)置模塊child_process來(lái)完成任務(wù)。
具體用法如下,
首先我們要新建一個(gè)child.js文件,子進(jìn)程中會(huì)執(zhí)行這些代碼,
module.exports = function (inp, callback) {
callback(null, inp + ' BAR (' + process.pid + ')')
}
然后我們?cè)趍ain.js文件中,使用worker-farm開(kāi)啟子進(jìn)程,
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被調(diào)用的時(shí)候,
Node.js才會(huì)加載并執(zhí)行子進(jìn)程中的代碼,
被加載的子進(jìn)程文件,我們可以查看下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行,調(diào)用了minify來(lái)進(jìn)行代碼壓縮。
callback(null, minify(options));
worker-farm的內(nèi)部邏輯,我們這里暫且略過(guò),
唯一會(huì)引起我們困擾的是,child.js中無(wú)法打斷點(diǎn),使用vscode調(diào)試的時(shí)候,也不會(huì)跳進(jìn)去。
所以,我們只能在里面寫log來(lái)確定child.js執(zhí)行了。
child.js中代碼執(zhí)行完了之后,調(diào)用callback,會(huì)觸發(fā)main.js 中workers的回調(diào)。
因此對(duì)于uglifyjs-webpack-plugin而言,
this.boundWorkers = (options, cb) => this.workers(serialize(options), cb);
this.workers中工作做完后,會(huì)導(dǎo)致this.boundWorkers的回調(diào)cb被觸發(fā)。
(3)一路callback

我們?cè)賮?lái)看下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完成后回調(diào),會(huì)導(dǎo)致this.boundWorkers返回,
this.boundWorkers返回,在第23行,會(huì)調(diào)用done();
done(); 會(huì)調(diào)用step,step會(huì)調(diào)用callback。
step中的callback,就是runTasks的callback。
這樣runTasks就結(jié)束了,回到了optimizeFn中,繼而完成了compilation.hooks.optimizeChunkAssets,
最終回到了 Compilation.js 中,第1283行。
this.hooks.optimizeChunkAssets.callAsync(this.chunks, err => {
// 這里
});
這樣就完成compilation.hooks.optimizeChunkAssets調(diào)用了。
2.3 enqueue
上文中我們留下了一個(gè)疑問(wèn),this.boundWorkers到底是什么時(shí)候觸發(fā)的呢?
答案是,它是在runTasks中enqueue函數(shù)里觸發(fā)的。
源碼位于,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可能會(huì)由cacahe.get調(diào)用,也可能在else語(yǔ)句中直接調(diào)用。
(1)cacache緩存

通過(guò)調(diào)試,我們發(fā)現(xiàn),cacheDir總是有值的,默認(rèn)開(kāi)啟了緩存,
~/Test/debug-webpack/node_modules/.cache/uglifyjs-webpack-plugin
我們進(jìn)入該目錄查看一下文件結(jié)構(gòu),
uglifyjs-webpack-plugin
├── content-v2
│ └── sha512
│ └── d9
│ └── 62
│ └── a6889cb7fcb5f74679b4995fc488b42058fa7d1974386c75e566747c31bff92b1690152d887937e411caa9618019c796801b4879f3c927bff72da41e4080
├── index-v5
│ └── f9
│ └── 62
│ └── ab8974c954e9577998f607845e6ff5dc6acf034140aaf851b6a3fbf93ead
└── tmp
其中,content-v2/sha512/d9/62/... 那個(gè)長(zhǎng)文件的內(nèi)容如下,
{"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/... 這個(gè)長(zhǎng)文件的內(nèi)容如下,
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
}
它們分別存儲(chǔ)了緩存的key和value,value就是uglifyjs minify后的代碼。
(2)then(..., enqueue)
如果有緩存,就不會(huì)觸發(fā)enqueue,也就不會(huì)觸發(fā)this.boundWorkers,繼而不會(huì)觸發(fā)this.workers被調(diào)用,
代碼就不會(huì)再次被minify了。
而如果沒(méi)有緩存,會(huì)發(fā)生什么呢?
我們把緩存目錄刪掉,
~/Test/debug-webpack/node_modules/.cache/uglifyjs-webpack-plugin
通過(guò)逐行調(diào)試,我們發(fā)現(xiàn)最終在 cacache/get.js 第38行 拋了一個(gè)異常,
return (
...
).then(entry => {
if (...) {
throw new index.NotFoundError(cache, key)
}
...
})
這個(gè)異常是在promise.then中拋出的,
因此,Runner.js 第72行,調(diào)用cacahe.get的地方,就會(huì)觸發(fā)then的第二個(gè)回調(diào)參數(shù),
cacache.get(this.cacheDir, serialize(task.cacheKeys)).then(({ data }) => step(index, JSON.parse(data)), enqueue);
這個(gè)回調(diào)正好是enqueue。
欲知后事如何,且待我下回分解。
參考
uglifyjs-webpack-plugin v1.3.0
worker-farm v1.6.0
cacache v10.0.4