webpack入口执行流及调试

1,336 阅读4分钟

以下内容为个人的理解,如有错漏希望大家指正

调试配置

  1. clone webpack github项目
  2. 在项目根目录建立debug目录
  3. 创建config.js简单配置打包项
const path = require('path')
const MinaWebpackPlugin = require('./plugins/MinaWebpackPlugin')
module.exports = {
    context: __dirname,
    mode: 'development',
    devtool: 'source-map',
    entry: './src/index.js',
    output: {
        path: path.join(__dirname, './dist'),
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                use: ['babel-loader'],
                exclude: /node_modules/,
            }
        ]
		},
		plugins: [
			new MinaWebpackPlugin()
		]
}
  1. 创建调试入口start.js文件,使用 require webpack 的方式
const webpack = require('../lib/index.js')  // 直接使用源码中的webpack函数
const config = require('./webpack.config')
const compiler = webpack(config)
compiler.run((err, stats)=>{
    if(err){
        console.error(err)
    }else{
        console.log(stats)
    }
})
  1. 新建vscode 调式配置 launch.json,将program设为start.js地址
{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "启动程序",
            "skipFiles": [
                "<node_internals>/**"
            ],
            "program": "${workspaceFolder}\\debug\\start.js" //只需更改此处
        }
    ]
}

webpack入口文件

通过package.json 可知入口文件为: webpack/lib/index.js

// webpack/lib/index.js
/**
 * @template {Function} T
 * @param {function(): T} factory factory function
 * @returns {T} function
 */
const lazyFunction = factory => {
	const fac = memoize(factory);
	const f = /** @type {any} */ ((...args) => {
		return fac()(...args);
	});
	return /** @type {T} */ (f);
};

//mergeExports 将第exports对象所有属性定义在函数fn下,并修改默认的访问器属性及数据属性
/**
 * @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 函数体;
//lazyFunction 通过 memoize 返回了一个闭包函数;
//此闭包函数中有缓存标识,缓存 webpack 函数是否运行过。
//也就是说首次运行webpack() 会完整走一遍index.js代码,如果有缓存直接读缓存
const fn = lazyFunction(() => require("./webpack"));

//mergeExports 将第二个参数对象的属性定义在了 webpack 函数下,并修改这些数据的默认访问器属性、数据属性行为
//因此 start.js 这段脚本const compiler = webpack(config),运行的还是 require("./webpack")) 导出的方法
module.exports = mergeExports(fn, {
	get webpack() {
		return require("./webpack");
	},
	get validate() {
		const validateSchema = require("./validateSchema");
		const webpackOptionsSchema = require("../schemas/WebpackOptions.json");
		return options => validateSchema(webpackOptionsSchema, options);
	},
	get validateSchema() {
		const validateSchema = require("./validateSchema");
		return validateSchema;
	},
	get version() {
		return /** @type {string} */ (require("../package.json").version);
	},

	get cli() {
		return require("./cli");
	},
	get AutomaticPrefetchPlugin() {
		return require("./AutomaticPrefetchPlugin");
	},
	get BannerPlugin() {
		return require("./BannerPlugin");
	},
	get Cache() {
		return require("./Cache");
	},
	get Chunk() {
		return require("./Chunk");
	},
	get ChunkGraph() {
		return require("./ChunkGraph");
	},
	get CleanPlugin() {
		return require("./CleanPlugin");
	},
	get Compilation() {
		return require("./Compilation");
	},
	get Compiler() {
		return require("./Compiler");
	},
    	...
});

webpack 函数主体

// webpack/lib/webpack.js

/**
 * @param {WebpackOptions[]} childOptions options array
 * @returns {MultiCompiler} a multi-compiler
 */
const createMultiCompiler = childOptions => {
	const compilers = childOptions.map(options => createCompiler(options));
	const compiler = new MultiCompiler(compilers);
	for (const childCompiler of compilers) {
		if (childCompiler.options.dependencies) {
			compiler.setDependencies(
				childCompiler,
				childCompiler.options.dependencies
			);
		}
	}
	return compiler;
};

/**
 * @param {WebpackOptions} rawOptions options object
 * @returns {Compiler} a compiler
 */
const createCompiler = rawOptions => {
	//使用用户配置项赋值所有配置项(标准化、正规化配置),webpack所有的配置都在这找到
	const options = getNormalizedWebpackOptions(rawOptions); 
	//如果配置中的 context 没有值,则给一个默认值:process.cwd()
   	applyWebpackOptionsBaseDefaults(options);

	//Compiler类(./lib/Compiler.js):webpack的主要引擎,在compiler对象记录了完整的webpack环境信息,
	//在webpack从启动到结束,compiler只会生成一次。
	//可以在compiler对象上读取到webpack config信息,outputPath等;
	const compiler = new Compiler(options.context);
	compiler.options = options;
	new NodeEnvironmentPlugin({
		infrastructureLogging: options.infrastructureLogging
	}).apply(compiler);

	//获取并调用用户配置中的插件▲▲也就是说用的插件优先级高于内部插件的运行▲▲
	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);
	compiler.hooks.environment.call();
	compiler.hooks.afterEnvironment.call();
 	//运行了各种内置插件
	new WebpackOptionsApply().process(options, compiler); 
	compiler.hooks.initialize.call();
	return compiler;
};

const webpack = /** @type {WebpackFunctionSingle & WebpackFunctionMulti} */ ((
	options,
	callback
) => {
	const create = () => {
		//validateSchema 实际为一个schema-utils包的validate方法,以验证用户webpack配置的参数类型是否通过校验
		//例如:我将 config.mode 故意配错成布尔值 true,打包时便会报错
		//configuration.mode should be one of these:
   		//"development" | "production" | "none"
		validateSchema(webpackOptionsSchema, options);
		/** @type {MultiCompiler|Compiler} */
		let compiler;
		let watch = false;
		/** @type {WatchOptions|WatchOptions[]} */
		let watchOptions;

		//wepback 配置允许为一个数组,每一个元素为一个配置,createMultiCompiler 会执行一遍每一个配置
		//watch、watchOptions 对应webpack的配置项(监控文件修改,自动build) https://webpack.docschina.org/configuration/watch/
		if (Array.isArray(options)) {
			/** @type {MultiCompiler} */
			compiler = createMultiCompiler(options);
			watch = options.some(options => options.watch);
			watchOptions = options.map(options => options.watchOptions || {});
		} else {
			/** @type {Compiler} */
			compiler = createCompiler(options);
			watch = options.watch;
			watchOptions = options.watchOptions || {};
		}
		return { compiler, watch, watchOptions };
	};


	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);
					});
				});
			}
			return compiler;
		} catch (err) {
			process.nextTick(() => callback(err));
			return null;
		}
	} else {
		const { compiler, watch } = create();
		if (watch) { //callback 设置项必须与 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"
			)();
		}
		return compiler;
	}
});

module.exports = webpack;

以上为一个简单的webpack入口流程的过程

自定义插件

  1. 自定义插件本质上是绑定自定义事件到webpack对应的钩子中,webpack运行到对应的钩子时会执行

  2. 如果webpack某个钩子是SyncBailHook类型,则可以在对应订阅事件中加入return true(只要不是undefined),来告诉webpack后续的订阅事件不用运行。
    SyncBailHook.js 实现逻辑
    tapable/lib/HookCodeFactory.js 调用1
    tapable/lib/HookCodeFactory.js 调用2

  3. 自定义插件还可以利用webpack内置插件实现功能避免

参考文章

  1. Webpack源码解读:理清编译主流程
  2. Webpack tapable 使用研究