webpack 源码解析一之启航

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 源码解析一之启航

可以看出 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 源码解析一之启航

  • 执行过程

    1. 想想我们平时咋用 webpack 的

      webpack --config=webpack.build.js
      
    2. 这一步其实相当于

      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;
		}
	});

上一篇:深入理解Compiler对C/C++程序的编译流程


下一篇:IntelliJ IDEA 编译程序出现 非法字符 的 解决方法(转)