Tapable 框架介绍

160 阅读9分钟

背景

我们知道 webpack 的强大离不开其丰富健壮的插件生态,其本身也是建立在插件的基础上运行的。而为了能有效的管理这些插件,webpack 实现了一个 tapable 框架来专门管理这些插件

本文基于实际的例子来介绍 tapable 框架中提供的 API,帮助读者能快速理解这些 API 的运行

基础概念

tapable 框架中主要通过各种 Hooks 来管理插件的运行,Hooks 类型如下:

  • Basic Hooks:名字中不带有 Waterfall, Bail 或者 Loop 的,它会直接调用每个绑定的插件
  • Waterfall:以一种流水线的形式来运行插件,每个插件的输出都可以作为下一个插件的输入
  • Bail:类似于 Promise.race,只要当其中一个插件返回任何值时,就停止运行剩下的插件
  • Loop:循环运行各个插件,直到所有插件都返回 undefined

Hooks 还分为同步、异步并行和异步串行:

  • Sync:同步运行所有插件,只能使用 tap 来绑定插件
  • AsyncSeries:异步串行运行插件,可以使用 tap、tapAsync 和 tapPromise 来绑定插件
  • AsyncParallel:异步并行运行插件,可以使用 tap、tapAsync 和 tapPromise 来绑定插件

上述两种类型可以相互组合,并形成最终 tapable 中提供的 Hooks,如下:

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

所有的 Hook 在初始化的时候,可以接收一个列表来描述参数(也可以不传参数),如下:

const hook = new SyncHook(["arg1", "arg2", "arg3"]);

新建完 hook 后,可以通过 tap 等方法来添加插件,然后可以通过 call 等方法来运行插件。下面给出各个不同的 Hook 的使用示例

Hook 使用示例

SyncHook

const syncHook = new SyncHook(["val"]);
syncHook.tap('LoggerPlugin', val => console.log('### in logger plugin, val: ', val));
syncHook.tap('TimeCalcPlugin', val => console.log('### in time calc plugin, val: ', val));
syncHook.call(123);
// ### in logger plugin, val:  123
// ### in time calc plugin, val:  123

// 参数个数不对示例
const syncHook2 = new SyncHook(["val"]);
syncHook2.tap('LoggerPlugin2', (val, val2) => console.log('### in logger plugin2, val, val2: ', val, val2));
syncHook2.tap('TimeCalcPlugin2', val => console.log('### in time calc plugin2, val: ', val));
syncHook2.call(123);
// ### in logger plugin2, val, val2:  123 undefined
// ### in time calc plugin2, val:  123

注意:新建 Hook 时的参数列表中的参数个数和添加插件时定义的参数列表的个数要保持一致,否则会接收不正确(新建时只指定一个,添加插件时有多个参数的话,插件只能收到第一个参数)

SyncBailHook

const syncBailHook = new SyncBailHook(['val']);

syncBailHook.tap('LoggerPlugin', val => console.log('### in logger plugin, val: ', val))
syncBailHook.tap('TimeCalcPlugin', val => {
  console.log('### in time calc plugin, val: ', val);
  return false;
});
syncBailHook.tap('HelloPlugin', val => console.log('### in time hello plugin, val: ', val));

syncBailHook.call(123);
// ### in logger plugin, val:  123
// ### in time calc plugin, val:  123

注意:插件返回任何除 undefined 以外的任何值时都可以阻止后续插件的运行

SyncWaterfallHook

const syncWfHook = new SyncWaterfallHook(['val']);
syncWfHook.tap('LoggerPlugin', val => {
  console.log('### in logger plugin, val: ', val);
  return 1;
});
syncWfHook.tap('TimeCalcPlugin', val => {
  console.log('### in time calc plugin, val: ', val)
});
syncWfHook.call(123);
// ### in logger plugin, val:  123
// ### in time calc plugin, val:  1

SyncLoopHook

const syncLpHook = new SyncLoopHook(['val']);
let cnt = 0;
syncLpHook.tap('LoggerPlugin', val => {
  console.log('### in logger plugin, val: ', val);
  cnt++;
  if (cnt > 4) {
    return;
  }
  return true;
});
syncLpHook.tap('TimeCalcPlugin', val => {
  console.log('### in time calc plugin, val: ', val)
  cnt++;
  if (cnt > 7) {
    return;
  }
  return true;
});
syncLpHook.call(123);
// 5 ### in logger plugin, val:  123
// ### in time calc plugin, val:  123
// ### in logger plugin, val:  123
// ### in time calc plugin, val:  123

注意:这里 LoggerPlugin 先会循环运行 5 轮,此时 cnt 为 5,然后运行 TimeCalcPlugin 一轮,此时 cnt 为 6,插件返回为 true,此时继续进行下一轮的运行,先运行 LoggerPlugin 一次,cnt 为 7,插件返回 undefined,然后运行 TimeCalcPlugin,此时 cnt 为 8,插件返回 undefined,结束运行

AsyncParallelHook

const asyncPlHook = new AsyncParallelHook(['val']);
asyncPlHook.tap('LoggerPlugin', val => console.log('### in logger plugin, val: ', val));
asyncPlHook.tapPromise('TimePromise1', val => {
  console.log('### in time promise1 plugin: ', val);
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('### end for time promise1 plugin.');
      resolve();
    }, 1000);
  });
});
asyncPlHook.tapPromise('TimePromise2', val => {
  console.log('### in time promise2 plugin: ', val);
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('### end for time promise2 plugin.');
      resolve();
    }, 2000);
  });
});
asyncPlHook.promise(123).then(res => {
  // res is undefined for AsyncParallelHook
  console.log('### end, res: ', res);
})

// ### in logger plugin, val:  123
// ### in time promise1 plugin:  123
// ### in time promise2 plugin:  123
// ### end for time promise1 plugin.
// ### end for time promise2 plugin.
// ### end, res:  undefined


// 这里的 callback 都传 undefined,此时会一直运行直到callback 运行次数与定义的次数一致才会最终调用 callback
const asyncPlHook2 = new AsyncParallelHook(['val']);
asyncPlHook2.tap('LoggerPlugin2', val => console.log('### in logger plugin2, val: ', val));
asyncPlHook2.tapAsync("BaiduPlugin", (val, callback) => {
  console.log('### in BaiduPlugin.');
  setTimeout(() => {
    console.log('### end for BaiduPlugin.');
    callback();
  }, 1000);
});
asyncPlHook2.tapAsync("GooglePlugin", (val, callback) => {
  console.log('### in GooglePlugin.');
  setTimeout(() => {
    console.log('### end for GooglePlugin.');
    callback();
  }, 3000);
});
asyncPlHook2.tapAsync("BingMapsPlugin", (val, callback) => {
  console.log('### in BingMapsPlugin.');
  setTimeout(() => {
    console.log('### end for BingMapsPlugin.');
    callback();
  }, 5000);
});

asyncPlHook2.callAsync(123, err => {
  console.log('### in callasync callback.');
});
// ### in logger plugin2, val:  123
// ### in BaiduPlugin.
// ### in GooglePlugin.
// ### in BingMapsPlugin.
// ### end for BaiduPlugin.
// ### end for GooglePlugin.
// ### end for BingMapsPlugin.
// ### in callasync callback.


// 第一个 callback 的参数为有效值(转为boolean时为 true),则直接运行最终的 callback
const asyncPlHook2 = new AsyncParallelHook(['val']);
asyncPlHook2.tap('LoggerPlugin2', val => console.log('### in logger plugin2, val: ', val));
asyncPlHook2.tapAsync("BaiduPlugin", (val, callback) => {
  console.log('### in BaiduPlugin.');
  setTimeout(() => {
    console.log('### end for BaiduPlugin.');
    callback(1);
  }, 1000);
});
asyncPlHook2.tapAsync("GooglePlugin", (val, callback) => {
  console.log('### in GooglePlugin.');
  setTimeout(() => {
    console.log('### end for GooglePlugin.');
    callback();
  }, 3000);
});
asyncPlHook2.tapAsync("BingMapsPlugin", (val, callback) => {
  console.log('### in BingMapsPlugin.');
  setTimeout(() => {
    console.log('### end for BingMapsPlugin.');
    callback();
  }, 5000);
});

asyncPlHook2.callAsync(123, val => {
  console.log('### end for hook, val is: ', val);
});
// ### in logger plugin2, val:  123
// ### in BaiduPlugin.
// ### in GooglePlugin.
// ### in BingMapsPlugin.
// ### end for BaiduPlugin.
// ### end for hook, val is:  1
// ### end for GooglePlugin.
// ### end for BingMapsPlugin.

注意:异步 Hook 即可以使用 tap 方法来添加同步插件,也可以使用 tapPromise 方法来添加异步插件,需要注意插件最终得返回一个 Promise 对象,还可以使用 tapAsync 来添加带回调函数的插件。这里需要避免 tapAsync 和 tapPromise 同时出现,两者的调用方式不一致

同时使用 tapAsync 时,当调用 callback 时的参数都为 undefined 时,则会一直运行直至其调用次数为定义的次数才会最终调用 callback,否则当传入的参数为非空时(转为boolean为true),则直接运行 callback 函数

AsyncParallelBailHook

const asPlBHook = new AsyncParallelBailHook(['val']);

asPlBHook.tapPromise('LoggerPlugin', val => {
  console.log('### in LoggerPlugin, val: ', val);
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('### end for LoggerPlugin');
      resolve(true);
    }, 1000)
  })
})

asPlBHook.tapPromise('TimePlugin', val => {
  console.log('### in TimePlugin, val: ', val);
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('### end for TimePlugin');
      resolve(true);
    }, 2000)
  })
})

asPlBHook.promise(123).then(() => {
  console.log('end for hook.');
});
// ### in LoggerPlugin, val:  123
// ### in TimePlugin, val:  123
// ### end for LoggerPlugin
// end for hook.
// ### end for TimePlugin

const asPlBHook = new AsyncParallelBailHook(['val']);

asPlBHook.tapAsync('LoggerPlugin', (val, callback) => {
  console.log('### in LoggerPlugin, val: ', val);
  callback(11, 33);
})

asPlBHook.tapAsync('TimePlugin', (val, callback) => {
  console.log('### in TimePlugin, val: ', val);
  callback(22, 33);
})

asPlBHook.callAsync(123, (val, val2) => {
  console.log('end for hook: ', val, val2);
});
// ### in LoggerPlugin, val:  123
// end for hook:  11 undefined


// 如果都为异步操作,则需要等所有的
const asPlBHook = new AsyncParallelBailHook(['val']);

asPlBHook.tapAsync('LoggerPlugin', (val, callback) => {
  console.log('### in LoggerPlugin, val: ', val);
  setTimeout(() => {
    callback(11, 33);
  }, 3000)
})

asPlBHook.tapAsync('TimePlugin', (val, callback) => {
  console.log('### in TimePlugin, val: ', val);
  setTimeout(() => {
    callback(22, 33);
  }, 2000)
})

asPlBHook.callAsync(123, (val, val2) => {
  console.log('end for hook: ', val, val2);
});
// ### in LoggerPlugin, val:  123
// ### in TimePlugin, val:  123
// end for hook:  11 undefined

注意:对于 promise 的形式,只要有一个的返回值不为 undefined 就直接运行结束;对于 async 的形式,当有一个插件直接调用 callback 时,就直接停止后续的运行(如果是异步操作,则不符合这个规则,需要等所有的操作都调用 callback 后才会最终结束,同时 callback 调用时的入参为第一次声明 callback 的地方,这里的运行逻辑感觉有点奇怪)

AsyncSeriesHook

const asSHook = new AsyncSeriesHook(['val']);

asSHook.tap('LoggerPlugin', val => {
  console.log('### in LoggerPlugin, val is: ', val);
  return val + 1;
});

asSHook.tapPromise('DelayPlugin', val => {
  console.log('### in DelayPlugin, val is: ', val);
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('### end for DelayPlugin');
      resolve(val + 1)
    }, 2000);
  });
})

asSHook.tapPromise('TimePlugin', val => {
  console.log('### in TimePlugin, val is: ', val);
  return Promise.resolve(val + 1);
});

asSHook.promise(1).then(res => {
  console.log('end for hook, res is: ', res);
})
// ### in LoggerPlugin, val is:  1
// ### in DelayPlugin, val is:  1
// ### end for DelayPlugin
// ### in TimePlugin, val is:  1
// end for hook, res is:  undefined


const asSHook = new AsyncSeriesHook(['val']);

asSHook.tap('LoggerPlugin', val => {
  console.log('### in LoggerPlugin, val is: ', val);
  return val + 1;
});

// TimePlugin 的运行要等当前 DelayPlugin 运行完成(调用 callback 即表示运行完成),同时因为传入参数不是 undefined,因此后面的 TimePlugin 不再运行
asSHook.tapAsync('DelayPlugin', (val, callback) => {
  console.log('### in DelayPlugin, val is: ', val);
  setTimeout(() => {
    console.log('### end for DelayPlugin');
    callback(1);
  }, 2000)
})

asSHook.tapAsync('TimePlugin', (val, callback) => {
  console.log('### in TimePlugin, val is: ', val);
  setTimeout(() => {
    console.log('### end for TimePlugin.');
    callback();
  }, 3000)
});

asSHook.callAsync(1, val => {
  console.log('end for hook, res is: ', val);
})
// ### in LoggerPlugin, val is:  1
// ### in DelayPlugin, val is:  1
// ### end for DelayPlugin
// end for hook, res is:  1

注意:对于 async 的调用方式,会先等当前异步任务操作完成(调用 callback 即表示运行完成),然后运行后续的异步任务,同时如果此时给 callback 传入有效参数,则会直接运行最终的 callback,并放弃后续异步函数的运行

AsyncSeriesBailHook

const asSBHook = new AsyncSeriesBailHook(['val']);

asSBHook.tap('LoggerPlugin', val => {
  console.log('### in LoggerPlugin, val is: ', val);
  return undefined;
});

asSBHook.tapPromise('DelayPlugin', (val) => {
  console.log('### in DelayPlugin, val is: ', val);
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('### end for DelayPlugin');
      resolve(1);
    }, 1000)
  })
})

asSBHook.tapPromise('TimePlugin', (val, callback) => {
  console.log('### in TimePlugin, val is: ', val);
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('### end for TimePlugin.');
      resolve(2);
    }, 3000)
  })
});

asSBHook.promise(1).then(val => {
  console.log('end for hook, res is: ', val);
});
// ### in LoggerPlugin, val is:  1
// ### in DelayPlugin, val is:  1
// ### end for DelayPlugin
// end for hook, res is:  1


const asSBHook = new AsyncSeriesBailHook(['val']);

asSBHook.tap('LoggerPlugin', val => {
  console.log('### in LoggerPlugin, val is: ', val);
  return undefined;
});

asSBHook.tapAsync('DelayPlugin', (val, callback) => {
  console.log('### in DelayPlugin, val is: ', val);
  setTimeout(() => {
    console.log('### end for DelayPlugin');
    callback(1);
  }, 2000)
})

asSBHook.tapAsync('TimePlugin', (val, callback) => {
  console.log('### in TimePlugin, val is: ', val);
  setTimeout(() => {
    console.log('### end for TimePlugin.');
    callback();
  }, 3000)
});

asSBHook.callAsync(1, val => {
  console.log('end for hook, res is: ', val);
})
// ### in LoggerPlugin, val is:  1
// ### in DelayPlugin, val is:  1
// ### end for DelayPlugin
// end for hook, res is:  1

AsyncSeriesLoopHook

webpack 中没有使用过这个 hook,故此处不再赘述

AsyncSeriesWaterfallHook

const asWfHook = new AsyncSeriesWaterfallHook(['val']);

asWfHook.tap('LoggerPlugin', val => {
  console.log('### in LoggerPlugin, val is: ', val);
  return val + 1;
});

asWfHook.tapPromise('DelayPlugin', (val) => {
  console.log('### in DelayPlugin, val is: ', val);
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('### end for DelayPlugin');
      resolve(val + 1);
    }, 1000)
  })
})

asWfHook.tapPromise('TimePlugin', (val, callback) => {
  console.log('### in TimePlugin, val is: ', val);
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('### end for TimePlugin.');
      resolve(val + 1);
    }, 3000)
  })
});

asWfHook.promise(1).then(res => {
  console.log('end for hook, res is: ', res);
});
// ### in LoggerPlugin, val is:  1
// ### in DelayPlugin, val is:  2
// ### end for DelayPlugin
// ### in TimePlugin, val is:  3
// ### end for TimePlugin.
// end for hook, res is:  4


const asWfHook = new AsyncSeriesWaterfallHook(['val']);

asWfHook.tap('LoggerPlugin', val => {
  console.log('### in LoggerPlugin, val is: ', val);
  return val + 1;
});

asWfHook.tapAsync('DelayPlugin', (val, callback) => {
  console.log('### in DelayPlugin, val is: ', val);
  setTimeout(() => {
    console.log('### end for DelayPlugin');
    callback(val + 1);
  }, 2000)
})

asWfHook.tapAsync('TimePlugin', (val, callback) => {
  console.log('### in TimePlugin, val is: ', val);
  setTimeout(() => {
    console.log('### end for TimePlugin.');
    callback(val + 1);
  }, 3000)
});

asWfHook.callAsync(1, res => {
  console.log('end for hook, res is: ', res);
})
// ### in LoggerPlugin, val is:  1
// ### in DelayPlugin, val is:  2
// ### end for DelayPlugin
// end for hook, res is:  3

注意:promise 的调用方式中,可以将当前插件的返回值作为参数传递给下一个插件,最终的结果即为最后一个插件的运行结果;async 的运行逻辑和 promise 不一样,同步插件的运行结果可以作为参数给下一个插件,但异步插件调用 callback 后(参数不为 undefined 时)会直接结束运行,并调用最终的 callback

Interception

hooks 中还提供了一些钩子函数,让调用者在插件被注册、调用等过程中运行一些自定义的代码,这些通过 intercept 函数来实现。如下,从输出结果可以看到在插件注册、触发、调用等过程中会先运行下我们通过 intercept 接口定义的钩子函数

const {
  SyncHook,
} = require("tapable");

const syncHook = new SyncHook(["val"]);

syncHook.intercept({
	call: (val) => {
		console.log("Starting to calculate routes: ", val);
	},
	register: (tapInfo) => {
		// tapInfo = { type: "promise", name: "GoogleMapsPlugin", fn: ... }
		console.log(`${tapInfo.name} is doing its job`);
		return tapInfo; // may return a new tapInfo object
	},
  tap: (tapInfo) => {
		// tapInfo = { type: "sync", name: "NoisePlugin", fn: ... }
		console.log(`${tapInfo.name} is doing it's job`);
	}
})

syncHook.tap('LoggerPlugin', val => console.log('### in logger plugin, val: ', val));
syncHook.tap('TimeCalcPlugin', val => console.log('### in time calc plugin, val: ', val));
syncHook.call(123);

// LoggerPlugin is doing its job
// TimeCalcPlugin is doing its job
// Starting to calculate routes:  123
// LoggerPlugin is doing it's job
// ### in logger plugin, val:  123
// TimeCalcPlugin is doing it's job
// ### in time calc plugin, val:  123