前言
webpack
是个内容很丰富的话题。举个栗子。
webpack
的loader
和plugin
有什么区别?- 写过
webpack
的loader
或则plugin
吗? webpack
的编译流程是什么?
然后你就可以开始吧啦吧啦吧啦说出一大堆。你都是对的,但面试时间有限,再加之网络的答案及其多。所以,类似这样的问题好像并不是面试官的首选问题。
那么,你有木有遇到或者看到过这样的问题。
Q:webpack
编译流程中的 hook
节点 有哪些?
在之前的文章里我有提到过 webpack 的编译流程,文字叙述已经很详细了。具体的可以点击那些高级/资深的前端是如何回答JavaScript面试题的 (二) 。
webpack的开始
我们先来看个 webpack.config.js
const path = require('path')
module.exports = {
devtool: 'none',
mode: 'development',
context: process.cwd(),
entry: './src/index.js',
output: {
filename: 'index.js',
path: path.resolve('dist')
},
module:{
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: [
[
"@babel/preset-env",
{
useBuiltIns: "usage"
}
],
"@babel/preset-react"
]
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
})
]
}
想想,为什么项目里写个webpack.config.js 就能做到所有的事情?
- 有
entry
(入口) - 有
loader
- 有
plugin
- 有
output
最终是 module.exports
出了一个 {}
。 那么这里以 commonJS 规范倒出的 {}
给谁用了?
我们看 node_modules/webpack-cli/bin/cli.js
里面的部分源码
// cli.js …… 部分
const webpack = require("webpack");
let lastHash = null;
let compiler;
try {
compiler = webpack(options); // 1
} catch (err) {
if (err.name === "WebpackOptionsValidationError") {
if (argv.color) console.error(`\u001b[1m\u001b[31m${err.message}\u001b[39m\u001b[22m`);
else console.error(err.message);
// eslint-disable-next-line no-process-exit
process.exit(1);
}
throw err;
}
// ...
// ...
// ...
if (firstOptions.watch || options.watch) { // watch模式
const watchOptions =
irstOptions.watchOptions || options.watchOptions ||
firstOptions.watch || options.watch || {};
if (watchOptions.stdin) {
process.stdin.on("end", function(_) {
process.exit(); // eslint-disable-line
});
process.stdin.resume();
}
compiler.watch(watchOptions, compilerCallback);
if (outputOptions.infoVerbosity !== "none") console.error("\nwebpack is watching the files…\n");
} else {
// run 方法,
compiler.run((err, stats) => { // 2
if (compiler.close) {
ompiler.close(err2 => {
compilerCallback(err || err2, stats);
});
} else {
compilerCallback(err, stats);
}
});
}
// ...
请看其中1,2.
compiler = webpack(options);
compiler.run
所以,前面提到的 module.exports
出了一个 {}
, 就是 第1点中的 options
。 这个 options
= 你 webpack.config.js 里的 module.exports
+ webpack默认的内置配置。 我要讲的关键点不是这里,若有时间,请小伙伴自行多了解了解。
继续往下看,有个webpack(options)
,那就看看 node_modules/lib/webpack.js
/**
* @param {WebpackOptions} options options object
* @param {function(Error=, Stats=): void=} callback callback
* @returns {Compiler | MultiCompiler} the compiler object
*/
const webpack = (options, callback) => {
// code ...
if (Array.isArray(options)) {
// 加入 你 module.exports = [], 就进入到这。
// code ...
} else if (typeof options === "object") {
options = new WebpackOptionsDefaulter().process(options); // 获取webpack默认的内置好的配置信息
compiler = new Compiler(options.context);
compiler.options = options;
// NodeEnvironmentPlugin 赋予compiler文件读写的能力
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).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(); // 遇见的第1个钩子
compiler.hooks.afterEnvironment.call(); // 遇见的第2 个钩子
// 下面这行代码很重要,后面会重点提及
compiler.options = new WebpackOptionsApply().process(options, compiler);
} else {
throw new Error("Invalid argument: options");
}
if (callback) {
// code ...
}
return compiler;
};
遇见的第一个钩子
compiler.hooks.environment
// 遇见的第1个钩子compiler.hooks.afterEnvironment
// 遇见的第2 个钩子
你是不是想问,这两钩子干嘛的?
那我们就直接跟随源码追溯到这两个钩子的具体位置: node_modules/lib/Compiler.js
class Compiler extends Tapable {
constructor(context) {
super();
this.hooks = {
/** @type {SyncBailHook<Compilation>} */
shouldEmit: new SyncBailHook(["compilation"]),
/** @type {AsyncSeriesHook<Stats>} */
done: new AsyncSeriesHook(["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, Buffer>} */
assetEmitted: new AsyncSeriesHook(["file", "content"]),
/** @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 {AsyncSeriesHook<Compilation>} */
afterCompile: new AsyncSeriesHook(["compilation"]),
/** @type {AsyncSeriesHook<Compiler>} */
watchRun: new AsyncSeriesHook(["compiler"]),
/** @type {SyncHook<Error>} */
failed: new SyncHook(["error"]),
/** @type {SyncHook<string, string>} */
invalid: new SyncHook(["filename", "changeTime"]),
/** @type {SyncHook} */
watchClose: new SyncHook([]),
/** @type {SyncBailHook<string, string, any[]>} */
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>} */
entryOption: new SyncBailHook(["context", "entry"])
};
// code ...
}
// code ...
}
瞅准 this.hooks 里面 最后那几行提示:the following hooks are weirdly located here. move them for webpack 5
。 意思是这几个钩子有点奇怪,webpack5 会移除它们。 因为webpack 预留了一些钩子,目的是在不同的阶段时间去触发这些钩子。 而这两个钩子,实际上只起到了阶段提示作用,并没有做什么特殊的事情。
继续看 compiler.run
方法。
// code...
run(callback) {
if (this.running) return callback(new ConcurrentCompilationError());
const finalCallback = (err, stats) => {
this.running = false;
if (err) {
this.hooks.failed.call(err);
}
if (callback !== undefined) return callback(err, stats);
};
const startTime = Date.now();
this.running = true;
const onCompiled = (err, compilation) => {
if (err) return finalCallback(err);
if (this.hooks.shouldEmit.call(compilation) === false) {
const stats = new Stats(compilation);
stats.startTime = startTime;
stats.endTime = Date.now();
this.hooks.done.callAsync(stats, err => {
if (err) return finalCallback(err);
return finalCallback(null, stats);
});
return;
}
this.emitAssets(compilation, err => {
if (err) return finalCallback(err);
if (compilation.hooks.needAdditionalPass.call()) {
compilation.needAdditionalPass = true;
const stats = new Stats(compilation);
stats.startTime = startTime;
stats.endTime = Date.now();
this.hooks.done.callAsync(stats, err => {
if (err) return finalCallback(err);
this.hooks.additionalPass.callAsync(err => {
if (err) return finalCallback(err);
this.compile(onCompiled);
});
});
return;
}
this.emitRecords(err => {
if (err) return finalCallback(err);
const stats = new Stats(compilation);
stats.startTime = startTime;
stats.endTime = Date.now();
this.hooks.done.callAsync(stats, err => {
if (err) return finalCallback(err);
return finalCallback(null, stats);
});
});
});
};
this.hooks.beforeRun.callAsync(this, err => {
if (err) return finalCallback(err);
this.hooks.run.callAsync(this, err => {
if (err) return finalCallback(err);
this.readRecords(err => {
if (err) return finalCallback(err);
this.compile(onCompiled);
});
});
});
}
解析:
-
先定义了一个
finalCallback
,方法内部会触发this.hooks.failed
钩子。此时方法没有被调用。 -
定义了一个
onCompiled
,等待后续在compile方法里面调用。意思是模块编译完成后要发生的逻辑。this.hooks.shouldEmit
钩子 是用来判定当前模块的编译(也就是compilation
)有没有完成。若完成了,直接执行 finalCallback。- 接着,执行
this.emitAssets
方法( 最终在这里将处理好的chunk
写入到指定的文件然后输出至dist
) - 触发
compilation.hooks.needAdditionalPass
钩子,意思是这里还需要满足额外的条件,不满足则return undefined, 程序终止。- 将
compilation.needAdditionalPass
置为true
- 将 compilation 对象身上的一些值给stats
- 触发 this.hooks.done 钩子,执行 finalCallback。
- 将
-
触发
this.hooks.beforeRun
钩子,若没有异常,则异步触发this.hooks.run
钩子。 -
调用
this.readRecords
方法(读取文件),将读到的内容交给this.compile
. 那我们继续往下走,看看compile
方法
compile(callback) {
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
if (err) return callback(err);
this.hooks.compile.call(params);
const compilation = this.newCompilation(params);
this.hooks.make.callAsync(compilation, err => {
if (err) return callback(err);
compilation.finish(err => {
if (err) return callback(err);
compilation.seal(err => {
if (err) return callback(err);
this.hooks.afterCompile.callAsync(compilation, err => {
if (err) return callback(err);
return callback(null, compilation);
});
});
});
});
});
}
// code ...
newCompilation(params) {
const compilation = this.createCompilation();
compilation.fileTimestamps = this.fileTimestamps;
compilation.contextTimestamps = this.contextTimestamps;
compilation.name = this.name;
compilation.records = this.records;
compilation.compilationDependencies = params.compilationDependencies;
this.hooks.thisCompilation.call(compilation, params);
this.hooks.compilation.call(compilation, params);
return compilation;
}
- compile 方法
- 通过 调用
this.newCompilationParams
方法得到实例化 compilation 的初始化参数。 - 触发
this.hooks.beforeCompile
钩子,若异常则return 回调。 - 紧接着触发
this.hooks.compile
钩子 - 调用
this.newCompilation
方法- 实例化对象:
compilation
- 触发
this.hooks.thisCompilation
钩子 - 触发
this.hooks.compilation
钩子 - 得到 实例化对象:
compilation
- 实例化对象:
- 触发
this.hooks.make
钩子,若异常则 return 回调。 - 调用
compilation.finish
方法,若异常则 return 回调。 - 调用
compilation.seal
方法(处理chunk),若异常则 return 回调。 - 触发
this.hooks.afterCompile
钩子 ,代表编译完成。 - 最终 return 的 callback 就是
this.onCompiled
. 执行this.onCompiled
,并且传入了compilation
。 回 步骤2
- 通过 调用
compilation 的钩子呢??
问: 去哪了? compilation对象身上的那些重要钩子去哪了?
答: 当你触发 this.hooks.make
钩子的时候,就会调用 compilation.addEntry
方法了。
再问: 如何监听 make
钩子的?
答: 你怕不是忘了开头的那句代码。。。。
compiler.options = new WebpackOptionsApply().process(options, compiler); // 忘记的童鞋请全局搜索这行代码
WebpackOptionsApply是啥?node_modules/lib/WebpackOptionsApply.js
// 简写一下代码,因为真的太多了😂
//WebpackOptionsApply.js
const EntryOptionPlugin = require("./EntryOptionPlugin")
class WebpackOptionsApply {
process(options, compiler) {
new EntryOptionPlugin().apply(compiler)
compiler.hooks.entryOption.call(options.context, options.entry)
}
}
module.exports = WebpackOptionsApply
复制代码
明显,依赖 EntryOptionPlugin.js
// EntryOptionPlugin.js
const SingleEntryPlugin = require("./SingleEntryPlugin")
const itemToPlugin = function (context, item, name) {
return new SingleEntryPlugin(context, item, name)
}
class EntryOptionPlugin {
apply(compiler) {
compiler.hooks.entryOption.tap('EntryOptionPlugin', (context, entry) => {
itemToPlugin(context, entry, "main").apply(compiler)
})
}
}
module.exports = EntryOptionPlugin
明显,依赖 SingleEntryPlugin.js
// SingleEntryPlugin.js
class SingleEntryPlugin {
constructor(context, entry, name) {
this.context = context
this.entry = entry
this.name = name
}
apply(compiler) {
compiler.hooks.compilation.tap(
"SingleEntryPlugin",
(compilation, { normalModuleFactory }) => {
compilation.dependencyFactories.set(
SingleEntryDependency,
normalModuleFactory
);
}
);
compiler.hooks.make.tapAsync(
"SingleEntryPlugin",
(compilation, callback) => {
const { entry, name, context } = this;
const dep = SingleEntryPlugin.createDependency(entry, name);
compilation.addEntry(context, dep, name, callback);
}
);
}
}
module.exports = SingleEntryPlugin
解析:
new WebpackOptionsApply().process(options, compiler)
挂载所有 webpack 内置的插件(入口)- 走到
WebpackOptionsApply.js
当中process
方法里调用了new EntryOptionPlugin().apply(compiler)
- 走到
EntryOptionPlugin.js
当中- 在调用
itemToPlugin
, 的时候又返回了一个SingleEntryPlugin
的实例对象,其构造函数,负责接收上文中的context entry name
entryOption
是一个钩子实例,entryOption
在EntryOptionPlugin
内部的apply
方法中调用了tap
(注册了事件监听)
- 在调用
- 走到
SingleEntryPlugin.js
compilation
钩子监听make
钩子监听。
- 走到
- 触发了
compiler.hooks.entryOption
钩子
敲黑板!划重点!make 钩子一旦被触发,它的回调方法里就会执行 compilation.addEntry
,这标志着模块编译前的所有准备工作都做完了。
addEntry
->this._addModuleChain
->this.createModule
- 最后由
compiler
调用compilation.seal
方法-
触发
compilation.h- ooks.seal
钩子 -
触发
compilation.hooks.beforeChunks
钩子 -
触发
compilation.hooks.afterChunks
钩子 -
调用
compilation.createChunkAssets
方法,最终调用 this.emitAssets 方法,输出文件到打包路径下。
-
最后
跳槽是升职涨薪最直接有效的方式,备战2021金九银十,各位做好面试造火箭,工作拧螺丝的准备了吗?
掌握了这些知识点,面试时在激烈竞争中又可以夺目不少。机会都是留给有准备的人,只有充足的准备,才可能让自己可以在候选人中脱颖而出。
如果你需要这份完整版的面试笔记,可以点击这里直达领取方式!