边学边想webpack5源码一起读(3)

1,049 阅读6分钟

引言

本文章基于webpack5源码进行解读,编写本文仅为方便作者学习以及记忆,如果有什么说的不对或者瞎猜的不对的地方,请大家指出,请勿乱喷,大家都是自己人感谢大家~

接上回

首先我们基于上回的一些内容,我们继续开始画我们图~ webpack流程图成长版本.png 我们找到webpack-cli构造函数中的这一段代码this.webpack = require(process.env.WEBPACK_PACKAGE || 'webpack');,就知道我们的this.webpack实际上就拉取自node_modules中的webpack,那么我们首先关注到node_modules/webpack/package.jsonmain字段

image.png

这边可以定位到我们的入口文件就是node_modules/webpack/lib/index.js,接着我们打开文档文件进行查看,文件主要是做了大量的参数入口声明,这边我们简化了一下文件,讲一些我们的关键路径

webpack/lib/index.js

//node_modules/webpack/lib/index.js

const util = require("util");
const memoize = require("./util/memoize");

// 实际上通过柯里化的方式去做了一个懒加载,等函数真的被调用才去进行拉取
const lazyFunction = factory => {
        // memoize 内部实现就比较简单,用闭包的方式针对factory做了个封装,缓存了factory调用的结果
	const fac = memoize(factory);
	const f = /** @type {any} */ (
		(...args) => {
			return fac()(...args);
		}
	);
	return /** @type {T} */ (f);
};

/**
 * @template A
 * @template B
 * @param {A} obj input a
 * @param {B} exports input b
 * @returns {A & B} merged
 */
 // 递归调用初始化对象属性的叙述属性,保证不可变不可重写
const mergeExports = (obj, exports) => {
	const descriptors = Object.getOwnPropertyDescriptors(exports);
	for (const name of Object.keys(descriptors)) {
		const descriptor = descriptors[name];
		if (descriptor.get) {
			const fn = descriptor.get;
			Object.defineProperty(obj, name, {
				configurable: false,
				enumerable: true,
				get: memoize(fn)
			});
		} else if (typeof descriptor.value === "object") {
			Object.defineProperty(obj, name, {
				configurable: false,
				enumerable: true,
				writable: false,
				value: mergeExports({}, descriptor.value)
			});
		} else {
			throw new Error(
				"Exposed values must be either a getter or an nested object"
			);
		}
	}
	return /** @type {A & B} */ (Object.freeze(obj));
};
// 包装webpack的拉取方法
const fn = lazyFunction(() => require("./webpack"));
// 合并对象并且使合并出来对外的对象成为不可变对象
module.exports = mergeExports(fn, {
    get webpack() {
            return require("./webpack");
    },
    ...
    get cli() {
            return require("./cli");
    },
    ...
    // 这里省略了很多很多的声明
})

实际看完这段代码明白当前入口文件就做了三件事情:

  1. 暴漏webpack包中大量属性方法的入口
  2. 缓存/懒加载导出对象
  3. 修改暴露属性的叙述属性,让其不可变

webpack/lib/webpack.js

另外关键路径为node_modules/webpack/lib/webpack.js,因为我们在webpack-cli中用用到的主要是暴漏构建函数,构建函数很明显是从上方代码的const fn = lazyFunction(() => require("./webpack"))获取的那么我们继续往下走(文件较大,为了保持关注点,我还是拆分了一下,我们还是按照顺序来看)

// node_modules/webpack/lib/webpack.js

const webpack = ((options, callback) => {
    const create = () => {
        ... // 这里先省略
    }
    // 这里callback并不是我们原生设定的,而是来自于node_modules/webpack-cli/lib/webpack-cli.js 的 1847行,感兴趣可以看一下,看过来主要是一些错误处理以及日志输入,我们继续
    if (callback) {
        try {
            const { compiler, watch, watchOptions } = create();
            if (watch) {
                compiler.watch(watchOptions, callback);
            } else {
                compiler.run((err, stats) => {
                    compiler.close(err2 => {
                        callback(err || err2, stats);
                    });
                });
            }
            // 无论是否输出watch都会返回一个compiler,虽然我们暂时不知道compiler是什么不过我们可以慢慢了解一下~
            return compiler;
        } catch (err) {
            process.nextTick(() => callback(err));
            return null;
        }
    } else {
        const { compiler, watch } = create();
        if (watch) {
            util.deprecate(
                    () => {},
                    "A 'callback' argument need to be provided to the 'webpack(options, callback)' function when the 'watch' option is set. There is no way to handle the 'watch' option without a callback.",
                    "DEP_WEBPACK_WATCH_WITHOUT_CALLBACK"
            )();
        }
        // 这里跟没有call的场景是一样,同样都是返回一个compiler
        return compiler;
    }
});

那么我们看完这一小段代码,实际上知道的是我们webpack(...)的这个函数,最主要的作用就是输出一个compiler,然后根据不同场景回调用compilerrun(...)watch(...)这两个不同的方法,并且挂载我们传入的callback(...), 那么在生产compiler的过程中,最重要的不外乎我们省略的create(...)函数中的内容,那么下面我们来看下create(...)函数做什么!

// node_modules/webpack/lib/webpack.js webpack(...)=>create(...)
const create = () => {
    /* 
    * 这里做了两次校验,一个使用的是检验包,一个是用的"node_modules/schema-utils/dist/index.js",当然这里作者没有细看详细的一些差距或者内容
    * 基本为作者猜测,检查方式看起来都是通过JSON Schema规则进行的,有可能第一层检查速度较快,第二层检查更加全面~
    */ 
    if (!webpackOptionsSchemaCheck(options)) {
        getValidateSchema()(webpackOptionsSchema, options);
    }
    let compiler;
    let watch = false;
    let watchOptions;
    /* 
    *  这里判断options是否为数组,根据官方文档的说明,webpack会为每一个配置去生产一个compiler
    *  也就是针对一个生产过程,去制造一个流水线,这个很好理解,毕竟不同的产品所需要的流水线是不一样的
    *  那么后续我们在理解上面,先暂时的将compiler理解为一个“生产线”
    */ 
    if (Array.isArray(options)) {
        compiler = createMultiCompiler(
            options,
            options
        );
        watch = options.some(options => options.watch);
        watchOptions = options.map(options => options.watchOptions || {});
    } else {
        /* 
        *  那么根据我们的初始化配置我们将会进入到这个流程
        *  根据代码我们知道关键路径一定是createCompiler这个听起来相当工厂的方法
        *  我们下一段就直接放这一块相关的代码吧
        */ 
        const webpackOptions = options;
        compiler = createCompiler(webpackOptions);
        watch = webpackOptions.watch;
        watchOptions = webpackOptions.watchOptions || {};
    }
    return { compiler, watch, watchOptions };
};
// node_modules/webpack/lib/webpack.js createCompiler(...)
const createCompiler = rawOptions => {
    /* 
    *  首先进入扁平化方法,大部分options类的三方公共库,都会存在的normalize, 那么我们主要关注前后差异即可
    *  那么我们这里的rawOptions是跟我们在最外层的webpack.config.js一致的吗?
    *  其实不是在这里我们options已经被webpack-cli做了一些初始化操作,比如plugins 以及 stats,我给大家在下面贴了本人debug的环境数据,大家对照的看就行
    */
    const options = getNormalizedWebpackOptions(rawOptions);
    /*
    * 大家自行DEBUG以后会发现options上多了很多一些webpack官方的配置项,那这里就不废话继续向下
    * applyWebpackOptionsBaseDefaults(...) 中主要基于process做了context的值绑定
    * 以及看起来像是日志参数的初始化操作 applyInfrastructureLoggingDefaults(...)
    */ 
    applyWebpackOptionsBaseDefaults(options);
    /*
    * Compiler的构造函数,options.context是刚才在 applyWebpackOptionsBaseDefaults中赋值的() => process.cwd()
    * Compiler的构造函数中主要做了大量的参数初始化操作,这里会放在下一个代码段
    */ 
    const compiler = new Compiler(options.context);
    // 赋值options(废话一句)
    compiler.options = options;
    /*
    * 在这里传入的infrastructureLogging参数就是来自于applyInfrastructureLoggingDefaults(...) 所初始化的部分日志参数
    * NodeEnvironmentPlugin 主要做三件事情:
    * 1. 文件读取相关的方法挂载,缓存,操作
    * 2. 初始化文件监听(Watchpack)
    * 3. 挂载插件hook beforeRun
    */ 
    new NodeEnvironmentPlugin({
        infrastructureLogging: options.infrastructureLogging
    }).apply(compiler);
    // 执行之前挂载的CLI的插件方法,主要是一些控制台相关的内容node_modules/webpack-cli/lib/plugins/CLIPlugin.js 感兴趣的同学可以去看看
    if (Array.isArray(options.plugins)) {
        for (const plugin of options.plugins) {
            if (typeof plugin === "function") {
                plugin.call(compiler, compiler);
            } else {
                plugin.apply(compiler);
            }
        }
    }
    // 扁平化格式后的基础赋值
    applyWebpackOptionsDefaults(options);
    // 调用所有“environment”的hook
    compiler.hooks.environment.call();
    // 调用所有“afterEnvironment”的hook
    compiler.hooks.afterEnvironment.call();
    // WebpackOptionsApply 中了大量的根据options初始化插件的操作,包括相关钩子的触发,各类关键节点的钩子初始化如果找不到的话 都可以看看这里
    new WebpackOptionsApply().process(options, compiler);
    // 调用所有“initialize”的hook
    compiler.hooks.initialize.call();
    // 返回compiler
    return compiler;
};

rawOptions数据内容~ rawOptions的初始化数据

好的,跟到这里我们发现,当前方法并没有触发compiler进入一个编译环节,主要还是做了全量的一个初始化方案。然后跟着调试我们会发现,关键路径实际上是在于compiler.run(...),那么我们下一章节,就主要来看看 从compiler.run(...)的执行过程中究竟发生了什么~

下一章节地址: 暂未发布