揭秘高级/资深的前端是如何回答JavaScript面试题的

前言

webpack 是个内容很丰富的话题。举个栗子。

  • webpackloaderplugin 有什么区别?
  • 写过 webpackloader 或则 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.

  1. compiler = webpack(options);
  2. 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);
				});
			});
		});
	}

解析:

  1. 先定义了一个 finalCallback ,方法内部会触发 this.hooks.failed 钩子。此时方法没有被调用。

  2. 定义了一个 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。
  3. 触发 this.hooks.beforeRun 钩子,若没有异常,则异步触发 this.hooks.run 钩子。

  4. 调用 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;
  }
  1. 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

解析:

  1. new WebpackOptionsApply().process(options, compiler) 挂载所有 webpack 内置的插件(入口)
  2. 走到 WebpackOptionsApply.js 当中
    • process 方法里调用了 new EntryOptionPlugin().apply(compiler)
      • 走到 EntryOptionPlugin.js 当中
        • 在调用 itemToPlugin, 的时候又返回了一个 SingleEntryPlugin 的实例对象,其构造函数,负责接收上文中的 context entry name
        • entryOption 是一个钩子实例, entryOptionEntryOptionPlugin 内部的 apply 方法中调用了 tap (注册了事件监听)
      • 走到 SingleEntryPlugin.js
        • compilation 钩子监听
        • make 钩子监听。
    • 触发了compiler.hooks.entryOption 钩子

敲黑板!划重点!make 钩子一旦被触发,它的回调方法里就会执行 compilation.addEntry,这标志着模块编译前的所有准备工作都做完了。

  1. addEntry -> this._addModuleChain -> this.createModule
  2. 最后由 compiler 调用 compilation.seal 方法
    • 触发 compilation.h- ooks.seal 钩子

    • 触发 compilation.hooks.beforeChunks 钩子

    • 触发 compilation.hooks.afterChunks 钩子

    • 调用 compilation.createChunkAssets 方法,最终调用 this.emitAssets 方法,输出文件到打包路径下。

最后

跳槽是升职涨薪最直接有效的方式,备战2021金九银十,各位做好面试造火箭,工作拧螺丝的准备了吗?

掌握了这些知识点,面试时在激烈竞争中又可以夺目不少。机会都是留给有准备的人,只有充足的准备,才可能让自己可以在候选人中脱颖而出。

如果你需要这份完整版的面试笔记,可以点击这里直达领取方式!

  • 10
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值