好了。。終于可以開始看源碼了,先了解下大體流程。
這里直接開始說webpack的源碼,就不再細說webpack-cli了。
這倆的區別就是,webpack核心庫,webpack-cli處理webpack的一系列命令行操作。感興趣的可以看看這篇文章,其實直接看源碼也很簡單就是常規的那一套使用yargs
這個庫,然后從命令行接受命令行參數轉化為webpack實際的參數。
進入正題,如果你使用過create-react-app或者vue-cli這樣的工具,你都發現他們都并沒有直接用命令行打包,而是自己寫了一個腳本然后require('webpack')
然后傳入config進行打包,這樣的好處是更靈活,更好控制,以下來自create-react-app中react-script的build:
const webpack = require('webpack');
function build(previousFileSizes) {
let compiler = webpack(config);
return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
// ....
})
})
}
先看webpack的package.json:
"main": "lib/webpack.js",
說明我們require的就是lib/webpack.js
:
const webpack = (options, callback) => {
const webpackOptionsValidationErrors = validateSchema(
webpackOptionsSchema,
options
);
if (webpackOptionsValidationErrors.length) {
throw new WebpackOptionsValidationError(webpackOptionsValidationErrors);
}
let compiler;
if (Array.isArray(options)) {
compiler = new MultiCompiler(options.map(options => webpack(options)));
} else if (typeof options === "object") {
options = new WebpackOptionsDefaulter().process(options);
compiler = new Compiler(options.context);
compiler.options = options;
new NodeEnvironmentPlugin().apply(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);
}
}
}
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
compiler.options = new WebpackOptionsApply().process(options, compiler);
} else {
throw new Error("Invalid argument: options");
}
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;
};
這段代碼主要的工作就是初始化complier。
options為數組的情況,目前項目中還真沒用過,以后有用到了再補充。
validateSchema
這部分主要就是校驗options并且返回報錯:
const validateObject = (schema, options) => {
const validate = ajv.compile(schema);
const valid = validate(options);
return valid ? [] : filterErrors(validate.errors);
};
return validateObject(schema, options);
schema是一個json形式的描述文件,描述著各個字段是什么類型,以及對應的錯誤信息,感覺就像async-validator的rules一樣,然后調用了一個叫ajv的庫,將秒速文件轉為了校驗函數校驗options。
WebpackOptionsDefaulter
其實webpack以及內置了很多默認的config,這個對象的作用就是合并默認的config和傳入的config。但是在源碼里這里不叫config,而是options。
class WebpackOptionsDefaulter extends OptionsDefaulter {
constructor() {
// this.set(xx, 'xx')
}
}
set方法就是給默認字段設置對應的值。
更核心的方法都在OptionsDefaulter里,比如webpack.js中的options = new WebpackOptionsDefaulter().process(options);的process
。
OptionsDefaulter中,一開始就是定了兩個方法,很有意思:
const getProperty = (obj, name) => {
name = name.split(".");
for (let i = 0; i < name.length - 1; i++) {
obj = obj[name[i]];
if (typeof obj !== "object" || !obj || Array.isArray(obj)) return;
}
return obj[name.pop()];
};
const setProperty = (obj, name, value) => {
name = name.split(".");
for (let i = 0; i < name.length - 1; i++) {
if (typeof obj[name[i]] !== "object" && obj[name[i]] !== undefined) return;
if (Array.isArray(obj[name[i]])) return;
if (!obj[name[i]]) obj[name[i]] = {};
obj = obj[name[i]];
}
obj[name.pop()] = value;
};
- getProperty:其作用就是拿對象的某個字段的值,但是這里的name可以是'xxx.xxx.xxx'這樣的格式,我們如果直接
a.b.c.d
如果b或者c就是undefined
,那么直接就報錯了,這里直接巧妙的避免了這樣的情況,類似于Experimental的語法 optional-chaining,如let value = a?.b?.c?.d
,如果有undefined最終就直接返回undefined了。 - setProperty:也類似,比如a是
{}
,我們不能直接設置a.b.c = xxx
,這樣如果b是undefined也會報錯,這里就可以直接設置了。
class OptionsDefaulter {
constructor() {
this.defaults = {};
this.config = {};
}
process(options) {
options = Object.assign({}, options);
for (let name in this.defaults) {
switch (this.config[name]) {
case undefined:
// ...
break;
case "call":
// ...
break;
case "make":
// ...
break;
case "append": {
// ...
}
default:
}
}
return options;
}
set(name, config, def) {
if (def !== undefined) {
this.defaults[name] = def;
this.config[name] = config;
} else {
this.defaults[name] = config;
delete this.config[name];
}
}
}
我們看到外層的WebpackOptionsDefaulter使用了很多set
,其實就是這里的set
,參數name代表option的具體名字,config代表具體的值。這里要說下的就是def:如果有def的時候,config就代表具體的合并方法的類型,而def就代表了option的具體值,具體的處理都在process中。
初始化complier
初始化complier就沒有太多好說的,主要就是初始化了一系列的勾子,和一系列參數:
this.hooks = { ... };
/** @type {string=} */
this.name = undefined;
/** @type {Compilation=} */
this.parentCompilation = undefined;
/** @type {string} */
this.outputPath = "";
this.outputFileSystem = null;
this.inputFileSystem = null;
/** @type {string|null} */
this.recordsInputPath = null;
/** @type {string|null} */
this.recordsOutputPath = null;
this.records = {};
this.removedFiles = new Set();
/** @type {Map<string, number>} */
this.fileTimestamps = new Map();
/** @type {Map<string, number>} */
this.contextTimestamps = new Map();
/** @type {ResolverFactory} */
this.resolverFactory = new ResolverFactory();
這些參數的具體含義簡單的看名字就知道,不懂的其實也要等到用到才明白。
可以講下的是ResolverFactory,看見resolve就能想到路徑解析。這個就是一個路徑解析器的工廠,在需要的時候根據option返回一個路徑解析器(這個類也有自己的勾子):
const { Tapable, HookMap, SyncHook, SyncWaterfallHook } = require("tapable");
const Factory = require("enhanced-resolve").ResolverFactory;
module.exports = class ResolverFactory extends Tapable {
constructor() {
super();
this.hooks = {
resolveOptions: new HookMap(
() => new SyncWaterfallHook(["resolveOptions"])
),
resolver: new HookMap(() => new SyncHook(["resolver", "resolveOptions"]))
};
this.cache1 = new WeakMap();
this.cache2 = new Map();
}
get(type, resolveOptions) {
const cachedResolver = this.cache1.get(resolveOptions);
if (cachedResolver) return cachedResolver();
const ident = `${type}|${JSON.stringify(resolveOptions)}`;
const resolver = this.cache2.get(ident);
if (resolver) return resolver;
const newResolver = this._create(type, resolveOptions);
this.cache2.set(ident, newResolver);
return newResolver;
}
_create(type, resolveOptions) {
// ....
}
};
他也是主要調用的enhanced-resolve
這個庫來解析路徑,并對解析器做了緩存。使用案例
應用NodeEnvironmentPlugin插件
該插件的主要作用是給complier初始化輸入輸出文件系統和監視文件系統。
class NodeEnvironmentPlugin {
apply(compiler) {
compiler.inputFileSystem = new CachedInputFileSystem(
new NodeJsInputFileSystem(),
60000
);
const inputFileSystem = compiler.inputFileSystem;
compiler.outputFileSystem = new NodeOutputFileSystem();
compiler.watchFileSystem = new NodeWatchFileSystem(
compiler.inputFileSystem
);
compiler.hooks.beforeRun.tap("NodeEnvironmentPlugin", compiler => {
if (compiler.inputFileSystem === inputFileSystem) inputFileSystem.purge();
});
}
}
module.exports = NodeEnvironmentPlugin;
- 輸出文件系統:非常簡單,就是包裝了一層node原生的fsapi。
- 輸入文件系統:復雜,目前還沒有看見使用的地方。
- 監視文件系統:復雜,目前還沒有看見使用的地方,但是主要的作用就是監視文件改動及熱更新等。
應用插件
這里很重要,我們webpack.config.js中的plugins就是在這個階段掛載上的:
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);
}
}
}
就像上一節講過的,在這里只是webpack抽象后的掛載插件,而真正的在勾子上掛載執行函數都是具體在每個plugin里實現的。
之前我們說過我們必須為plugin實現apply方法,實際上這里也展示了掛載動態插件的方式,就是為plugin實現call方法。
WebpackOptionsApply
這個對象做的事情理解起來非常簡單,那就是根據options為complier應用插件(但插件本身復雜度,哭了)。
process(options, compiler) {
let ExternalsPlugin;
compiler.outputPath = options.output.path;
compiler.recordsInputPath = options.recordsInputPath || options.recordsPath;
compiler.recordsOutputPath =
options.recordsOutputPath || options.recordsPath;
compiler.name = options.name;
// TODO webpack 5 refactor this to MultiCompiler.setDependencies() with a WeakMap
// @ts-ignore TODO
compiler.dependencies = options.dependencies;
new xxxPlugin().apply(compiler);
// ........
}
我們之前說過,webpack是插件機制,那到底是怎么樣的插件機制呢?其實就是webpack聲明了很多勾子,然后調用了很多勾子。 emmm,所以你看源碼的時候你會發現,他就是調用了很多勾子,然后代碼就完成打包了,但是你根本找不到處理代碼的地方到底在哪里,實際上做處理的是掛載在勾子上的插件,那么為了完成webpack的所有功能,到底掛載了哪些插件?這個問題的答案就在這個對象里。
在這里插一段其他文章里,我很認同的一段話:
- 聯系松散。你可以發現:使用tapable鉤子類似事件監聽模式,雖然能有效解耦,但鉤子的注冊與調用幾乎完全無關,很難將一個鉤子的“創建 - 注冊 - 調用”過程有效聯系起來。
- 模塊交互基于鉤子。webpack內部模塊與插件在很多時候,是通過鉤子機制來進行聯系與調用的。但是,基于鉤子的模式是松散的。例如你看到源碼里一個模塊提供了幾個鉤子,但你并不知道,在何時、何地該鉤子會被調用,又在何時、何地鉤子上被注冊了哪些方法。這些以往都是需要我們通過在代碼庫中搜索關鍵詞來解決。
- 鉤子數量眾多。webpack內部的鉤子非常多,數量達到了180+,類型也五花八門。除了官網列出的compiler與compilation中那些常用的鉤子,還存在著眾多其他可以使用的鉤子。有些有用的鉤子你可能無從知曉,例如我最近用到的localVars、requireExtensions等鉤子。
- 內置插件眾多。webpack v4+ 本身內置了許多插件。即使非插件,webpack的模塊自身也經常使用tapable鉤子來交互。甚至可以認為,webpack項目中的各個模塊都是“插件化”的。這也使得幾乎每個模塊都會和各種鉤子“打交道”。
我的建議是這個對象可以暫時不管,用到再查。
到現在只講了complier的初始化,實際上complier.run()
后你會發現更多的勾子調用,而且compiler里還會引用其他模塊,其他模塊還有自己的勾子,所以我們在閱讀源碼的時候,建議可以看見了某個勾子調用就全局搜下這個勾子的名字加上調用方法,然后再看掛載的執行函數到底做了什么處理。例如:這個勾子賦值給了complier.hooks.run,并且async的勾子,那么全局就搜索hooks.run.tap
或hooks.run.tapAsync
或hooks.run.tapPromise
,然后再看到底做了什么。