深入webpack4源码(二)—— 基本运行流程

好了。。终于可以开始看源码了,先了解下大体流程。

这里直接开始说webpack的源码,就不再细说webpack-cli了。
这俩的区别就是,webpack核心库,webpack-cli处理webpack的一系列命令行操作。感兴趣的可以看看这篇文章,其实直接看源码也很简单就是常规的那一套使用yargs这个库,然后从命令行接受命令行参数转化为webpack实际的参数。

进入正题,如果你使用过create-react-app或者vue-cli这样的工具,你都发现他们都并没有直接用命令行打包,而是自己写了一个脚本然后require('webpack')然后传入config进行打包,这样的好处是更灵活,更好控制,以下来自create-react-app中react-script的build:

const webpack = require('webpack');
function build(previousFileSizes) {
  let compiler = webpack(config);
  return new Promise((resolve, reject) => {
    compiler.run((err, stats) => {
        // ....
    })  
})
}

先看webpack的package.json:

"main": "lib/webpack.js",

说明我们require的就是lib/webpack.js

const webpack = (options, callback) => {
    const webpackOptionsValidationErrors = validateSchema(
        webpackOptionsSchema,
        options
    );
    if (webpackOptionsValidationErrors.length) {
        throw new WebpackOptionsValidationError(webpackOptionsValidationErrors);
    }
    let compiler;
    if (Array.isArray(options)) {
        compiler = new MultiCompiler(options.map(options => webpack(options)));
    } else if (typeof options === "object") {
        options = new WebpackOptionsDefaulter().process(options);

        compiler = new Compiler(options.context);
        compiler.options = options;
        new NodeEnvironmentPlugin().apply(compiler);
        if (options.plugins && Array.isArray(options.plugins)) {
            for (const plugin of options.plugins) {
                if (typeof plugin === "function") {
                    plugin.call(compiler, compiler);
                } else {
                    plugin.apply(compiler);
                }
            }
        }
        compiler.hooks.environment.call();
        compiler.hooks.afterEnvironment.call();
        compiler.options = new WebpackOptionsApply().process(options, compiler);
    } else {
        throw new Error("Invalid argument: options");
    }
    if (callback) {
        if (typeof callback !== "function") {
            throw new Error("Invalid argument: callback");
        }
        if (
            options.watch === true ||
            (Array.isArray(options) && options.some(o => o.watch))
        ) {
            const watchOptions = Array.isArray(options)
                ? options.map(o => o.watchOptions || {})
                : options.watchOptions || {};
            return compiler.watch(watchOptions, callback);
        }
        compiler.run(callback);
    }
    return compiler;
};

这段代码主要的工作就是初始化complier。


options为数组的情况,目前项目中还真没用过,以后有用到了再补充。

validateSchema

这部分主要就是校验options并且返回报错:

const validateObject = (schema, options) => {
    const validate = ajv.compile(schema);
    const valid = validate(options);
    return valid ? [] : filterErrors(validate.errors);
};

return validateObject(schema, options);

schema是一个json形式的描述文件,描述着各个字段是什么类型,以及对应的错误信息,感觉就像async-validator的rules一样,然后调用了一个叫ajv的库,将秒速文件转为了校验函数校验options。

WebpackOptionsDefaulter

其实webpack以及内置了很多默认的config,这个对象的作用就是合并默认的config和传入的config。但是在源码里这里不叫config,而是options。

class WebpackOptionsDefaulter extends OptionsDefaulter {
  constructor() {
      // this.set(xx, 'xx')
  }
}

set方法就是给默认字段设置对应的值。
更核心的方法都在OptionsDefaulter里,比如webpack.js中的options = new WebpackOptionsDefaulter().process(options);的process
OptionsDefaulter中,一开始就是定了两个方法,很有意思:

const getProperty = (obj, name) => {
    name = name.split(".");
    for (let i = 0; i < name.length - 1; i++) {
        obj = obj[name[i]];
        if (typeof obj !== "object" || !obj || Array.isArray(obj)) return;
    }
    return obj[name.pop()];
};

const setProperty = (obj, name, value) => {
    name = name.split(".");
    for (let i = 0; i < name.length - 1; i++) {
        if (typeof obj[name[i]] !== "object" && obj[name[i]] !== undefined) return;
        if (Array.isArray(obj[name[i]])) return;
        if (!obj[name[i]]) obj[name[i]] = {};
        obj = obj[name[i]];
    }
    obj[name.pop()] = value;
};
  • getProperty:其作用就是拿对象的某个字段的值,但是这里的name可以是'xxx.xxx.xxx'这样的格式,我们如果直接a.b.c.d如果b或者c就是undefined,那么直接就报错了,这里直接巧妙的避免了这样的情况,类似于Experimental的语法 optional-chaining,如let value = a?.b?.c?.d,如果有undefined最终就直接返回undefined了。
  • setProperty:也类似,比如a是{},我们不能直接设置a.b.c = xxx,这样如果b是undefined也会报错,这里就可以直接设置了。
class OptionsDefaulter {
    constructor() {
        this.defaults = {};
        this.config = {};
    }

    process(options) {
        options = Object.assign({}, options);
        for (let name in this.defaults) {
            switch (this.config[name]) {
                case undefined:
                    // ...
                    break;
                case "call":
                    // ...
                    break;
                case "make":
                    // ...
                    break;
                case "append": {
                    // ...
                }
                default:
            }
        }
        return options;
    }

    set(name, config, def) {
        if (def !== undefined) {
            this.defaults[name] = def;
            this.config[name] = config;
        } else {
            this.defaults[name] = config;
            delete this.config[name];
        }
    }
}

我们看到外层的WebpackOptionsDefaulter使用了很多set,其实就是这里的set,参数name代表option的具体名字,config代表具体的值。这里要说下的就是def:如果有def的时候,config就代表具体的合并方法的类型,而def就代表了option的具体值,具体的处理都在process中。

初始化complier

初始化complier就没有太多好说的,主要就是初始化了一系列的勾子,和一系列参数:

this.hooks = { ... };
/** @type {string=} */
this.name = undefined;
/** @type {Compilation=} */
this.parentCompilation = undefined;
/** @type {string} */
this.outputPath = "";

this.outputFileSystem = null;
this.inputFileSystem = null;

/** @type {string|null} */
this.recordsInputPath = null;
/** @type {string|null} */
this.recordsOutputPath = null;
this.records = {};
this.removedFiles = new Set();
/** @type {Map<string, number>} */
this.fileTimestamps = new Map();
/** @type {Map<string, number>} */
this.contextTimestamps = new Map();
/** @type {ResolverFactory} */
this.resolverFactory = new ResolverFactory();

这些参数的具体含义简单的看名字就知道,不懂的其实也要等到用到才明白。
可以讲下的是ResolverFactory,看见resolve就能想到路径解析。这个就是一个路径解析器的工厂,在需要的时候根据option返回一个路径解析器(这个类也有自己的勾子):

const { Tapable, HookMap, SyncHook, SyncWaterfallHook } = require("tapable");
const Factory = require("enhanced-resolve").ResolverFactory;

module.exports = class ResolverFactory extends Tapable {
    constructor() {
        super();
        this.hooks = {
            resolveOptions: new HookMap(
                () => new SyncWaterfallHook(["resolveOptions"])
            ),
            resolver: new HookMap(() => new SyncHook(["resolver", "resolveOptions"]))
        };
        this.cache1 = new WeakMap();
        this.cache2 = new Map();
    }

    get(type, resolveOptions) {
        const cachedResolver = this.cache1.get(resolveOptions);
        if (cachedResolver) return cachedResolver();
        const ident = `${type}|${JSON.stringify(resolveOptions)}`;
        const resolver = this.cache2.get(ident);
        if (resolver) return resolver;
        const newResolver = this._create(type, resolveOptions);
        this.cache2.set(ident, newResolver);
        return newResolver;
    }

    _create(type, resolveOptions) {
        // ....
    }
};

他也是主要调用的enhanced-resolve这个库来解析路径,并对解析器做了缓存。使用案例

应用NodeEnvironmentPlugin插件

该插件的主要作用是给complier初始化输入输出文件系统和监视文件系统。

class NodeEnvironmentPlugin {
    apply(compiler) {
        compiler.inputFileSystem = new CachedInputFileSystem(
            new NodeJsInputFileSystem(),
            60000
        );
        const inputFileSystem = compiler.inputFileSystem;
        compiler.outputFileSystem = new NodeOutputFileSystem();
        compiler.watchFileSystem = new NodeWatchFileSystem(
            compiler.inputFileSystem
        );
        compiler.hooks.beforeRun.tap("NodeEnvironmentPlugin", compiler => {
            if (compiler.inputFileSystem === inputFileSystem) inputFileSystem.purge();
        });
    }
}
module.exports = NodeEnvironmentPlugin;
  • 输出文件系统:非常简单,就是包装了一层node原生的fsapi。
  • 输入文件系统:复杂,目前还没有看见使用的地方。
  • 监视文件系统:复杂,目前还没有看见使用的地方,但是主要的作用就是监视文件改动及热更新等。

应用插件

这里很重要,我们webpack.config.js中的plugins就是在这个阶段挂载上的:

if (options.plugins && Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
        if (typeof plugin === "function") {
            plugin.call(compiler, compiler);
        } else {
            plugin.apply(compiler);
        }
    }
}

就像上一节讲过的,在这里只是webpack抽象后的挂载插件,而真正的在勾子上挂载执行函数都是具体在每个plugin里实现的。
之前我们说过我们必须为plugin实现apply方法,实际上这里也展示了挂载动态插件的方式,就是为plugin实现call方法。

WebpackOptionsApply

这个对象做的事情理解起来非常简单,那就是根据options为complier应用插件(但插件本身复杂度,哭了)。

process(options, compiler) {
  let ExternalsPlugin;
  compiler.outputPath = options.output.path;
  compiler.recordsInputPath = options.recordsInputPath ||     options.recordsPath;
  compiler.recordsOutputPath =
    options.recordsOutputPath || options.recordsPath;
  compiler.name = options.name;
  // TODO webpack 5 refactor this to     MultiCompiler.setDependencies() with a WeakMap
  // @ts-ignore TODO
  compiler.dependencies = options.dependencies;
  new xxxPlugin().apply(compiler);
  // ........
}

我们之前说过,webpack是插件机制,那到底是怎么样的插件机制呢?其实就是webpack声明了很多勾子,然后调用了很多勾子。 emmm,所以你看源码的时候你会发现,他就是调用了很多勾子,然后代码就完成打包了,但是你根本找不到处理代码的地方到底在哪里,实际上做处理的是挂载在勾子上的插件,那么为了完成webpack的所有功能,到底挂载了哪些插件?这个问题的答案就在这个对象里。

在这里插一段其他文章里,我很认同的一段话:

  • 联系松散。你可以发现:使用tapable钩子类似事件监听模式,虽然能有效解耦,但钩子的注册与调用几乎完全无关,很难将一个钩子的“创建 - 注册 - 调用”过程有效联系起来。
  • 模块交互基于钩子。webpack内部模块与插件在很多时候,是通过钩子机制来进行联系与调用的。但是,基于钩子的模式是松散的。例如你看到源码里一个模块提供了几个钩子,但你并不知道,在何时、何地该钩子会被调用,又在何时、何地钩子上被注册了哪些方法。这些以往都是需要我们通过在代码库中搜索关键词来解决。
  • 钩子数量众多。webpack内部的钩子非常多,数量达到了180+,类型也五花八门。除了官网列出的compiler与compilation中那些常用的钩子,还存在着众多其他可以使用的钩子。有些有用的钩子你可能无从知晓,例如我最近用到的localVars、requireExtensions等钩子。
  • 内置插件众多。webpack v4+ 本身内置了许多插件。即使非插件,webpack的模块自身也经常使用tapable钩子来交互。甚至可以认为,webpack项目中的各个模块都是“插件化”的。这也使得几乎每个模块都会和各种钩子“打交道”。

我的建议是这个对象可以暂时不管,用到再查。

到现在只讲了complier的初始化,实际上complier.run()后你会发现更多的勾子调用,而且compiler里还会引用其他模块,其他模块还有自己的勾子,所以我们在阅读源码的时候,建议可以看见了某个勾子调用就全局搜下这个勾子的名字加上调用方法,然后再看挂载的执行函数到底做了什么处理。例如:这个勾子赋值给了complier.hooks.run,并且async的勾子,那么全局就搜索hooks.run.taphooks.run.tapAsynchooks.run.tapPromise,然后再看到底做了什么。

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

推荐阅读更多精彩内容