[FE] webpack群俠傳(七):代碼壓縮和緩存

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,
其中最重要的兩件事情是,createChunkAssetsoptimizeChunkAssets。

(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-esworker-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è)置parallelfalse,

...
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,就是runTaskscallback。
這樣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ǔ)了緩存的keyvaluevalue就是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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容