[FE] webpack群侠传(六):代码生成

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.compileCompiler.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.makecallback传递给了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._addModuleChainCompilation.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)实现
createChunkAssetscompilation实例的一个方法,位于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中,filesource分别是什么。
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/_babel-loader@8.0.4@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/_uglifyjs-webpack-plugin@1.3.0@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返回。

其中,runneruglifyjs-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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容