美文网首页Front End
[FE] webpack群侠传(七):代码压缩和缓存

[FE] webpack群侠传(七):代码压缩和缓存

作者: 何幻 | 来源:发表于2018-10-24 18:48 被阅读2次

    1. 回顾

    上文我们介绍了webpack在代码生成阶段做的事情。

    我们知道,webpack调用了compiler.hooks.make加载资源,
    它会先加载loader,然后用loader加载源文件,
    对于js而言,babel-loader会返回转换后的es5代码,而不是AST

    加载完资源之后,webpack就会调用compilation.seal来生成代码,
    compilation.seal中调用了一大堆hooks,
    其中最重要的两件事情是,createChunkAssetsoptimizeChunkAssets

    (1)createChunkAssets会填充compilation.assets对象,
    compilation.assets中保存了待生成的目标文件名,和文件内容。

    (2)optimizeChunkAssets会调用uglifyjs-webpack-plugin进行代码压缩,
    而uglifyjs-webpack-plugin则引用了uglify-esworker-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,并设置parallelfalse

    ...
    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(); 会调用stepstep会调用callback

    step中的callback,就是runTaskscallback
    这样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
    }
    

    它们分别存储了缓存的keyvaluevalue就是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

    相关文章

      网友评论

        本文标题:[FE] webpack群侠传(七):代码压缩和缓存

        本文链接:https://www.haomeiwen.com/subject/npbxtqtx.html