【webpack源码2】- webpack.js

webpack | 2020-06-09 16:33:41 177次 1次

一、webpack.js执行

webpack-cli 中引入了 webpack,这个方法由 webpack 包中的webpack.js 暴露出,接受一个配置项和回调函数。这里面主要做的就是实例化一个 compiler,其他的就是一些错误检查、海量默认插件的导出、用户配置的插件调用、对文件系统做了一些封装(输入,输出,缓存,监听等等)并挂载在 compiler 对象下。

const webpack = (options, callback) => {
    //错误校验
    const webpackOptionsValidationErrors = validateSchema(
        webpackOptionsSchema,
        options
    );
    if (webpackOptionsValidationErrors.length) {
        throw new WebpackOptionsValidationError(webpackOptionsValidationErrors);
    }
    let compiler;
    if (Array.isArray(options)) {
        //如果传入的配置是数组,则创建多个compiler(compiler包含compiler与watching两个对象),
        //这里先不做分析
        compiler = new MultiCompiler(
            Array.from(options).map(options => webpack(options))
        );
    } else if (typeof options === "object") {
        //默认参数
        options = new WebpackOptionsDefaulter().process(options);

        //实例化 Compiler 对象 options.context代表当前运行webpack路径
        compiler = new Compiler(options.context);
        compiler.options = options;
        //基于node api封装文件操作
        new NodeEnvironmentPlugin({
            infrastructureLogging: options.infrastructureLogging
        }).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();
        //这个模块主要是根据options选项的配置,设置compile的相应的插件,属性,
        compiler.options = new WebpackOptionsApply().process(options, compiler);
    } else {
        throw new Error("Invalid argument: options");
    }
    if (callback) {
        ... //没有用到
        compiler.run(callback);
    }
    return compiler;
};

下面介绍这个文件中用到的其他几个大的模块方法。


二、validateSchema错误检测

const webpackOptionsValidationErrors = validateSchema(
    webpackOptionsSchema,
    options
);

第一个参数是对 option 配置文件做一个静态检查,如果数据格式不符合规定则给出提示,第二个参数就是用户传入的配置项。

const validateSchema = (schema, options) => {
    if (Array.isArray(options)) {
        ...
    } else {
        return validateObject(schema, options);
    }
};

这里会发现 validateSchema 被调用了两次,既然它是对配置文件进项校验的,那么我们应该联想到在 cli 那个包下主要处理的命令行一些参数配置,所以在 convert-args 文件下的 processConfiguredOptions 方法中也需要调用校验一次。

validateObject 方法中通过 ajv.compile 传入配置项,进行校验,校验通过给出一个空数组,反之给出错误信息:

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

然后回到 webpack.js 中判断如果有错误则抛出 WebpackOptionsValidationError 错误。

三、WebpackOptionsDefaulter

配置的参数校验无误之后,预置默认一些配置 WebpackOptionsDefaulter 继承自 OptionsDefaulter,所以 webpack 对用户来说是可以零配置的,OptionsDefaulter 中含有 set 方法记录一些预置配置操作,process 进行真正处理。

class WebpackOptionsDefaulter extends OptionsDefaulter {
    constructor(){
        super();
        this.set("entry", "./src");
        ...
    }
}

这个方法中都是 set 操作,但是会有默认callmakeappend 四种操作类型,看下 OptinsDefaulter

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:
                    if (getProperty(options, name) === undefined) {
                      setProperty(options, name, this.defaults[name]);
                    }
                    break;
                case "call":
                    setProperty(
                      options,
                      name,
                      this.defaults[name].call(this, getProperty(options, name), options)
                    );
                    break;
                ...
            }
        }
        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];
        }
    }
}

module.exports = OptionsDefaulter;

通过 new WebpackOptionsDefaulter().process(options);的调用,返回一个包装后的 options,这里面最频繁使用的两个函数就是 getProperty 和  setProperty,因为 webpack 的配置有时候需要多层对象,那么这两个方法的处理就有点意思了,比如我们之前判断一个对象中的某个属性是否存在或是否有值,会使用 obj.name 看看它的值判断是否存在,这样的话在 webpack 的基类中处理起来就相当不方便了,所以,它是通过字符串拼接起来然后截取为数组再判断,这样省去写一个麻烦的代码:

const getProperty = (obj, path) => {
    let name = path.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()];
};

console.log(getProperty({
    a: {
        b: {
            c: [1, 2, 3]
        }
    }
}, 'a.b.c'))
const setProperty = (obj, path, value) => {
    let name = path.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;
};
let obj = {}
setProperty(obj, 'a.b.c', 111)
console.log(obj)


四、NodeEnvironmentPlugin

该类主要对文件系统做了一些封装,包括输入,输出,缓存,监听等等,这些扩展后的方法全部挂载在 compiler 对象下。接受一个 infrastructureLogging(日志输出)参数,这个可以自定义配置或使用默认的。

class NodeEnvironmentPlugin {
    ...
    apply(compiler) {
        //命令面板中日志记录
        compiler.infrastructureLogger = createConsoleLogger(
            Object.assign(
                {
                    level: "info",
                    debug: false,
                    console: nodeConsole
                },
                this.options.infrastructureLogging
            )
        );
        // 可以缓存输入的文件系统
        compiler.inputFileSystem = new CachedInputFileSystem(
            new NodeJsInputFileSystem(),
            60000
        );
        const inputFileSystem = compiler.inputFileSystem;
        // 输出文件系统
        compiler.outputFileSystem = new NodeOutputFileSystem();
        // 监视文件系统
        compiler.watchFileSystem = new NodeWatchFileSystem(
            compiler.inputFileSystem
        );
        // 添加事件流before-run
        compiler.hooks.beforeRun.tap("NodeEnvironmentPlugin", compiler => {
            if (compiler.inputFileSystem === inputFileSystem) inputFileSystem.purge();
        });
    }
}
module.exports = NodeEnvironmentPlugin;

这些基本上都是在 compiler(这个方法留到 tapable 之后再分析)对象上挂载 node fs 文件系统,比如输出文件系统:

const fs = require("fs");
const path = require("path");
const mkdirp = require("mkdirp");
class NodeOutputFileSystem {
    constructor() {
        this.mkdirp = mkdirp;
        this.mkdir = fs.mkdir.bind(fs);
        this.rmdir = fs.rmdir.bind(fs);
        this.unlink = fs.unlink.bind(fs);
        this.writeFile = fs.writeFile.bind(fs);
        this.join = path.join.bind(path);
    }
}
module.exports = NodeOutputFileSystem;


五、WebpackOptionsApply

这个模块主要是根据 options 配置,设置 compile 的相应的插件、属性,里面写了大量的 apply(compiler); 使得模块的 this 指向 compiler,没有对 options 做任何处理。其中根据 target 对环境部署配置进行相应操作,默认为 web 环境部署。

这个里面加载了大量插件并触发事件流:

① 根据 options.target 加载对应的插件,如果配置文件没有配置该参数,则在 WebpackOptionsDefaulter 模块会被自动初始化为 web

② 处理 options.output.library、options.output.externals 参数

③ 处理 options.devtool 参数

④ 加载 EntryOptionPlugin 插件并触发 entry-option 的事件流

⑤ 加载大量插件

⑥ 处理 options.performance 参数

⑦ 加载 TemplatePathPlugin、RecordIdPlugin、WarnCaseSensitiveModulesPlugin 插件

⑧ 触发 after-plugins 事件流

⑨ 设置 compiler.resolvers 的值

⑩ 触发 after-resolvers 事件流


六、导出配置

最后往 webpack 上挂载一些方法:

webpack.WebpackOptionsDefaulter = WebpackOptionsDefaulter;
webpack.WebpackOptionsApply = WebpackOptionsApply;
webpack.Compiler = Compiler;
webpack.MultiCompiler = MultiCompiler;
webpack.NodeEnvironmentPlugin = NodeEnvironmentPlugin;
// @ts-ignore Global @this directive is not supported
webpack.validate = validateSchema.bind(this, webpackOptionsSchema);
webpack.validateSchema = validateSchema;
webpack.WebpackOptionsValidationError = WebpackOptionsValidationError;

然后给 export 上挂载海量插件:

const exportPlugins = (obj, mappings) => {
    for (const name of Object.keys(mappings)) {
        Object.defineProperty(obj, name, {
            configurable: false,  //不可删除
            enumerable: true,
            get: mappings[name]
        });
    }
};

exportPlugins(exports, {
    AutomaticPrefetchPlugin: () => require("./AutomaticPrefetchPlugin"),
    ...
}

这样以后用的时候,可以直接从 webpack 这个文件引入了,并且是按需加载的。

对 webpack 这个文件有个大致了解后,接下来在进入 Compiler 模块之前先看下 Tapable 的使用以及分析。

1人赞

分享到: