webpack 源码解析
首先我们上 github 上面 clone 一份源码,我这里 clone 的是 4.30 版本的 webpack 源码
git clone https://github.com/webpack/webpack.git
起步
- 首先,看一个源码的初始步骤就是打开 package.json 找到它的入口文件
"main": "lib/webpack.js",
- 确定了 webpack.js 文件,我们就可以开始代码阅读了.
webpack.js
- 一开始我们可以分析一下它的结构
可以看出
webpack.js
主要用于导出一些默认的 plugin 与工具函数.其中我们发现exports = module.exports = webpack
默认导出的就是webpack
函数,这个函数也就是我们平时执行时所使用的.我们来看一下它的源码.
/**
* @param {WebpackOptions} options options object
* @param {function(Error=, Stats=): void=} callback callback
* @returns {Compiler | MultiCompiler} the compiler object
*/
/**
* 从参数列表中,我们可以看出, webpack 主要携带两个参数,即一个是 webpack 的配置
* 另一个则是 webpack 执行结束之后的回调, 因为是 node 程序,所以 webpack 参照了一些 node 函数的使用方法
* 第一个参数是 err 信息, 这是因为 node 主要是异步的,异常不能正常捕获,所以这么设计
*/
const webpack = (options, callback) => {
// 根据设定好的 validate 来对 options 进行校验,如果有异常,那么就终止程序并抛出异常
const webpackOptionsValidationErrors = validateSchema(
webpackOptionsSchema,
options
)
if (webpackOptionsValidationErrors.length) {
throw new WebpackOptionsValidationError(webpackOptionsValidationErrors)
}
// 初始化编译器,其实 webpack 打包也就是一个编译器,把我们的代码,转换成打包后的目标代码
// 接受两种类型的参数,一个是 Array<Object:options> 型 , 另一种是 Object:options 型
// 如果不是指定的参数类型,那么就会招出参数异常
let compiler
if (Array.isArray(options)) {
// 如果 options 是 Array<Object:options> ,那么就在可以在单个 compiler 中执行多个配置
compiler = new MultiCompiler(options.map(options => webpack(options)))
} else if (typeof options === 'object') {
// 如果 options 是 Object:options 的话,也就是通常使用的模式
// Array<Object:options> 其实是对每个配置来执行本步骤
//将用户自定义的配置信息与默认信息进行 minix
options = new WebpackOptionsDefaulter().process(options)
// 根据配置信息,初始化webpack编辑器对象,并把配置信息配置给它
compiler = new Compiler(options.context)
compiler.options = options
// 注册 NodeEnvironmentPlugin node 环境插件,并用其为 compiler 添加一些环境信息
new NodeEnvironmentPlugin().apply(compiler)
// 把用户注册的插件挂载到 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)
}
}
}
// 触发 environment 和 afterEnvironment 上注册的事件
compiler.hooks.environment.call()
compiler.hooks.afterEnvironment.call()
// 注册 webpack 内置的一些插件
compiler.options = new WebpackOptionsApply().process(options, compiler)
} else {
throw new Error('Invalid argument: options')
}
// 如果传入了回调函数,那么先检查一下传入的参数类型,类型不正确就退出程序并抛出异常
// 然后再检查配置项中 watch 是否开始,如果开启那么以 watch() 方式执行回调,否则直接执行回调
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
}
其 流程图
如下:
-
执行过程
-
想想我们平时咋用 webpack 的
webpack --config=webpack.build.js
-
这一步其实相当于
const Webpack = require('./node_modules/webpack'); const config = require('./own-config.js'); const compiler = Webpack(config); compiler.run();
最后一步有编译器的执行过程,所以这一波高阶操作是可向下继承执行的, 由
run()
来调用编译器,并进行打包操作.
-
NEXT
接下来我们要就要根据主模块调用的步骤来一个一个分析模块啦~!
WebpackOptionsValidattionError -> validateSchema.js
参考资料
核心第三方库 ajv
核心第三方库 ajv-keywords
json schemas 校验
校验输入
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Gajus Kuizinas @gajus
*/
'use strict'
// 初始化 ajv 对象并配置信息
const Ajv = require('ajv')
const ajv = new Ajv({
errorDataPath: 'configuration',
allErrors: true,
verbose: true
})
// 为 ajv 的校验添加实例校验逻辑,否则的话,引用类型的值就只能校验成 Object
require('ajv-keywords')(ajv, ['instanceof'])
// 为 ajv 的校验增加了绝对路径的校验规则,相关代码我们稍后叙述
require('../schemas/ajv.absolutePath')(ajv)
// 校验传入的 json 规则序列与被校验的属性对象
// 还根据传入的 options 类型来进行校验
// 如果传入的 options 是 Array<Object> 类型的,那么遍历调用校验函数校验
// 如果传入的 options 是 Object 类型的,那么直接使用校验函数校验
// 校验结果后会整合错误集合,返回一个错误信息数组
const validateSchema = (schema, options) => {
if (Array.isArray(options)) {
const errors = options.map(options => validateObject(schema, options))
errors.forEach((list, idx) => {
const applyPrefix = err => {
err.dataPath = `[${idx}]${err.dataPath}`
if (err.children) {
err.children.forEach(applyPrefix)
}
}
list.forEach(applyPrefix)
})
return errors.reduce((arr, items) => {
return arr.concat(items)
}, [])
} else {
return validateObject(schema, options)
}
}
// 根据 schema 来生成对应的 validate 来进行校验,最后返回一个错误数组,如果没有错误,返回一个空数组
const validateObject = (schema, options) => {
const validate = ajv.compile(schema)
const valid = validate(options)
return valid ? [] : filterErrors(validate.errors)
}
// 过滤错误信息,防止某些内容重复报错用的,其实就是一个错误信息去重
const filterErrors = errors => {
let newErrors = []
for (const err of errors) {
const dataPath = err.dataPath
let children = []
newErrors = newErrors.filter(oldError => {
if (oldError.dataPath.includes(dataPath)) {
if (oldError.children) {
children = children.concat(oldError.children.slice(0))
}
oldError.children = undefined
children.push(oldError)
return false
}
return true
})
if (children.length) {
err.children = children
}
newErrors.push(err)
}
return newErrors
}
module.exports = validateSchema
自定义的 ajv 校验器(划重点,不考) -> ajv.sbsolutePath.js
"use strict";
// 组装错误信息,返回一个包含详细错误信息的对象
const errorMessage = (schema, data, message) => ({
keyword: "absolutePath",
params: { absolutePath: data },
message: message,
parentSchema: schema
});
// 根据情况(是否需要绝对路径),来生成一个错误信息
const getErrorFor = (shouldBeAbsolute, data, schema) => {
const message = shouldBeAbsolute
? `The provided value ${JSON.stringify(data)} is not an absolute path!`
: `A relative path is expected. However, the provided value ${JSON.stringify(
data
)} is an absolute path!`;
return errorMessage(schema, data, message);
};
// 就是为 ajv 添加一个 keyword absolutePath, 这样它就会校验 type 以外的 keyword 定义了
module.exports = ajv =>
ajv.addKeyword("absolutePath", {
errors: true,
type: "string",
// 这个其实就是校验时所执行的函数,传入值与校验规则
// 主要功能是根据正则判断传入的值是不是绝对路径,再根据规则返回错误结果,如果是正确的话,返回空数组
compile(expected, schema) {
function callback(data) {
let passes = true;
const isExclamationMarkPresent = data.includes("!");
const isCorrectAbsoluteOrRelativePath =
expected === /^(?:[A-Za-z]:\\|\/)/.test(data);
if (isExclamationMarkPresent) {
callback.errors = [
errorMessage(
schema,
data,
`The provided value ${JSON.stringify(
data
)} contains exclamation mark (!) which is not allowed because it's reserved for loader syntax.`
)
];
passes = false;
}
if (!isCorrectAbsoluteOrRelativePath) {
callback.errors = [getErrorFor(expected, data, schema)];
passes = false;
}
return passes;
}
callback.errors = [];
return callback;
}
});