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