Webpack tapable 使用研究

原由

想研究一下Webpack运行原理,发现都提到了tapable,搞得我云里雾里,那咱们就好好研究一番,这究竟是个啥库。webpack

在Webpack官方文档上,查看Webpack的声明周期钩子函数,能够看到下图的内容:git

image

能够看到run函数是AsyncSeriesHook类型的钩子函数,这个就是tapable提供的钩子类型了。github

想理解Webpack的运行流程,先要了解这个钩子的使用,近而了解Webpack在运行的过程当中,是如何调用各类插件的。web

开始研究

先搭建一个最简单的项目

依照惯例,咱们先搭建个最简单的项目:npm

image

安装必要的库:json

npm install --save-dev wepback
npm install --save-dev webpack-cli
npm install --save-dev webpack-dev-server

npm install --save tapable
复制代码

咱们在src下写咱们的测试代码,而后运行起来,看咱们的实验结果。 webpack.config.js的配置以下:数组

module.exports = {
  entry: {
    index: __dirname + "/src/index.js",
  },
  output: {
    path: __dirname + "/dist",//打包后的文件存放的地方
    filename: "[name].js", //打包后输出文件的文件名
    chunkFilename: '[name].js',
  },
  mode: 'development',
  devtool: false,

  devServer: {
    contentBase: "./dist",//本地服务器所加载的页面所在的目录
    historyApiFallback: true,//不跳转
    inline: true//实时刷新
  },
}
复制代码

在package.json中配置好启动脚本,使用npm run server便可查看运行结果:promise

"scripts": {
    "start": "webpack",
    "server": "webpack-dev-server --open"
},
复制代码

同步钩子

第一个钩子 SyncHook

tapable的github地址是:github.com/webpack/tap…浏览器

这里给出的是tapable-1分支的地址,我看这个分支才是Webpack如今使用的。安全

依照它readme.md中介绍,tapable暴露了不少的Hook类,能够帮助咱们为插件建立钩子。

const {
	SyncHook,
	SyncBailHook,
	SyncWaterfallHook,
	SyncLoopHook,
	AsyncParallelHook,
	AsyncParallelBailHook,
	AsyncSeriesHook,
	AsyncSeriesBailHook,
	AsyncSeriesWaterfallHook
 } = require("tapable");
复制代码

这么多Hook,咱们一个一个来,先看看SyncHook怎么使用,在index.js中写下:

import { SyncHook } from 'tapable';

const hook = new SyncHook(); // 建立钩子对象
hook.tap('logPlugin', () => console.log('被勾了')); // tap方法注册钩子回调
hook.call(); // call方法调用钩子,打印出‘被勾了’三个字
复制代码

使用npm run server,在浏览器中运行成功。也成功打印‘被勾了’。用起来 仍是很简单的。

这就是经典的事件注册和触发机制啊。实际使用的时候,声明事件和触发事件的代码一般在一个类中,注册事件的代码在另外一个类(咱们的插件)中。代码以下:

// Car.js
import { SyncHook } from 'tapable';

export default class Car {
  constructor() {
    this.startHook = new SyncHook();
  }

  start() {
    this.startHook.call();
  }
}
复制代码
// index.js
import Car from './Car';

const car = new Car();
car.startHook.tap('startPlugin', () => console.log('我系一下安全带'));
car.start();
复制代码

钩子的使用基本就是这个意思,Car中只负责声明和调用钩子,真正的执行逻辑,再也不Car中,而是在注册它的index.js之中,是在Car以外。这样就作到了很好的解耦。

对于Car而言,经过这种注册插件的方式,丰富本身的功能。

向插件传递参数

我但愿这样:

// index.js
import Car from './Car';

const car = new Car();
car.accelerateHook.tap('acceleratePlugin', (speed) => console.log(`加速到${speed}`));
car.accelerate(100); // 调用时,将100传给插件回调的speed
复制代码

能够这样写Car类:

import { SyncHook } from 'tapable';

export default class Car {
  constructor() {
    this.startHook = new SyncHook();
    this.accelerateHook = new SyncHook(["newSpeed"]); // 在声明的时候,说明我这个Hook须要一个参数便可。
  }

  start() {
    this.startHook.call();
  }

  accelerate(speed) {
    this.accelerateHook.call(speed);
  }
}
复制代码

这样就完成了带参数的Hook,SyncHook参数是传递个数组,就是说也可让咱们传递多个参数,如 new SyncHook(["arg1","arg2","arg3"])。这样在call的时候也能够传递三个参数,在回调函数,也能接收到call的三个参数。

咱们的Car类,就是一个Tapable类,事件的声明和调用中心。

第二个钩子 SyncBailHook

Hook的注册/调用机制咱们大体了解了,SyncHook的工做很完美,可是tapable还提供了不少Hook,这些Hook又是解决什么问题的呢?

缘由在于对于某一个事件,咱们能够注册屡次,以下:

const car = new Car();
car.hooks.brake.tap('brakePlugin1', () => console.log(`刹车1`));
car.hooks.brake.tap('brakePlugin2', () => console.log(`刹车2`));
car.hooks.brake.tap('brakePlugin3', () => console.log(`刹车3`));

car.brake(); // 会打印‘刹车1’‘刹车2’‘刹车3’
复制代码

这里咱们为Car类添加了hooks.brake的钩子,和一个brake方法。brake的钩子被注册了3次,咱们调用brake方式的时候,3个插件都接受到了事件。

咱们稍微重构了一下Car类,听说这种写法,更符合tapable使用的最佳实践,其实就是将钩子都放到一个hooks字段里。Car代码以下:

import { SyncHook, SyncBailHook } from 'tapable';

export default class Car {
  constructor() {
    this.hooks = {
      start: new SyncHook(),
      accelerate: new SyncHook(["newSpeed"]),
      brake: new SyncBailHook(), // 这里咱们要使用SyncBailHook钩子啦
    };
  }

  start() {
    this.hooks.start.call();
  }

  accelerate(speed) {
    this.hooks.accelerate.call(speed);
  }

  brake() {
    this.hooks.brake.call();
  }
}
复制代码

咱们如今要知足这样一个需求,无论你注册多少插件,我只想被刹两次,就不通知别的插件了。这时候就SyncBailHook就能够,代码以下:

import Car from './Car';

const car = new Car();
car.hooks.brake.tap('brakePlugin1', () => console.log(`刹车1`));
// 只需在不想继续往下走的插件return非undefined便可。
car.hooks.brake.tap('brakePlugin2', () => { console.log(`刹车2`); return 1; }); 
car.hooks.brake.tap('brakePlugin3', () => console.log(`刹车3`));

car.brake(); // 只会打印‘刹车1’‘刹车2’
复制代码

SyncBailHook就是根据每一步返回的值来决定要不要继续往下走,若是return了一个非undefined的值 那就不会往下走,注意 若是什么都不return 也至关于return了一个undefined。

由此推测,tabable提供各种钩子,目的是处理这些外部插件的关系。

第三个钩子 SyncWaterfallHook

搞明白了第二个钩子,接下来的钩子就很好理解了,这里直接给出SyncWaterfallHook的定义:它的每一步都依赖上一步的执行结果,也就是上一步return的值就是下一步的参数。

咱们改造一下accelerate钩子为SyncWaterfallHook:

import { SyncHook, SyncBailHook, SyncWaterfallHook } from 'tapable';

export default class Car {
  constructor() {
    this.hooks = {
      start: new SyncHook(),
      accelerate: new SyncWaterfallHook(["newSpeed"]), // 重点在这里
      brake: new SyncBailHook(),
    };
  }

  start() {
    this.hooks.start.call();
  }

  accelerate(speed) {
    this.hooks.accelerate.call(speed);
  }

  brake() {
    this.hooks.brake.call();
  }
}
复制代码
// index.js
import Car from './Car';

const car = new Car();
car.hooks.accelerate.tap('acceleratePlugin1', (speed) => { console.log(`加速到${speed}`); return speed + 100; });
car.hooks.accelerate.tap('acceleratePlugin2', (speed) => { console.log(`加速到${speed}`); return speed + 100; });
car.hooks.accelerate.tap('acceleratePlugin3', (speed) => { console.log(`加速到${speed}`); });

car.accelerate(50); // 打印‘加速到50’‘加速到150’‘加速到250’

复制代码

第四个钩子 SyncLoopHook

SyncLoopHook是同步的循环钩子,它的插件若是返回一个非undefined。就会一直执行这个插件的回调函数,直到它返回undefined。

咱们把start的钩子改为SyncLoopHook。

import { SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook } from 'tapable';

export default class Car {
  constructor() {
    this.hooks = {
      start: new SyncLoopHook(), // 重点看这里
      accelerate: new SyncWaterfallHook(["newSpeed"]),
      brake: new SyncBailHook(),
    };
  }

  start() {
    this.hooks.start.call();
  }

  accelerate(speed) {
    this.hooks.accelerate.call(speed);
  }

  brake() {
    this.hooks.brake.call();
  }
}
复制代码
// index.js
import Car from './Car';

let index = 0;
const car = new Car();
car.hooks.start.tap('startPlugin1', () => {
  console.log(`启动`);
  if (index < 5) {
    index++;
    return 1;
  }
}); // 这回咱们获得一辆破车,启动6次才会启动成功。

car.hooks.start.tap('startPlugin2', () => {
  console.log(`启动成功`);
});

car.start(); // 打印‘启动’6次,打印‘启动成功’一次。

复制代码

异步钩子

当插件的回调函数,存在异步的时候。就须要使用异步的钩子了。

第五个钩子 AsyncParallelHook

AsyncParallelHook处理异步并行执行的插件。

咱们在Car类中添加calculateRoutes,使用AsyncParallelHook。再写一个calculateRoutes方法,调用callAsync方法时会触发钩子执行。这里能够传递一个回调,当全部插件都执行完毕的时候,被调用。

// Car.js
import {
  ...
  AsyncParallelHook,
} from 'tapable';

export default class Car {
  constructor() {
    this.hooks = {
      ...
      calculateRoutes: new AsyncParallelHook(),
    };
  }

  ...
  
  calculateRoutes(callback) {
    this.hooks.calculateRoutes.callAsync(callback);
  }
}
复制代码
// index.js
import Car from './Car';

const car = new Car();
car.hooks.calculateRoutes.tapAsync('calculateRoutesPlugin1', (callback) => {
  setTimeout(() => {
    console.log('计算路线1');
    callback();
  }, 1000);
});

car.hooks.calculateRoutes.tapAsync('calculateRoutesPlugin2', (callback) => {
  setTimeout(() => {
    console.log('计算路线2');
    callback();
  }, 2000);
});

car.calculateRoutes(() => { console.log('最终的回调'); }); // 会在1s的时候打印‘计算路线1’。2s的时候打印‘计算路线2’。紧接着打印‘最终的回调’
复制代码

我以为AsyncParallelHook的精髓就在于这个最终的回调。当全部的异步任务执行结束后,再最终的回调中执行接下来的代码。能够确保全部的插件的代码都执行完毕后,再执行某些逻辑。若是不须要这个最终的回调来执行某些代码,那使用SyncHook就好了啊,反正你又不关心插件中的代码何时执行完毕。

AsyncParallelHook的Promise方式

除了使用tapAsync/callAsync的方式使用AsyncParallelHook。还可使用tapPromise/promise的方式。

代码以下:

// Car.js
import {
  SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook,
  AsyncParallelHook,
} from 'tapable';

export default class Car {
  constructor() {
    this.hooks = {
      ...
      calculateRoutes: new AsyncParallelHook(),
    };
  }

  ...

  calculateRoutes() {
    return this.hooks.calculateRoutes.promise();
  }
}
复制代码
// index.js
import Car from './Car';

const car = new Car();
car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin1', () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('计算路线1');
      resolve();
    }, 1000);
  });
});

car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin2', () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('计算路线2');
      resolve();
    }, 2000);
  });
});

car.calculateRoutes().then(() => { console.log('最终的回调'); });
复制代码

只是用法上有区别,效果同tapAsync/callAsync同样的。

第六个钩子 AsyncParallelBailHook

这个我靠猜都知道它是怎么回事了,插件都并行执行,有一个执行成功而且传递的值不是undefined,就调用最终的回调。

来验证一下猜测:

// Car.js
import {
  AsyncParallelBailHook,
} from 'tapable';

export default class Car {
  constructor() {
    this.hooks = {
      drift: new AsyncParallelBailHook(),
    };
  }

  drift(callback) {
    this.hooks.drift.callAsync(callback);
  }
}
复制代码
// index.js
import Car from './Car';

const car = new Car();
car.hooks.drift.tapAsync('driftPlugin1', (callback) => {
  setTimeout(() => {
    console.log('计算路线1');
    callback(1); // 这里传递个1,不是undefined
  }, 1000);
});

car.hooks.drift.tapAsync('driftPlugin2', (callback) => {
  setTimeout(() => {
    console.log('计算路线2');
    callback(2); // 这里传递个2,不是undefined
  }, 2000);

});

car.drift((result) => { console.log('最终的回调', result); }); 
// 打印结果是,等1s打印'计算路线1' ,立刻打印‘最终的回调 1’,再到第2s,打印'计算路线2'
复制代码

咱们来分析下打印结果,说明AsyncParallelBailHook在插件调用callback时,若是给callback传参数,就会立马调用最终的回调函数。但并不会阻止其余插件继续执行本身的异步,只不过最终的回调拿不到这些比较慢的插件的回调结果了。

一样的AsyncParallelBailHook也有promise的调用方式,与AsyncParallelHook相似,我们就不实验了。

第七个钩子 AsyncSeriesHook

说完了并行,那必定有串行。就是插件一个一个的按顺序执行。

实验代码以下:

// Car.js
import {
  AsyncSeriesHook,
} from 'tapable';

export default class Car {
  constructor() {
    this.hooks = {
      calculateRoutes: new AsyncSeriesHook(),
    };
  }

  calculateRoutes() {
    return this.hooks.calculateRoutes.promise();
  }
}
复制代码
// index.js
import Car from './Car';

const car = new Car();
car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin1', () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('计算路线1');

      resolve();
    }, 1000);
  });
});

car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin2', () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('计算路线2');
      resolve();
    }, 2000);
  });
});

car.calculateRoutes().then(() => { console.log('最终的回调'); });
// 1s事后,打印计算路线1,再过2s(而不是到了第2s,而是到了第3s),打印计算路线2,再立马打印最终的回调。
复制代码

咱们这里直接使用promise的格式,同样执行。

第八个钩子 AsyncSeriesBailHook

串行执行,而且只要一个插件有返回值,立马调用最终的回调,而且不会继续执行后续的插件。

实验代码以下:

// Car.js
import {
  AsyncSeriesBailHook,
} from 'tapable';

export default class Car {
  constructor() {
    this.hooks = {
      calculateRoutes: new AsyncSeriesBailHook(),
    };
  }

  calculateRoutes() {
    return this.hooks.calculateRoutes.promise();
  }
}
复制代码
// index.js
import Car from './Car';

const car = new Car();
car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin1', () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('计算路线1');

      resolve(1);
    }, 1000);
  });
});

car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin2', () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('计算路线2');
      resolve(2);
    }, 2000);
  });
});

car.calculateRoutes().then(() => { console.log('最终的回调'); });
// 1s事后,打印计算路线1,立马打印最终的回调,不会再执行计算路线2了。
复制代码

第九个钩子 AsyncSeriesWaterfallHook

串行执行,而且前一个插件的返回值,会做为后一个插件的参数。

代码以下:

// Car.js
import {
  AsyncSeriesWaterfallHook,
} from 'tapable';

export default class Car {
  constructor() {
    this.hooks = {
      calculateRoutes: new AsyncSeriesWaterfallHook(['home']), // 要标注一下,要传参数啦
    };
  }

  calculateRoutes() {
    return this.hooks.calculateRoutes.promise();
  }
}
复制代码
// index.js
import Car from './Car';

const car = new Car();
car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin1', (result) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('计算路线1', result);

      resolve(1);
    }, 1000);
  });
});

car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin2', (result) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('计算路线2', result);
      resolve(2);
    }, 2000);
  });
});

car.calculateRoutes().then(() => { console.log('最终的回调'); });
// 1s事后,打印计算路线1 undefined,再过2s打印计算路线2 北京,而后立马打印最终的回调。
复制代码

打印结果如图:

image

封装插件

咱们将注册插件的逻辑单独封装出来,以下:

export default class CalculateRoutesPlugin {
  // 调用apply方法就能够完成注册
  apply(car) {
    car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin', (result) => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          console.log('计算路线1', result);

          resolve('北京');
        }, 1000);
      });
    });
  }
}
复制代码

在index.js中调用:

// index.js
import Car from './Car';
import CalculateRoutesPlugin from './CalculateRoutesPlugin';
const car = new Car();
const calculateRoutesPlugin = new CalculateRoutesPlugin();

calculateRoutesPlugin.apply(car); // 此节重点逻辑

car.calculateRoutes().then(() => { console.log('最终的回调'); });
// 运行正常,会打印'计算路线1'
复制代码

看到这里,代码和Webpack的使用方式就差很少了,car相似Webpack中的Compiler/Compilation。index.js比做是Webpack的运行类,使用咱们的Car(类比Compiler/Compilation),使用注入来的CalculateRoutesPlugin(类比Webpack的各类插件)。完成打包工做。

Tapable

tapable的readme中没有介绍一个类,就是Tapable,可是是可使用到的,以下

const {
  Tapable
} = require("tapable");
 
export default class Car extends Tapable {
    ...
}
复制代码

若是看tapable源码的话,看不到这个类,可是切换到tapable-1分支,能够看到。

在Webpack源码中,Compiler和Compilation都和上面的Car同样,继承自Tapable。

那Tapable究竟干了啥啊,看了一下它源码,发现它啥也没干,就是一个标志,表示我这个类是一个能够注册插件的类。

虽然没有什么加强的功能,可是此时的Car有了两个限制。以下:

const car = new Car();
car.apply(); // 报错  Tapable.apply is deprecated. Call apply on the plugin directly instead
car.plugin(); // 报错 Tapable.plugin is deprecated. Use new API on `.hooks` instead
复制代码

这两个方法不让用了,我理解是为Webpack而作的限制,提醒插件做者升级本身的插件,使用最新的实践。

钩子类型(Hook Types)

上面咱们研究了钩子们的使用,接下来作一些总结。首先来讲钩子的类型。

按被注册插件们的执行逻辑来分钩子

  1. 基本钩子。注册的插件顺序执行。如SyncHook、AsyncParallelHook、AsyncSeriesHook。

  2. 瀑布流钩子。前一个插件的返回值,是后一个插件的入参。如SyncWaterfallHook,AsyncSeriesWaterfallHook。

  3. Bail钩子。Bail钩子是指一个插件返回非undefined的值,就不继续执行后续的插件。我理解这里Bail是取迅速离开的意思。如:SyncBailHook,AsyncSeriesBailHook

  4. 循环钩子。循环调用插件,直到插件的返回值是undefined。如SyncLoopHook。

按时序来区分钩子

  1. 同步钩子。Sync开头的钩子
  2. 异步串行钩子。AsyncSeries开头的钩子。
  3. 异步并行钩子。AsyncParallel开头的钩子。

拦截器(Interception)

咱们还能够为钩子添加拦截器。 一个插件从对钩子注册,到钩子调用,再到插件响应。咱们均可以经过拦截器监听到。

car.hooks.calculateRoutes.intercept({
  call: (...args) => {
    console.log(...args, 'intercept call');
  }, // 插件被call时响应。
  //
  register: (tap) => {
    console.log(tap, 'ntercept register');

    return tap;
  },// 插件用tap方法注册时响应。
  loop: (...args) => {
    console.log(...args, 'intercept loop')
  },// loop hook的插件被调用时响应。
  tap: (tap) => {
    console.log(tap, 'intercept tap')
  } // hook的插件被调用时响应。
})
复制代码

上下文(Context)

插件和拦截器均可以往里面传一个上下文对象的参数,该对象可用于向后续插件和拦截器传递任意值。

myCar.hooks.accelerate.intercept({
	context: true, // 这里配置启用上下文对象
	tap: (context, tapInfo) => {
		if (context) { // 这里就能够拿到上下文对象
			context.hasMuffler = true;
		}
	}
});

myCar.hooks.accelerate.tap({
	name: "NoisePlugin",
	context: true
}, (context, newSpeed) => {
    // 这里能够拿到拦截器里的上下文对象,而后咱们在插件里利用它的值作相应操做。
	if (context && context.hasMuffler) {
		console.log("Silence...");
	} else {
		console.log("Vroom!");
	}
});
复制代码

结束语

tapable的简单使用,就研究到这里。它为插件机制提供了很强大的支持,不但让咱们对主体(Car)注册各类插件,还能控制插件彼此的关系,控制自身相应的时机。

在Webpack中使用这样的库,再合适不过,Webpack是一个插件的集合,经过tapable,有效的将插件们组织起来,在合理的时机,合理的调用。

相关文章
相关标签/搜索