阅读源码的目的
为了有能力自己自定义编写自己的webpack插件(ps:为了写个自定义插件还需要阅读源代码--,给你一个赞👍,写的很好,下次别写了)
源码流程
执行webpack(options)返回一个compiler 首先看看compiler是如何生成的
const webpack = /** @type {WebpackFunctionSingle & WebpackFunctionMulti} */ (
/**
* @param {WebpackOptions | (ReadonlyArray<WebpackOptions> & MultiCompilerOptions)} options options
* @param {Callback<Stats> & Callback<MultiStats>=} callback callback
* @returns {Compiler | MultiCompiler}
*/
(options, callback) => {
...
const { compiler, watch } = create();
...
return compiler;
}
}
);
const create = () => {
...
const webpackOptions = /** @type {WebpackOptions} */ (options);
/** @type {Compiler} */
compiler = createCompiler(webpackOptions);
watch = webpackOptions.watch;
watchOptions = webpackOptions.watchOptions || {};
return { compiler, watch, watchOptions };
};
const createCompiler = rawOptions => {
//配置默认属性
const options = getNormalizedWebpackOptions(rawOptions);
applyWebpackOptionsBaseDefaults(options);
//创建compiler
const compiler = new Compiler(options.context, options);
/*
新增NodeEnvironmentPlugin插件:
给compiler绑定infrastructureLogger、
inputFileSystem(fileSystem引用graceful-fs 然后把其中一些方法进行缓存并且暴露出来 比如lstat、stata、readDir、readFile)、
outputFileSystem = intermediateFileSystem = graceful-fs、
watchFileSystem = new NodeWatchFileSystem(inputFileSystem)、其中watcher属性新建Watchpack实例 与监听文件相关 稍后再讲
然后在compiler.hooks.beforeRun.taps上增加事件
*/
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).apply(compiler);
//遍历plugins 执行plugin的apply方法 传递compiler
if (Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
//继续配置一些默认属性 如entry、output、resolve
applyWebpackOptionsDefaults(options);
//这时可以知道webpack首先执行的是compiler的environment事件钩子 此钩子在webpack执行初始化配置时启用 此时taps是空 没有添加事件执行
compiler.hooks.environment.call();
//afterEnvironment钩子在environment钩子后直接调用 此时taps是空 没有添加事件执行
compiler.hooks.afterEnvironment.call();
//默认会有许多plugin注册进插件中 另外介绍
new WebpackOptionsApply().process(options, compiler);
//compiler.hooks.initialize钩子事件触发
//如果配置了HtmlWebpackPlugin会在此钩子上绑定一个事件 调用
compiler.hooks.initialize.call();
return compiler;
};
createCompiler首先会创建compiler实例、新增NodeEnvironmentPlugin插件,在beforeRun钩子中增加事件、循环调用plugins的apply方法注册插件、配置初始化参数..
WebpackOptionApply类
默认初始化plugin的类
class WebpackOptionsApply extends OptionsApply {
constructor() {
super();
}
/**
* @param {WebpackOptions} options options object
* @param {Compiler} compiler compiler object
* @returns {WebpackOptions} options object
*/
process(options, compiler) {
//options属性给到compiler
compiler.outputPath = options.output.path;
compiler.recordsInputPath = options.recordsInputPath || null;
compiler.recordsOutputPath = options.recordsOutputPath || null;
compiler.name = options.name;
//注册初始plugin到compiler.hooks上
...
if (options.output.clean) {
const CleanPlugin = require("./CleanPlugin");
new CleanPlugin(
options.output.clean === true ? {} : options.output.clean
).apply(compiler);
}
...
//增加完EntryOptionPlugin到compiler.hooks.entryOption
new EntryOptionPlugin().apply(compiler);
//之后立刻执行entryOption钩子
//EntryOptionPlugin处理完会引入EntryPlugin 遍历每个入口 每个入口都会往compiler.hook.compilation和make中加入事件
compiler.hooks.entryOption.call(options.context, options.entry);
//初始化内部插件集合 有空自己研究 一大堆内部插件
...
new CommonJsPlugin().apply(compiler);
new LoaderPlugin({}).apply(compiler);
...
//执行afterPlugins钩子 默认空
compiler.hooks.afterPlugins.call(compiler);
if (!compiler.inputFileSystem) {
throw new Error("No input filesystem provided");
}
//compiler.resolverFactory.hooks有两个hooks
//往compiler.resolverFactory.hooks.resolveOptions._map(Map对象).normal.taps增加WebpackOptionsApply事件
compiler.resolverFactory.hooks.resolveOptions
.for("normal")
.tap("WebpackOptionsApply", resolveOptions => {
resolveOptions = cleverMerge(options.resolve, resolveOptions);
resolveOptions.fileSystem = compiler.inputFileSystem;
return resolveOptions;
});
//往compiler.resolverFactory.hooks.resolveOptions._map(Map对象).context.taps增加WebpackOptionsApply事件
compiler.resolverFactory.hooks.resolveOptions
.for("context")
.tap("WebpackOptionsApply", resolveOptions => {
resolveOptions = cleverMerge(options.resolve, resolveOptions);
resolveOptions.fileSystem = compiler.inputFileSystem;
resolveOptions.resolveToContext = true;
return resolveOptions;
});
//往compiler.resolverFactory.hooks.resolveOptions._map(Map对象).loader.taps增加WebpackOptionsApply事件
compiler.resolverFactory.hooks.resolveOptions
.for("loader")
.tap("WebpackOptionsApply", resolveOptions => {
resolveOptions = cleverMerge(options.resolveLoader, resolveOptions);
resolveOptions.fileSystem = compiler.inputFileSystem;
return resolveOptions;
});
//compiler.hooks.afterResolvers钩子事件触发 默认为空
compiler.hooks.afterResolvers.call(compiler);
return options;
}
}
初始的一些plugin为:
- ExternalsPlugin 注册到compiler.hooks.compile,与externals有关
- ChunkPrefetchPreloadPlugin 注册到compiler.hooks.compilation
- ArrayPushCallbackChunkFormatPlugin 注册到compiler.hooks.thisCompilation,与output.chunkformat == 'array-push'有关
- ModuleInfoHeaderPlugin 与output.pathinfo有关,输出额外注释到bundle中,注册到compiler.hooks.compilation
- CleanPlugin 用来清除output输出的,注册到compiler.hooks.emit
- EvalDevToolModulePlugin devtool相关,注册到compiler.hooks.compilation
- JavascriptModulesPlugin 注册到compiler.hooks.compilation
- JsonModulesPlugin 注册到compiler.hooks.compilation
- AssetModulesPlugin 注册到compiler.hooks.compilation
- EntryOptionPlugin 注册到compiler.hooks.entryOption...
Compiler介绍
Compiler 模块是 webpack 的主要引擎,它通过 CLI 或者 Node API 传递的所有选项创建出一个 compilation 实例。 它扩展(extends)自 Tapable 类,用来注册和调用插件。 大多数面向用户的插件会首先在 Compiler 上注册。
class Compiler {
/**
* @param {string} context the compilation path
* @param {WebpackOptions} options options
*/
constructor(context, options = /** @type {WebpackOptions} */ ({})) {
//初始化hook
this.hooks = Object.freeze({
/** @type {SyncHook<[]>} */
initialize: new SyncHook([]),
/** @type {SyncBailHook<[Compilation], boolean>} */
shouldEmit: new SyncBailHook(["compilation"]),
/** @type {AsyncSeriesHook<[Stats]>} */
done: new AsyncSeriesHook(["stats"]),
/** @type {SyncHook<[Stats]>} */
afterDone: new SyncHook(["stats"]),
/** @type {AsyncSeriesHook<[]>} */
additionalPass: new AsyncSeriesHook([]),
/** @type {AsyncSeriesHook<[Compiler]>} */
beforeRun: new AsyncSeriesHook(["compiler"]),
/** @type {AsyncSeriesHook<[Compiler]>} */
run: new AsyncSeriesHook(["compiler"]),
/** @type {AsyncSeriesHook<[Compilation]>} */
emit: new AsyncSeriesHook(["compilation"]),
/** @type {AsyncSeriesHook<[string, AssetEmittedInfo]>} */
assetEmitted: new AsyncSeriesHook(["file", "info"]),
/** @type {AsyncSeriesHook<[Compilation]>} */
afterEmit: new AsyncSeriesHook(["compilation"]),
/** @type {SyncHook<[Compilation, CompilationParams]>} */
thisCompilation: new SyncHook(["compilation", "params"]),
/** @type {SyncHook<[Compilation, CompilationParams]>} */
compilation: new SyncHook(["compilation", "params"]),
/** @type {SyncHook<[NormalModuleFactory]>} */
normalModuleFactory: new SyncHook(["normalModuleFactory"]),
/** @type {SyncHook<[ContextModuleFactory]>} */
contextModuleFactory: new SyncHook(["contextModuleFactory"]),
/** @type {AsyncSeriesHook<[CompilationParams]>} */
beforeCompile: new AsyncSeriesHook(["params"]),
/** @type {SyncHook<[CompilationParams]>} */
compile: new SyncHook(["params"]),
/** @type {AsyncParallelHook<[Compilation]>} */
make: new AsyncParallelHook(["compilation"]),
/** @type {AsyncParallelHook<[Compilation]>} */
finishMake: new AsyncSeriesHook(["compilation"]),
/** @type {AsyncSeriesHook<[Compilation]>} */
afterCompile: new AsyncSeriesHook(["compilation"]),
/** @type {AsyncSeriesHook<[]>} */
readRecords: new AsyncSeriesHook([]),
/** @type {AsyncSeriesHook<[]>} */
emitRecords: new AsyncSeriesHook([]),
/** @type {AsyncSeriesHook<[Compiler]>} */
watchRun: new AsyncSeriesHook(["compiler"]),
/** @type {SyncHook<[Error]>} */
failed: new SyncHook(["error"]),
/** @type {SyncHook<[string | null, number]>} */
invalid: new SyncHook(["filename", "changeTime"]),
/** @type {SyncHook<[]>} */
watchClose: new SyncHook([]),
/** @type {AsyncSeriesHook<[]>} */
shutdown: new AsyncSeriesHook([]),
/** @type {SyncBailHook<[string, string, any[]], true>} */
infrastructureLog: new SyncBailHook(["origin", "type", "args"]),
// TODO the following hooks are weirdly located here
// TODO move them for webpack 5
/** @type {SyncHook<[]>} */
environment: new SyncHook([]),
/** @type {SyncHook<[]>} */
afterEnvironment: new SyncHook([]),
/** @type {SyncHook<[Compiler]>} */
afterPlugins: new SyncHook(["compiler"]),
/** @type {SyncHook<[Compiler]>} */
afterResolvers: new SyncHook(["compiler"]),
/** @type {SyncBailHook<[string, Entry], boolean>} */
entryOption: new SyncBailHook(["context", "entry"])
});
//compiler.webpack中带有webpack暴露出的方法、变量
//const webpack = require("./");引入自身
this.webpack = webpack;
/** @type {string=} */
this.name = undefined;
/** @type {Compilation=} */
this.parentCompilation = undefined;
/** @type {Compiler} */
this.root = this;
/** @type {string} */
this.outputPath = "";
/** @type {Watching} */
this.watching = undefined;
/** @type {OutputFileSystem} */
this.outputFileSystem = null;
/** @type {IntermediateFileSystem} */
this.intermediateFileSystem = null;
/** @type {InputFileSystem} */
this.inputFileSystem = null;
/** @type {WatchFileSystem} */
this.watchFileSystem = null;
/** @type {string|null} */
this.recordsInputPath = null;
/** @type {string|null} */
this.recordsOutputPath = null;
this.records = {};
/** @type {Set<string | RegExp>} */
this.managedPaths = new Set();
/** @type {Set<string | RegExp>} */
this.immutablePaths = new Set();
/** @type {ReadonlySet<string>} */
this.modifiedFiles = undefined;
/** @type {ReadonlySet<string>} */
this.removedFiles = undefined;
/** @type {ReadonlyMap<string, FileSystemInfoEntry | "ignore" | null>} */
this.fileTimestamps = undefined;
/** @type {ReadonlyMap<string, FileSystemInfoEntry | "ignore" | null>} */
this.contextTimestamps = undefined;
/** @type {number} */
this.fsStartTime = undefined;
/** @type {ResolverFactory} */
this.resolverFactory = new ResolverFactory();
this.infrastructureLogger = undefined;
this.options = options;
this.context = context;
this.requestShortener = new RequestShortener(context, this.root);
this.cache = new Cache();
/** @type {Map<Module, { buildInfo: object, references: WeakMap<Dependency, Module>, memCache: WeakTupleMap }> | undefined} */
this.moduleMemCaches = undefined;
this.compilerPath = "";
/** @type {boolean} */
this.running = false;
/** @type {boolean} */
this.idle = false;
/** @type {boolean} */
this.watchMode = false;
this._backCompat = this.options.experiments.backCompat !== false;
/** @type {Compilation} */
this._lastCompilation = undefined;
/** @type {NormalModuleFactory} */
this._lastNormalModuleFactory = undefined;
/** @private @type {WeakMap<Source, { sizeOnlySource: SizeOnlySource, writtenTo: Map<string, number> }>} */
this._assetEmittingSourceCache = new WeakMap();
/** @private @type {Map<string, number>} */
this._assetEmittingWrittenFiles = new Map();
/** @private @type {Set<string>} */
this._assetEmittingPreviousFiles = new Set();
**}**
compiler hook的执行顺序:
SyncHook
在编译器准备环境时调用,时机就在配置文件中初始化插件之后,在createCompiler函数中,生成compiler实例,配置完初始化插件参数后调用。
-
afterEnvironment 环境设置完成后
SyncHook
当编译器环境设置完成后,在 environment hook 后直接调用。在createCompiler函数中。
SyncBailHook
- 回调参数:
context,entry
在createCompiler函数,WebpackOptionsApply.process(options, compiler)初始化默认插件的函数中调用,添加完EntryOptionPlugin到此钩子后调用,传入context跟options.entry选项,此时taps中有EntryOptionPlugin插件事件,处理事件,结果就是根据entry每个入口往compiler.hooks.compiliation跟make中各加入一个事件
class EntryOptionPlugin {
/**
* @param {Compiler} compiler the compiler instance one is tapping into
* @returns {void}
*/
apply(compiler) {
compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
EntryOptionPlugin.applyEntryOption(compiler, context, entry);
return true;
});
}
/**
* @param {Compiler} compiler the compiler
* @param {string} context context directory
* @param {Entry} entry request
* @returns {void}
*/
static applyEntryOption(compiler, context, entry) {
if (typeof entry === "function") {
const DynamicEntryPlugin = require("./DynamicEntryPlugin");
new DynamicEntryPlugin(context, entry).apply(compiler);
} else {
const EntryPlugin = require("./EntryPlugin");
for (const name of Object.keys(entry)) {
const desc = entry[name];
const options = EntryOptionPlugin.entryDescriptionToOptions(
compiler,
name,
desc
);
for (const entry of desc.import) {
new EntryPlugin(context, entry, options).apply(compiler);
}
}
}
}
/**
* @param {Compiler} compiler the compiler
* @param {string} name entry name
* @param {EntryDescription} desc entry description
* @returns {EntryOptions} options for the entry
*/
static entryDescriptionToOptions(compiler, name, desc) {
/** @type {EntryOptions} */
const options = {
name,
filename: desc.filename,
runtime: desc.runtime,
layer: desc.layer,
dependOn: desc.dependOn,
baseUri: desc.baseUri,
publicPath: desc.publicPath,
chunkLoading: desc.chunkLoading,
asyncChunks: desc.asyncChunks,
wasmLoading: desc.wasmLoading,
library: desc.library
};
if (desc.layer !== undefined && !compiler.options.experiments.layers) {
throw new Error(
"'entryOptions.layer' is only allowed when 'experiments.layers' is enabled"
);
}
if (desc.chunkLoading) {
const EnableChunkLoadingPlugin = require("./javascript/EnableChunkLoadingPlugin");
EnableChunkLoadingPlugin.checkEnabled(compiler, desc.chunkLoading);
}
if (desc.wasmLoading) {
const EnableWasmLoadingPlugin = require("./wasm/EnableWasmLoadingPlugin");
EnableWasmLoadingPlugin.checkEnabled(compiler, desc.wasmLoading);
}
if (desc.library) {
const EnableLibraryPlugin = require("./library/EnableLibraryPlugin");
EnableLibraryPlugin.checkEnabled(compiler, desc.library.type);
}
return options;
}
}
-
afterPlugins 初始化内部插件集合后
SyncHook
- 回调参数:
compiler
在createCompiler函数,WebpackOptionsApply.process(options, compiler)初始化默认插件的函数中调用,在初始化内部插件集合完成设置之后调用。
-
afterResolvers 设置完compiler.resolveFactory.hooks.resolveOptions钩子函数后触发
SyncHook
在createCompiler函数,WebpackOptionsApply.process(options, compiler)初始化默认插件的函数中调用
- 回调参数:
compiler
-
initialize 当compiler对象被初始化plugins、hooks后调用
SyncHook
在createCompiler函数,WebpackOptionsApply.process(options, compiler)初始化默认插件的函数后调用。如果配置了HtmlWebpackPlugin,会在此时调用配置在此钩子下的函数:
class HtmlWebpackPlugin {
/**
* @param {HtmlWebpackOptions} [options]
*/
constructor (options) {
/** @type {HtmlWebpackOptions} */
this.userOptions = options || {};
this.version = HtmlWebpackPlugin.version;
}
apply (compiler) {
// Wait for configuration preset plugions to apply all configure webpack defaults
compiler.hooks.initialize.tap('HtmlWebpackPlugin', () => {
//配置的参数与默认参数合并
const userOptions = this.userOptions;
// Default options
/** @type {ProcessedHtmlWebpackOptions} */
const defaultOptions = {
template: 'auto',
templateContent: false,
templateParameters: templateParametersGenerator,
filename: 'index.html',
publicPath: userOptions.publicPath === undefined ? 'auto' : userOptions.publicPath,
hash: false,
inject: userOptions.scriptLoading === 'blocking' ? 'body' : 'head',
scriptLoading: 'defer',
compile: true,
favicon: false,
minify: 'auto',
cache: true,
showErrors: true,
chunks: 'all',
excludeChunks: [],
chunksSortMode: 'auto',
meta: {},
base: false,
title: 'Webpack App',
xhtml: false
};
/** @type {ProcessedHtmlWebpackOptions} */
const options = Object.assign(defaultOptions, userOptions);
this.options = options;
// Assert correct option spelling
...
// Default metaOptions if no template is provided
...
// 遍历entry 根据每个key生成数组 数组元素是option 调用hookIntoCompiler
const userOptionFilename = userOptions.filename || defaultOptions.filename;
const filenameFunction = typeof userOptionFilename === 'function'
? userOptionFilename
// Replace '[name]' with entry name
: (entryName) => userOptionFilename.replace(/\[name\]/g, entryName);
/** output filenames for the given entry names */
const entryNames = Object.keys(compiler.options.entry);
const outputFileNames = new Set((entryNames.length ? entryNames : ['main']).map(filenameFunction));
/** Option for every entry point */
const entryOptions = Array.from(outputFileNames).map((filename) => ({
...options,
filename
}));
//调用hookIntoCompiler函数 主要是处理一下参数的一些初始化操作 给compiler.hooks.thisCompilation添加上事件
entryOptions.forEach((instanceOptions) => {
hookIntoCompiler(compiler, instanceOptions, this);
});
});
}
function hookIntoCompiler (compiler, options, plugin) {
const webpack = compiler.webpack;
// Instance variables to keep caching information
// for multiple builds
let assetJson;
/**
* store the previous generated asset to emit them even if the content did not change
* to support watch mode for third party plugins like the clean-webpack-plugin or the compression plugin
* @type {Array<{html: string, name: string}>}
*/
let previousEmittedAssets = [];
//将options.template转化成 ..dirname/html-webpack-plugin/lib/loader.js!...dirname/template.ejs(你的template文件)的形式
options.template = getFullTemplatePath(options.template, compiler.context);
// 使用缓存 将key用compiler映射为PersistentChildCompilerSingletonPlugin
//调用PersistentChildCompilerSingletonPlugin.apply方法 为compiler.hooks.make添加一个事件
const childCompilerPlugin = new CachedChildCompilation(compiler);
//如果没有templateContent说明template是一个文件
if (!options.templateContent) {
//调用PersistentChildCompilerSingletonPlugin.addEntry(entry)
//给PersistentChildCompilerSingletonPlugin实例.compilationState.entries数组添加上上面的template request路径
childCompilerPlugin.addEntry(options.template);
}
//判断options.filname是不是相对路径那种 是就转化为相对于ouput.path的路径
const filename = options.filename;
if (path.resolve(filename) === path.normalize(filename)) {
const outputPath = /** @type {string} - Once initialized the path is always a string */(compiler.options.output.path);
options.filename = path.relative(outputPath, filename);
}
//检查options.minify是否启用以及当前是否为正式环境 是就启用压缩
const isProductionLikeMode = compiler.options.mode === 'production' || !compiler.options.mode;
const minify = options.minify;
if (minify === true || (minify === 'auto' && isProductionLikeMode)) {
/** @type { import('html-minifier-terser').Options } */
options.minify = {
// https://www.npmjs.com/package/html-minifier-terser#options-quick-reference
collapseWhitespace: true,
keepClosingSlash: true,
removeComments: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
useShortDoctype: true
};
}
//给compiler.hooks.thisCompilation添加上事件
compiler.hooks.thisCompilation.tap('HtmlWebpackPlugin',
/**
* Hook into the webpack compilation
* @param {WebpackCompilation} compilation
*/
(compilation) => {
...
});
});
总结
compiler总的来说就是webpack的一个构建器,开发者可以通过往compiler.hook上绑定各生命周期的钩子来影响webpack的构建过程。创建compiler的主要流程有:
- 创建compiler实例
- 新增NodeEnvironmentPlugin插件,在beforeRun钩子中增加事件
- 循环调用plugins的apply方法注册插件
- 配置一些默认属性 如entry、output、resolve
- 执行compiler.hooks.environment事件钩子、afterEnvironment钩子在environment钩子后直接调用
- 配置许多默认plugin注册进插件中
- compiler.hooks.initialize钩子事件触发
- 返回compiler