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
}

流程圖如下:
image

  • 執行過程

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章