前端工程化-插件机制

Posted natsu-cc

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前端工程化-插件机制相关的知识,希望对你有一定的参考价值。

前言

webpack本身并不难,它所完成的各种复杂炫酷的功能都依赖于他的插件机制。在 webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 webpack 提供的 API 改变输出结果。



Tapable:

webpack的插件机制依赖于一个核心的库, Tapable。tapable 是一个类似于nodejs 的EventEmitter 的库, 主要是控制钩子函数的发布与订阅。当然,tapable提供的hook机制比较全面,分为同步和异步两个大类(异步中又区分异步并行和异步串行),而根据事件执行的终止条件的不同,由衍生出 Bail/Waterfall/Loop 类型。

  • Sync: 同步执行。
  • AsyncSeries:异步顺序执行。
  • AsyncParallel: 异步并行执行。
基本使用:
const 
  SyncHook
 = require('tapable')

// 创建一个同步 Hook,指定参数
const hook = new SyncHook(['arg1', 'arg2'])

// 注册
hook.tap('a', function (arg1, arg2) 
	console.log('a')
)

hook.tap('b', function (arg1, arg2) 
	console.log('b')
)

hook.call(1, 2)

钩子类型:
  • BasicHook:执行每一个,不关心函数的返回值,有SyncHook、AsyncParallelHook、AsyncSeriesHook。
  • BailHook:顺序执行 Hook,遇到第一个结果result!==undefined则返回,不再继续执行。有:SyncBailHook、AsyncSeriseBailHook, AsyncParallelBailHook。
  • WaterfallHook:类似于 reduce,如果前一个 Hook 函数的结果 result !== undefined,则 result 会作为后一个 Hook 函数的第一个参数。有:SyncWaterfallHook,AsyncSeriesWaterfallHook。
  • LoopHook:不停的循环执行 Hook,直到所有函数结果 result === undefined。同样的,由于对串行性有依赖,所以只有 SyncLoopHook 和 AsyncSeriseLoopHook 。

分析:

Tapable 基本逻辑是,先通过类实例的 tap 方法注册对应 Hook 的处理函数。

const hook = new SyncHook(['arg1', 'arg2'])

然后其他各种hook都继承于一个基类Hook。

初始化完成后, 通常会注册一个事件,然后排序tap并放入到taps数组里面

// 注册
hook.tap('a', function (arg1, arg2) 
	console.log('a')
)

hook.tap('b', function (arg1, arg2) 
	console.log('b')
)

然后就是触发阶段

hook.call(1, 2)  // 触发函数

compile:

compiler 对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用 compiler 来访问 webpack 的主环境。

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> */
      ......
      ......
      some code
    ;
    ......
    ......
    some code

Compier继承了Tapable, 并且在实例上绑定了一个hook对象。

compilation:

compilation 对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。

class Compilation extends Tapable 
	/**
	 * Creates an instance of Compilation.
	 * @param Compiler compiler the compiler which created the compilation
	 */
	constructor(compiler) 
		super();
		this.hooks = 
			/** @type SyncHook<Module> */
			buildModule: new SyncHook(["module"]),
			/** @type SyncHook<Module> */
			rebuildModule: new SyncHook(["module"]),
			/** @type SyncHook<Module, Error> */
			failedModule: new SyncHook(["module", "error"]),
			/** @type SyncHook<Module> */
			succeedModule: new SyncHook(["module"]),

			/** @type SyncHook<Dependency, string> */
			addEntry: new SyncHook(["entry", "name"]),
			/** @type SyncHook<Dependency, string, Error> */
		
	


小结:

compiler 对象在 webpack 构建过程中代表着整个 webpack 环境,包含上下文、项目配置信息、执行、监听、统计等等一系列的信息,提供给 loader 和插件使用。它继承于 Tapable(Tapable 是 webpack 的一个底层库,类似于 NodeJS 的 EventEmitter 类),使用事件的发布 compiler.applyPlugins(‘eventName’) 订阅compiler.plugin(‘eventName’, callback) 模式注册 new WebpackPlugin().apply(compiler) 所有插件,插件必须提供 apply 方法给 webpack 完成注册流程,插件在 apply 方法内做一些初始化操作并监听 webpack 构建过程中的生命周期事件,等待构建时生命周期事件的发布。

所有插件都会在构建方法 compiler.run(callback) 之前注册,当 webpack 构建到某个阶段就会发布一个生命周期事件,此时所有订阅了当前发布的生命周期事件的插件会按照注册顺序一个一个执行订阅时提供的回调函数,回调函数的参数是与发布的生命周期事件相对应的参数,比如常用的 compilation 生命周期事件回调函数参数就包含 compilation 对象(此对象也是 webpack 构建机制的重要成员),entry-option 生命周期事件回调函数参数是 context(项目上下文路径)和 entry(项目配置的入口对象)。另外,插件如果需要异步执行编译,则还会提供一个回调函数作为监听回调函数的参数,异步编译完成必须调用回调函数。

简单点说,webpack 的构建包含很多个阶段,每个阶段都会发布对应的生命周期事件,插件需要提供 apply 方法注册并在此方法内监听指定的生命周期事件,事件发布后会顺序执行监听的回调函数并提供相对应的参数。

compiler 是 webpack 环境的代表,compilation 则是 webpack 构建内容的代表,它包含了每个构建环节及输出环节所对应的方法,存放着所有 module、chunk、asset 以及用来生成最后打包文件的 template 的信息。

编写一个插件:

class MyWebpackPlugin 
  // 定义 `apply` 方法
  apply(compiler) 
    // 指定要追加的事件钩子函数
    compiler.hooks.compile.tapAsync(
      'afterCompile',
      (compilation, callback) => 
        console.log('This is an example plugin!');
        console.log('Here’s the `compilation` object which represents a single build of assets:', compilation);

        // 使用 webpack 提供的 plugin API 操作构建结果
        compilation.addModule(/* ... */);

        callback();
      
    );
  

其实就是在apply中传入一个Compiler实例, 然后基于该实例注册事件, compilation同理, 最后webpack会在各流程执行call方法。

构建过程:
new WebpackOptionsDefaulter().process(options);
  • 创建了 WebpackOptionsDefaulter 实例,然后马上执行 process 方法并传入 options 作为参数,初始化 webpack options 后,创建 compiler 实例并设置 context(项目上下文路径)和 options(项目配置)
if(Array.isArray(options)) 
  compiler = new MultiCompiler(options.map(options => webpack(options)));
 else if(typeof options === "object") 
  // ...
  compiler = new Compiler();
  compiler.context = options.context;
  compiler.options = options;

  • 一般情况下,我们项目配置的 options 是一个 object,使用数组时应该是一个大的项目中包含多个小的子项目,需要能够单独打包小的子项目,也需要能够一次打包整个大的项目,这时候才应用到 MultiCompiler。
new NodeEnvironmentPlugin().apply(compiler);
  • 注册一个 NodeEnvironmentPlugin 插件:
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.plugin("before-run", (compiler, callback) => 
			if(compiler.inputFileSystem === inputFileSystem)
				inputFileSystem.purge();
			callback();
		);
	

if(options.plugins && Array.isArray(options.plugins)) 
  compiler.apply.apply(compiler, options.plugins);

  • 判断是否有 plugins 属性并且是否为数组,执行 compiler 的 apply 方法并且绑定 this 为 compiler 对象,再传入 options.plugins(项目配置的所有插件)作为参数。

Compiler主要继承于Tapable,Compiler 实例上的 apply 方法调用的是 Tapable.prototype.apply。

Tapable.prototype.apply = function apply() 
	for(var i = 0; i < arguments.length; i++) 
		arguments[i].apply(this);
	
;
compiler.options = new WebpackOptionsApply().process(options, compiler);
  • 之前执行 new WebpackOptionsDefaulter().process(options); 已经对 options 做过一次初始化操作,融合了 webpack 的默认配置与项目配置的结果,这次WebpackOptionsApply 类的 process 方法主要做了以下这些事:
  1. 把一些 options 上的属性赋值给 compiler 对象。
  2. 根据 options.target 的值注册相应的 webpack 内部插件。
  3. 根据 options 的配置确定是否要注册一些内部插件,如果配置了 externals 属性需要则注册 ExternalsPlugin 插件。
  4. 确定 compiler.resolvers 三个属性 normal、context、loader 的值。
  5. 发布三个生命周期事件:entry-option、after-plugins、after-resolvers。
  6. 返回 options。

这个过程就是在完善 compiler 对象,根据当前项目配置应用一些相对应的内部插件,从这里可以看出,webpack 内部也大量运用插件机制来实现编译构建,插件机制让 webpack 变得灵活而强大。

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;
  • 判断是否有 callback 参数,如果没有则直接返回 compiler 对象,构建程序 compiler.run(callback) 方法只有当有 callback 参数时才会执行,并且 callback 必须为函数。如果开启 watch 模式,则 webpack 会监听文件变化,当文件发生变动则会触发重新编译,像 webpack-dev-server 和 webpack-dev-middleware 里 watch 模式是默认开启的,方便进行开发。
    执行 compiler.run(callback),表示开始进行构建。至此,webpack 构建前的初始化操作已经全部完成,构建的关键节点:
  1. compile: 开始编译。
  2. make: 从入口点分析模块及其依赖的模块并创建这些模块对象。
  3. build-module: 构建模块。
  4. after-compile: 完成构建。
  5. emit: 把各个chunk输出到结果文件。
  6. after-emit: 完成输出。

重要的事件钩子:

compier:



compilation:



总结:

webpack本身并不复杂, 实质内部大量运用插件机制来实现编译构建,插件机制让 webpack 变得灵活而强大。

参考:

Tapable
Compiler
Compilation

以上是关于前端工程化-插件机制的主要内容,如果未能解决你的问题,请参考以下文章

前端工程化系列[03]-Grunt构建工具的运转机制

2021年抓住金三银四涨薪好时机,Android岗

前端工程化系列[04]-Grunt构建工具的使用进阶

深度掌握webpack的5个关键点

前端工程化系列[06]-Yeoman脚手架核心机制

webpack之plugin内部运行机制