我是好人

我是好人 查看完整档案

芜湖编辑  |  填写毕业院校  |  填写所在公司/组织 onlytg.com 编辑
编辑

搞事情

个人动态

我是好人 收藏了文章 · 3月20日

webpack核心模块tapable用法解析

前不久写了一篇webpack基本原理和AST用法的文章,本来想接着写webpack plugin的原理的,但是发现webpack plugin高度依赖tapable这个库,不清楚tapable而直接去看webpack plugin始终有点雾里看花的意思。所以就先去看了下tapable的文档和源码,发现这个库非常有意思,是增强版的发布订阅模式发布订阅模式在源码世界实在是太常见了,我们已经在多个库源码里面见过了:

  1. reduxsubscribedispatch
  2. Node.jsEventEmitter
  3. redux-sagatakeput

这些库基本都自己实现了自己的发布订阅模式,实现方式主要是用来满足自己的业务需求,而tapable并没有具体的业务逻辑,是一个专门用来实现事件订阅或者他自己称为hook(钩子)的工具库,其根本原理还是发布订阅模式,但是他实现了多种形式的发布订阅模式,还包含了多种形式的流程控制。

tapable暴露多个API,提供了多种流程控制方式,连使用都是比较复杂的,所以我想分两篇文章来写他的原理:

  1. 先看看用法,体验下他的多种流程控制方式
  2. 通过用法去看看源码是怎么实现的

本文就是讲用法的文章,知道了他的用法,大家以后如果有自己实现hook或者事件监听的需求,可以直接拿过来用,非常强大!

本文例子已经全部上传到GitHub,大家可以拿下来做个参考:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-usage

tapable是什么

tapablewebpack的核心模块,也是webpack团队维护的,是webpack plugin的基本实现方式。他的主要功能是为使用者提供强大的hook机制,webpack plugin就是基于hook的。

主要API

下面是官方文档中列出来的主要API,所有API的名字都是以Hook结尾的:

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

这些API的名字其实就解释了他的作用,注意这些关键字:Sync, Async, Bail, Waterfall, Loop, Parallel, Series。下面分别来解释下这些关键字:

Sync:这是一个同步的hook

Async:这是一个异步的hook

BailBail在英文中的意思是保险,保障的意思,实现的效果是,当一个hook注册了多个回调方法,任意一个回调方法返回了不为undefined的值,就不再执行后面的回调方法了,就起到了一个“保险丝”的作用。

WaterfallWaterfall在英语中是瀑布的意思,在编程世界中表示顺序执行各种任务,在这里实现的效果是,当一个hook注册了多个回调方法,前一个回调执行完了才会执行下一个回调,而前一个回调的执行结果会作为参数传给下一个回调函数。

LoopLoop就是循环的意思,实现的效果是,当一个hook注册了回调方法,如果这个回调方法返回了true就重复循环这个回调,只有当这个回调返回undefined才执行下一个回调。

ParallelParallel是并行的意思,有点类似于Promise.all,就是当一个hook注册了多个回调方法,这些回调同时开始并行执行。

SeriesSeries就是串行的意思,就是当一个hook注册了多个回调方法,前一个执行完了才会执行下一个。

ParallelSeries的概念只存在于异步的hook中,因为同步hook全部是串行的。

下面我们分别来介绍下每个API的用法和效果。

同步API

同步API就是这几个:

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

前面说了,同步API全部是串行的,所以这几个的区别就在流程控制上。

SyncHook

SyncHook是一个最基础的hook,其使用方法和效果接近我们经常使用的发布订阅模式,注意tapable导出的所有hook都是类,基本用法是这样的:

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

因为SyncHook是一个类,所以使用new来生成一个实例,构造函数接收的参数是一个数组["arg1", "arg2", "arg3"],这个数组有三项,表示生成的这个实例注册回调的时候接收三个参数。实例hook主要有两个实例方法:

  1. tap:就是注册事件回调的方法。
  2. call:就是触发事件,执行回调的方法。

下面我们扩展下官方文档中小汽车加速的例子来说明下具体用法:

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

// 实例化一个加速的hook
const accelerate = new SyncHook(["newSpeed"]);

// 注册第一个回调,加速时记录下当前速度
accelerate.tap("LoggerPlugin", (newSpeed) =>
  console.log("LoggerPlugin", `加速到${newSpeed}`)
);

// 再注册一个回调,用来检测是否超速
accelerate.tap("OverspeedPlugin", (newSpeed) => {
  if (newSpeed > 120) {
    console.log("OverspeedPlugin", "您已超速!!");
  }
});

// 再注册一个回调,用来检测速度是否快到损坏车子了
accelerate.tap("DamagePlugin", (newSpeed) => {
  if (newSpeed > 300) {
    console.log("DamagePlugin", "速度实在太快,车子快散架了。。。");
  }
});

// 触发一下加速事件,看看效果吧
accelerate.call(500);

然后运行下看看吧,当加速事件出现的时候,会依次执行这三个回调:

image-20210309160302799

上面这个例子主要就是用了tapcall这两个实例方法,其中tap接收两个参数,第一个是个字符串,并没有实际用处,仅仅是一个注释的作用,第二个参数就是一个回调函数,用来执行事件触发时的具体逻辑。

accelerate.tap("LoggerPlugin", (newSpeed) =>
  console.log("LoggerPlugin", `加速到${newSpeed}`)
);

上述这种写法其实与webpack官方文档中对于plugin的介绍非常像了,因为webpackplguin就是用tapable实现的,第一个参数一般就是plugin的名字:

image-20210309154641835

call就是简单的触发这个事件,在webpackplugin中一般不需要开发者去触发事件,而是webpack自己在不同阶段会触发不同的事件,比如beforeRun, run等等,plguin开发者更多的会关注这些事件出现时应该进行什么操作,也就是在这些事件上注册自己的回调。

SyncBailHook

上面的SyncHook其实就是一个简单的发布订阅模式SyncBailHook就是在这个基础上加了一点流程控制,前面我们说过了,Bail就是个保险,实现的效果是,前面一个回调返回一个不为undefined的值,就中断这个流程。比如我们现在将前面这个例子的SyncHook换成SyncBailHook,然后在检测超速的这个插件里面加点逻辑,当它超速了就返回错误,后面的DamagePlugin就不会执行了:

const { SyncBailHook } = require("tapable");    // 使用的是SyncBailHook

// 实例化一个加速的hook
const accelerate = new SyncBailHook(["newSpeed"]);

accelerate.tap("LoggerPlugin", (newSpeed) =>
  console.log("LoggerPlugin", `加速到${newSpeed}`)
);

// 再注册一个回调,用来检测是否超速
// 如果超速就返回一个错误
accelerate.tap("OverspeedPlugin", (newSpeed) => {
  if (newSpeed > 120) {
    console.log("OverspeedPlugin", "您已超速!!");

    return new Error('您已超速!!');
  }
});

accelerate.tap("DamagePlugin", (newSpeed) => {
  if (newSpeed > 300) {
    console.log("DamagePlugin", "速度实在太快,车子快散架了。。。");
  }
});

accelerate.call(500);

然后再运行下看看:

image-20210309161001682

可以看到由于OverspeedPlugin返回了一个不为undefined的值,DamagePlugin被阻断,没有运行了。

SyncWaterfallHook

SyncWaterfallHook也是在SyncHook的基础上加了点流程控制,前面说了,Waterfall实现的效果是将上一个回调的返回值作为参数传给下一个回调。所以通过call传入的参数只会传递给第一个回调函数,后面的回调接受都是上一个回调的返回值,最后一个回调的返回值会作为call的返回值返回给最外层:

const { SyncWaterfallHook } = require("tapable");

const accelerate = new SyncWaterfallHook(["newSpeed"]);

accelerate.tap("LoggerPlugin", (newSpeed) => {
  console.log("LoggerPlugin", `加速到${newSpeed}`);

  return "LoggerPlugin";
});

accelerate.tap("Plugin2", (data) => {
  console.log(`上一个插件是: ${data}`);

  return "Plugin2";
});

accelerate.tap("Plugin3", (data) => {
  console.log(`上一个插件是: ${data}`);

  return "Plugin3";
});

const lastPlugin = accelerate.call(100);

console.log(`最后一个插件是:${lastPlugin}`);

然后看下运行效果吧:

image-20210309162008465

SyncLoopHook

SyncLoopHook是在SyncHook的基础上添加了循环的逻辑,也就是如果一个插件返回true就会一直执行这个插件,直到他返回undefined才会执行下一个插件:

const { SyncLoopHook } = require("tapable");

const accelerate = new SyncLoopHook(["newSpeed"]);

accelerate.tap("LoopPlugin", (newSpeed) => {
  console.log("LoopPlugin", `循环加速到${newSpeed}`);

  return new Date().getTime() % 5 !== 0 ? true : undefined;
});

accelerate.tap("LastPlugin", (newSpeed) => {
  console.log("循环加速总算结束了");
});

accelerate.call(100);

执行效果如下:

image-20210309163514680

异步API

所谓异步API是相对前面的同步API来说的,前面的同步API的所有回调都是按照顺序同步执行的,每个回调内部也全部是同步代码。但是实际项目中,可能需要回调里面处理异步情况,也可能希望多个回调可以同时并行执行,也就是Parallel。这些需求就需要用到异步API了,主要的异步API就是这些:

const {
    AsyncParallelHook,
    AsyncParallelBailHook,
    AsyncSeriesHook,
    AsyncSeriesBailHook,
    AsyncSeriesWaterfallHook
 } = require("tapable");

既然涉及到了异步,那肯定还需要异步的处理方式,tapable支持回调函数和Promise两种异步的处理方式。所以这些异步API除了用前面的tap来注册回调外,还有两个注册回调的方法:tapAsynctapPromise,对应的触发事件的方法为callAsyncpromise。下面分别来看下每个API吧:

AsyncParallelHook

AsyncParallelHook从前面介绍的命名规则可以看出,他是一个异步并行执行的Hook,我们先用tapAsync的方式来看下怎么用吧。

tapAsync和callAsync

还是那个小汽车加速的例子,只不过这个小汽车加速没那么快了,需要一秒才能加速完成,然后我们在2秒的时候分别检测是否超速和是否损坏,为了看出并行的效果,我们记录下整个过程从开始到结束的时间:

const { AsyncParallelHook } = require("tapable");

const accelerate = new AsyncParallelHook(["newSpeed"]);

console.time("total time"); // 记录起始时间

// 注意注册异步事件需要使用tapAsync
// 接收的最后一个参数是done,调用他来表示当前任务执行完毕
accelerate.tapAsync("LoggerPlugin", (newSpeed, done) => {
  // 1秒后加速才完成
  setTimeout(() => {
    console.log("LoggerPlugin", `加速到${newSpeed}`);

    done();
  }, 1000);
});

accelerate.tapAsync("OverspeedPlugin", (newSpeed, done) => {
  // 2秒后检测是否超速
  setTimeout(() => {
    if (newSpeed > 120) {
      console.log("OverspeedPlugin", "您已超速!!");
    }
    done();
  }, 2000);
});

accelerate.tapAsync("DamagePlugin", (newSpeed, done) => {
  // 2秒后检测是否损坏
  setTimeout(() => {
    if (newSpeed > 300) {
      console.log("DamagePlugin", "速度实在太快,车子快散架了。。。");
    }

    done();
  }, 2000);
});

accelerate.callAsync(500, () => {
  console.log("任务全部完成");
  console.timeEnd("total time"); // 记录总共耗时
});

上面代码需要注意的是,注册回调要使用tapAsync,而且回调函数里面最后一个参数会自动传入done,你可以调用他来通知tapable当前任务已经完成。触发任务需要使用callAsync,他最后也接收一个函数,可以用来处理所有任务都完成后需要执行的操作。所以上面的运行结果就是:

image-20210309171527773

从这个结果可以看出,最终消耗的时间大概是2秒,也就是三个任务中最长的单个任务耗时,而不是三个任务耗时的总额,这就实现了Parallel并行的效果。

tapPromise和promise

现在都流行Promise,所以tapable也是支持的,执行效果是一样的,只是写法不一样而已。要用tapPromise,需要注册的回调返回一个promise,同时触发事件也需要用promise,任务运行完执行的处理可以直接使用then,所以上述代码改为:

const { AsyncParallelHook } = require("tapable");

const accelerate = new AsyncParallelHook(["newSpeed"]);

console.time("total time"); // 记录起始时间

// 注意注册异步事件需要使用tapPromise
// 回调函数要返回一个promise
accelerate.tapPromise("LoggerPlugin", (newSpeed) => {
  return new Promise((resolve) => {
    // 1秒后加速才完成
    setTimeout(() => {
      console.log("LoggerPlugin", `加速到${newSpeed}`);

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

accelerate.tapPromise("OverspeedPlugin", (newSpeed) => {
  return new Promise((resolve) => {
    // 2秒后检测是否超速
    setTimeout(() => {
      if (newSpeed > 120) {
        console.log("OverspeedPlugin", "您已超速!!");
      }
      resolve();
    }, 2000);
  });
});

accelerate.tapPromise("DamagePlugin", (newSpeed) => {
  return new Promise((resolve) => {
    // 2秒后检测是否损坏
    setTimeout(() => {
      if (newSpeed > 300) {
        console.log("DamagePlugin", "速度实在太快,车子快散架了。。。");
      }

      resolve();
    }, 2000);
  });
});

// 触发事件使用promise,直接用then处理最后的结果
accelerate.promise(500).then(() => {
  console.log("任务全部完成");
  console.timeEnd("total time"); // 记录总共耗时
});

这段代码的逻辑和运行结果和上面那个是一样的,只是写法不一样:

image-20210309172537951

tapAsync和tapPromise混用

既然tapable支持这两种异步写法,那这两种写法可以混用吗?我们来试试吧:

const { AsyncParallelHook } = require("tapable");

const accelerate = new AsyncParallelHook(["newSpeed"]);

console.time("total time"); // 记录起始时间

// 来一个promise写法
accelerate.tapPromise("LoggerPlugin", (newSpeed) => {
  return new Promise((resolve) => {
    // 1秒后加速才完成
    setTimeout(() => {
      console.log("LoggerPlugin", `加速到${newSpeed}`);

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

// 再来一个async写法
accelerate.tapAsync("OverspeedPlugin", (newSpeed, done) => {
  // 2秒后检测是否超速
  setTimeout(() => {
    if (newSpeed > 120) {
      console.log("OverspeedPlugin", "您已超速!!");
    }
    done();
  }, 2000);
});

// 使用promise触发事件
// accelerate.promise(500).then(() => {
//   console.log("任务全部完成");
//   console.timeEnd("total time"); // 记录总共耗时
// });

// 使用callAsync触发事件
accelerate.callAsync(500, () => {
  console.log("任务全部完成");
  console.timeEnd("total time"); // 记录总共耗时
});

这段代码无论我是使用promise触发事件还是callAsync触发运行的结果都是一样的,所以tapable内部应该是做了兼容转换的,两种写法可以混用:

image-20210309173217034

由于tapAsynctapPromise只是写法上的不一样,我后面的例子就全部用tapAsync了。

AsyncParallelBailHook

前面已经看了SyncBailHook,知道带Bail的功能就是当一个任务返回不为undefined的时候,阻断后面任务的执行。但是由于Parallel任务都是同时开始的,阻断是阻断不了了,实际效果是如果有一个任务返回了不为undefined的值,最终的回调会立即执行,并且获取Bail任务的返回值。我们将上面三个任务执行时间错开,分别为1秒,2秒,3秒,然后在2秒的任务触发Bail就能看到效果了:

const { AsyncParallelBailHook } = require("tapable");

const accelerate = new AsyncParallelBailHook(["newSpeed"]);

console.time("total time"); // 记录起始时间

accelerate.tapAsync("LoggerPlugin", (newSpeed, done) => {
  // 1秒后加速才完成
  setTimeout(() => {
    console.log("LoggerPlugin", `加速到${newSpeed}`);

    done();
  }, 1000);
});

accelerate.tapAsync("OverspeedPlugin", (newSpeed, done) => {
  // 2秒后检测是否超速
  setTimeout(() => {
    if (newSpeed > 120) {
      console.log("OverspeedPlugin", "您已超速!!");
    }

    // 这个任务的done返回一个错误
    // 注意第一个参数是node回调约定俗成的错误
    // 第二个参数才是Bail的返回值
    done(null, new Error("您已超速!!"));
  }, 2000);
});

accelerate.tapAsync("DamagePlugin", (newSpeed, done) => {
  // 3秒后检测是否损坏
  setTimeout(() => {
    if (newSpeed > 300) {
      console.log("DamagePlugin", "速度实在太快,车子快散架了。。。");
    }

    done();
  }, 3000);
});

accelerate.callAsync(500, (error, data) => {
  if (data) {
    console.log("任务执行出错:", data);
  } else {
    console.log("任务全部完成");
  }
  console.timeEnd("total time"); // 记录总共耗时
});

可以看到执行到任务2时,由于他返回了一个错误,所以最终的回调会立即执行,但是由于任务3之前已经同步开始了,所以他自己仍然会运行完,只是已经不影响最终结果了:

image-20210311142451224

AsyncSeriesHook

AsyncSeriesHook是异步串行hook,如果有多个任务,这多个任务之间是串行的,但是任务本身却可能是异步的,下一个任务必须等上一个任务done了才能开始:

const { AsyncSeriesHook } = require("tapable");

const accelerate = new AsyncSeriesHook(["newSpeed"]);

console.time("total time"); // 记录起始时间

accelerate.tapAsync("LoggerPlugin", (newSpeed, done) => {
  // 1秒后加速才完成
  setTimeout(() => {
    console.log("LoggerPlugin", `加速到${newSpeed}`);

    done();
  }, 1000);
});

accelerate.tapAsync("OverspeedPlugin", (newSpeed, done) => {
  // 2秒后检测是否超速
  setTimeout(() => {
    if (newSpeed > 120) {
      console.log("OverspeedPlugin", "您已超速!!");
    }
    done();
  }, 2000);
});

accelerate.tapAsync("DamagePlugin", (newSpeed, done) => {
  // 2秒后检测是否损坏
  setTimeout(() => {
    if (newSpeed > 300) {
      console.log("DamagePlugin", "速度实在太快,车子快散架了。。。");
    }

    done();
  }, 2000);
});

accelerate.callAsync(500, () => {
  console.log("任务全部完成");
  console.timeEnd("total time"); // 记录总共耗时
});

每个任务代码跟AsyncParallelHook是一样的,只是使用的Hook不一样,而最终效果的区别是:AsyncParallelHook所有任务同时开始,所以最终总耗时就是耗时最长的那个任务的耗时;AsyncSeriesHook的任务串行执行,下一个任务要等上一个任务完成了才能开始,所以最终总耗时是所有任务耗时的总和,上面这个例子就是1 + 2 + 2,也就是5秒:

image-20210311144738884

AsyncSeriesBailHook

AsyncSeriesBailHook就是在AsyncSeriesHook的基础上加上了Bail的逻辑,也就是中间任何一个任务返回不为undefined的值,终止执行,直接执行最后的回调,并且将这个返回值传给最终的回调:

const { AsyncSeriesBailHook } = require("tapable");

const accelerate = new AsyncSeriesBailHook(["newSpeed"]);

console.time("total time"); // 记录起始时间

accelerate.tapAsync("LoggerPlugin", (newSpeed, done) => {
  // 1秒后加速才完成
  setTimeout(() => {
    console.log("LoggerPlugin", `加速到${newSpeed}`);

    done();
  }, 1000);
});

accelerate.tapAsync("OverspeedPlugin", (newSpeed, done) => {
  // 2秒后检测是否超速
  setTimeout(() => {
    if (newSpeed > 120) {
      console.log("OverspeedPlugin", "您已超速!!");
    }

    // 这个任务的done返回一个错误
    // 注意第一个参数是node回调约定俗成的错误
    // 第二个参数才是Bail的返回值
    done(null, new Error("您已超速!!"));
  }, 2000);
});

accelerate.tapAsync("DamagePlugin", (newSpeed, done) => {
  // 2秒后检测是否损坏
  setTimeout(() => {
    if (newSpeed > 300) {
      console.log("DamagePlugin", "速度实在太快,车子快散架了。。。");
    }

    done();
  }, 2000);
});

accelerate.callAsync(500, (error, data) => {
  if (data) {
    console.log("任务执行出错:", data);
  } else {
    console.log("任务全部完成");
  }
  console.timeEnd("total time"); // 记录总共耗时
});

这个执行结果跟AsyncParallelBailHook的区别就是AsyncSeriesBailHook被阻断后,后面的任务由于还没开始,所以可以被完全阻断,而AsyncParallelBailHook后面的任务由于已经开始了,所以还会继续执行,只是结果已经不关心了。

image-20210311145241190

AsyncSeriesWaterfallHook

Waterfall的作用是将前一个任务的结果传给下一个任务,其他的跟AsyncSeriesHook一样的,直接来看代码吧:

const { AsyncSeriesWaterfallHook } = require("tapable");

const accelerate = new AsyncSeriesWaterfallHook(["newSpeed"]);

console.time("total time"); // 记录起始时间

accelerate.tapAsync("LoggerPlugin", (newSpeed, done) => {
  // 1秒后加速才完成
  setTimeout(() => {
    console.log("LoggerPlugin", `加速到${newSpeed}`);

    // 注意done的第一个参数会被当做error
    // 第二个参数才是传递给后面任务的参数
    done(null, "LoggerPlugin");
  }, 1000);
});

accelerate.tapAsync("Plugin2", (data, done) => {
  setTimeout(() => {
    console.log(`上一个插件是: ${data}`);

    done(null, "Plugin2");
  }, 2000);
});

accelerate.tapAsync("Plugin3", (data, done) => {
  setTimeout(() => {
    console.log(`上一个插件是: ${data}`);

    done(null, "Plugin3");
  }, 2000);
});

accelerate.callAsync(500, (error, data) => {
  console.log("最后一个插件是:", data);
  console.timeEnd("total time"); // 记录总共耗时
});

运行效果如下:

image-20210311150510851

总结

本文例子已经全部上传到GitHub,大家可以拿下来做个参考:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-usage

  1. tapablewebpack实现plugin的核心库,他为webpack提供了多种事件处理和流程控制的Hook
  2. 这些Hook主要有同步(Sync)和异步(Async)两种,同时还提供了阻断(Bail),瀑布(Waterfall),循环(Loop)等流程控制,对于异步流程还提供了并行(Paralle)和串行(Series)两种控制方式。
  3. tapable其核心原理还是事件的发布订阅模式,他使用tap来注册事件,使用call来触发事件。
  4. 异步hook支持两种写法:回调和Promise,注册和触发事件分别使用tapAsync/callAsynctapPromise/promise
  5. 异步hook使用回调写法的时候要注意,回调函数的第一个参数默认是错误,第二个参数才是向外传递的数据,这也符合node回调的风格。

这篇文章主要讲述了tapable的用法,后面我会写一篇文章来分析他的源码,点个关注不迷路,哈哈~

文章的最后,感谢你花费宝贵的时间阅读本文,如果本文给了你一点点帮助或者启发,请不要吝啬你的赞和GitHub小星星,你的支持是作者持续创作的动力。

欢迎关注我的公众号进击的大前端第一时间获取高质量原创~

“前端进阶知识”系列文章源码地址: https://github.com/dennis-jiang/Front-End-Knowledges

1270_300二维码_2.png

查看原文

我是好人 收藏了文章 · 3月20日

webpack核心模块tapable用法解析

前不久写了一篇webpack基本原理和AST用法的文章,本来想接着写webpack plugin的原理的,但是发现webpack plugin高度依赖tapable这个库,不清楚tapable而直接去看webpack plugin始终有点雾里看花的意思。所以就先去看了下tapable的文档和源码,发现这个库非常有意思,是增强版的发布订阅模式发布订阅模式在源码世界实在是太常见了,我们已经在多个库源码里面见过了:

  1. reduxsubscribedispatch
  2. Node.jsEventEmitter
  3. redux-sagatakeput

这些库基本都自己实现了自己的发布订阅模式,实现方式主要是用来满足自己的业务需求,而tapable并没有具体的业务逻辑,是一个专门用来实现事件订阅或者他自己称为hook(钩子)的工具库,其根本原理还是发布订阅模式,但是他实现了多种形式的发布订阅模式,还包含了多种形式的流程控制。

tapable暴露多个API,提供了多种流程控制方式,连使用都是比较复杂的,所以我想分两篇文章来写他的原理:

  1. 先看看用法,体验下他的多种流程控制方式
  2. 通过用法去看看源码是怎么实现的

本文就是讲用法的文章,知道了他的用法,大家以后如果有自己实现hook或者事件监听的需求,可以直接拿过来用,非常强大!

本文例子已经全部上传到GitHub,大家可以拿下来做个参考:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-usage

tapable是什么

tapablewebpack的核心模块,也是webpack团队维护的,是webpack plugin的基本实现方式。他的主要功能是为使用者提供强大的hook机制,webpack plugin就是基于hook的。

主要API

下面是官方文档中列出来的主要API,所有API的名字都是以Hook结尾的:

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

这些API的名字其实就解释了他的作用,注意这些关键字:Sync, Async, Bail, Waterfall, Loop, Parallel, Series。下面分别来解释下这些关键字:

Sync:这是一个同步的hook

Async:这是一个异步的hook

BailBail在英文中的意思是保险,保障的意思,实现的效果是,当一个hook注册了多个回调方法,任意一个回调方法返回了不为undefined的值,就不再执行后面的回调方法了,就起到了一个“保险丝”的作用。

WaterfallWaterfall在英语中是瀑布的意思,在编程世界中表示顺序执行各种任务,在这里实现的效果是,当一个hook注册了多个回调方法,前一个回调执行完了才会执行下一个回调,而前一个回调的执行结果会作为参数传给下一个回调函数。

LoopLoop就是循环的意思,实现的效果是,当一个hook注册了回调方法,如果这个回调方法返回了true就重复循环这个回调,只有当这个回调返回undefined才执行下一个回调。

ParallelParallel是并行的意思,有点类似于Promise.all,就是当一个hook注册了多个回调方法,这些回调同时开始并行执行。

SeriesSeries就是串行的意思,就是当一个hook注册了多个回调方法,前一个执行完了才会执行下一个。

ParallelSeries的概念只存在于异步的hook中,因为同步hook全部是串行的。

下面我们分别来介绍下每个API的用法和效果。

同步API

同步API就是这几个:

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

前面说了,同步API全部是串行的,所以这几个的区别就在流程控制上。

SyncHook

SyncHook是一个最基础的hook,其使用方法和效果接近我们经常使用的发布订阅模式,注意tapable导出的所有hook都是类,基本用法是这样的:

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

因为SyncHook是一个类,所以使用new来生成一个实例,构造函数接收的参数是一个数组["arg1", "arg2", "arg3"],这个数组有三项,表示生成的这个实例注册回调的时候接收三个参数。实例hook主要有两个实例方法:

  1. tap:就是注册事件回调的方法。
  2. call:就是触发事件,执行回调的方法。

下面我们扩展下官方文档中小汽车加速的例子来说明下具体用法:

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

// 实例化一个加速的hook
const accelerate = new SyncHook(["newSpeed"]);

// 注册第一个回调,加速时记录下当前速度
accelerate.tap("LoggerPlugin", (newSpeed) =>
  console.log("LoggerPlugin", `加速到${newSpeed}`)
);

// 再注册一个回调,用来检测是否超速
accelerate.tap("OverspeedPlugin", (newSpeed) => {
  if (newSpeed > 120) {
    console.log("OverspeedPlugin", "您已超速!!");
  }
});

// 再注册一个回调,用来检测速度是否快到损坏车子了
accelerate.tap("DamagePlugin", (newSpeed) => {
  if (newSpeed > 300) {
    console.log("DamagePlugin", "速度实在太快,车子快散架了。。。");
  }
});

// 触发一下加速事件,看看效果吧
accelerate.call(500);

然后运行下看看吧,当加速事件出现的时候,会依次执行这三个回调:

image-20210309160302799

上面这个例子主要就是用了tapcall这两个实例方法,其中tap接收两个参数,第一个是个字符串,并没有实际用处,仅仅是一个注释的作用,第二个参数就是一个回调函数,用来执行事件触发时的具体逻辑。

accelerate.tap("LoggerPlugin", (newSpeed) =>
  console.log("LoggerPlugin", `加速到${newSpeed}`)
);

上述这种写法其实与webpack官方文档中对于plugin的介绍非常像了,因为webpackplguin就是用tapable实现的,第一个参数一般就是plugin的名字:

image-20210309154641835

call就是简单的触发这个事件,在webpackplugin中一般不需要开发者去触发事件,而是webpack自己在不同阶段会触发不同的事件,比如beforeRun, run等等,plguin开发者更多的会关注这些事件出现时应该进行什么操作,也就是在这些事件上注册自己的回调。

SyncBailHook

上面的SyncHook其实就是一个简单的发布订阅模式SyncBailHook就是在这个基础上加了一点流程控制,前面我们说过了,Bail就是个保险,实现的效果是,前面一个回调返回一个不为undefined的值,就中断这个流程。比如我们现在将前面这个例子的SyncHook换成SyncBailHook,然后在检测超速的这个插件里面加点逻辑,当它超速了就返回错误,后面的DamagePlugin就不会执行了:

const { SyncBailHook } = require("tapable");    // 使用的是SyncBailHook

// 实例化一个加速的hook
const accelerate = new SyncBailHook(["newSpeed"]);

accelerate.tap("LoggerPlugin", (newSpeed) =>
  console.log("LoggerPlugin", `加速到${newSpeed}`)
);

// 再注册一个回调,用来检测是否超速
// 如果超速就返回一个错误
accelerate.tap("OverspeedPlugin", (newSpeed) => {
  if (newSpeed > 120) {
    console.log("OverspeedPlugin", "您已超速!!");

    return new Error('您已超速!!');
  }
});

accelerate.tap("DamagePlugin", (newSpeed) => {
  if (newSpeed > 300) {
    console.log("DamagePlugin", "速度实在太快,车子快散架了。。。");
  }
});

accelerate.call(500);

然后再运行下看看:

image-20210309161001682

可以看到由于OverspeedPlugin返回了一个不为undefined的值,DamagePlugin被阻断,没有运行了。

SyncWaterfallHook

SyncWaterfallHook也是在SyncHook的基础上加了点流程控制,前面说了,Waterfall实现的效果是将上一个回调的返回值作为参数传给下一个回调。所以通过call传入的参数只会传递给第一个回调函数,后面的回调接受都是上一个回调的返回值,最后一个回调的返回值会作为call的返回值返回给最外层:

const { SyncWaterfallHook } = require("tapable");

const accelerate = new SyncWaterfallHook(["newSpeed"]);

accelerate.tap("LoggerPlugin", (newSpeed) => {
  console.log("LoggerPlugin", `加速到${newSpeed}`);

  return "LoggerPlugin";
});

accelerate.tap("Plugin2", (data) => {
  console.log(`上一个插件是: ${data}`);

  return "Plugin2";
});

accelerate.tap("Plugin3", (data) => {
  console.log(`上一个插件是: ${data}`);

  return "Plugin3";
});

const lastPlugin = accelerate.call(100);

console.log(`最后一个插件是:${lastPlugin}`);

然后看下运行效果吧:

image-20210309162008465

SyncLoopHook

SyncLoopHook是在SyncHook的基础上添加了循环的逻辑,也就是如果一个插件返回true就会一直执行这个插件,直到他返回undefined才会执行下一个插件:

const { SyncLoopHook } = require("tapable");

const accelerate = new SyncLoopHook(["newSpeed"]);

accelerate.tap("LoopPlugin", (newSpeed) => {
  console.log("LoopPlugin", `循环加速到${newSpeed}`);

  return new Date().getTime() % 5 !== 0 ? true : undefined;
});

accelerate.tap("LastPlugin", (newSpeed) => {
  console.log("循环加速总算结束了");
});

accelerate.call(100);

执行效果如下:

image-20210309163514680

异步API

所谓异步API是相对前面的同步API来说的,前面的同步API的所有回调都是按照顺序同步执行的,每个回调内部也全部是同步代码。但是实际项目中,可能需要回调里面处理异步情况,也可能希望多个回调可以同时并行执行,也就是Parallel。这些需求就需要用到异步API了,主要的异步API就是这些:

const {
    AsyncParallelHook,
    AsyncParallelBailHook,
    AsyncSeriesHook,
    AsyncSeriesBailHook,
    AsyncSeriesWaterfallHook
 } = require("tapable");

既然涉及到了异步,那肯定还需要异步的处理方式,tapable支持回调函数和Promise两种异步的处理方式。所以这些异步API除了用前面的tap来注册回调外,还有两个注册回调的方法:tapAsynctapPromise,对应的触发事件的方法为callAsyncpromise。下面分别来看下每个API吧:

AsyncParallelHook

AsyncParallelHook从前面介绍的命名规则可以看出,他是一个异步并行执行的Hook,我们先用tapAsync的方式来看下怎么用吧。

tapAsync和callAsync

还是那个小汽车加速的例子,只不过这个小汽车加速没那么快了,需要一秒才能加速完成,然后我们在2秒的时候分别检测是否超速和是否损坏,为了看出并行的效果,我们记录下整个过程从开始到结束的时间:

const { AsyncParallelHook } = require("tapable");

const accelerate = new AsyncParallelHook(["newSpeed"]);

console.time("total time"); // 记录起始时间

// 注意注册异步事件需要使用tapAsync
// 接收的最后一个参数是done,调用他来表示当前任务执行完毕
accelerate.tapAsync("LoggerPlugin", (newSpeed, done) => {
  // 1秒后加速才完成
  setTimeout(() => {
    console.log("LoggerPlugin", `加速到${newSpeed}`);

    done();
  }, 1000);
});

accelerate.tapAsync("OverspeedPlugin", (newSpeed, done) => {
  // 2秒后检测是否超速
  setTimeout(() => {
    if (newSpeed > 120) {
      console.log("OverspeedPlugin", "您已超速!!");
    }
    done();
  }, 2000);
});

accelerate.tapAsync("DamagePlugin", (newSpeed, done) => {
  // 2秒后检测是否损坏
  setTimeout(() => {
    if (newSpeed > 300) {
      console.log("DamagePlugin", "速度实在太快,车子快散架了。。。");
    }

    done();
  }, 2000);
});

accelerate.callAsync(500, () => {
  console.log("任务全部完成");
  console.timeEnd("total time"); // 记录总共耗时
});

上面代码需要注意的是,注册回调要使用tapAsync,而且回调函数里面最后一个参数会自动传入done,你可以调用他来通知tapable当前任务已经完成。触发任务需要使用callAsync,他最后也接收一个函数,可以用来处理所有任务都完成后需要执行的操作。所以上面的运行结果就是:

image-20210309171527773

从这个结果可以看出,最终消耗的时间大概是2秒,也就是三个任务中最长的单个任务耗时,而不是三个任务耗时的总额,这就实现了Parallel并行的效果。

tapPromise和promise

现在都流行Promise,所以tapable也是支持的,执行效果是一样的,只是写法不一样而已。要用tapPromise,需要注册的回调返回一个promise,同时触发事件也需要用promise,任务运行完执行的处理可以直接使用then,所以上述代码改为:

const { AsyncParallelHook } = require("tapable");

const accelerate = new AsyncParallelHook(["newSpeed"]);

console.time("total time"); // 记录起始时间

// 注意注册异步事件需要使用tapPromise
// 回调函数要返回一个promise
accelerate.tapPromise("LoggerPlugin", (newSpeed) => {
  return new Promise((resolve) => {
    // 1秒后加速才完成
    setTimeout(() => {
      console.log("LoggerPlugin", `加速到${newSpeed}`);

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

accelerate.tapPromise("OverspeedPlugin", (newSpeed) => {
  return new Promise((resolve) => {
    // 2秒后检测是否超速
    setTimeout(() => {
      if (newSpeed > 120) {
        console.log("OverspeedPlugin", "您已超速!!");
      }
      resolve();
    }, 2000);
  });
});

accelerate.tapPromise("DamagePlugin", (newSpeed) => {
  return new Promise((resolve) => {
    // 2秒后检测是否损坏
    setTimeout(() => {
      if (newSpeed > 300) {
        console.log("DamagePlugin", "速度实在太快,车子快散架了。。。");
      }

      resolve();
    }, 2000);
  });
});

// 触发事件使用promise,直接用then处理最后的结果
accelerate.promise(500).then(() => {
  console.log("任务全部完成");
  console.timeEnd("total time"); // 记录总共耗时
});

这段代码的逻辑和运行结果和上面那个是一样的,只是写法不一样:

image-20210309172537951

tapAsync和tapPromise混用

既然tapable支持这两种异步写法,那这两种写法可以混用吗?我们来试试吧:

const { AsyncParallelHook } = require("tapable");

const accelerate = new AsyncParallelHook(["newSpeed"]);

console.time("total time"); // 记录起始时间

// 来一个promise写法
accelerate.tapPromise("LoggerPlugin", (newSpeed) => {
  return new Promise((resolve) => {
    // 1秒后加速才完成
    setTimeout(() => {
      console.log("LoggerPlugin", `加速到${newSpeed}`);

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

// 再来一个async写法
accelerate.tapAsync("OverspeedPlugin", (newSpeed, done) => {
  // 2秒后检测是否超速
  setTimeout(() => {
    if (newSpeed > 120) {
      console.log("OverspeedPlugin", "您已超速!!");
    }
    done();
  }, 2000);
});

// 使用promise触发事件
// accelerate.promise(500).then(() => {
//   console.log("任务全部完成");
//   console.timeEnd("total time"); // 记录总共耗时
// });

// 使用callAsync触发事件
accelerate.callAsync(500, () => {
  console.log("任务全部完成");
  console.timeEnd("total time"); // 记录总共耗时
});

这段代码无论我是使用promise触发事件还是callAsync触发运行的结果都是一样的,所以tapable内部应该是做了兼容转换的,两种写法可以混用:

image-20210309173217034

由于tapAsynctapPromise只是写法上的不一样,我后面的例子就全部用tapAsync了。

AsyncParallelBailHook

前面已经看了SyncBailHook,知道带Bail的功能就是当一个任务返回不为undefined的时候,阻断后面任务的执行。但是由于Parallel任务都是同时开始的,阻断是阻断不了了,实际效果是如果有一个任务返回了不为undefined的值,最终的回调会立即执行,并且获取Bail任务的返回值。我们将上面三个任务执行时间错开,分别为1秒,2秒,3秒,然后在2秒的任务触发Bail就能看到效果了:

const { AsyncParallelBailHook } = require("tapable");

const accelerate = new AsyncParallelBailHook(["newSpeed"]);

console.time("total time"); // 记录起始时间

accelerate.tapAsync("LoggerPlugin", (newSpeed, done) => {
  // 1秒后加速才完成
  setTimeout(() => {
    console.log("LoggerPlugin", `加速到${newSpeed}`);

    done();
  }, 1000);
});

accelerate.tapAsync("OverspeedPlugin", (newSpeed, done) => {
  // 2秒后检测是否超速
  setTimeout(() => {
    if (newSpeed > 120) {
      console.log("OverspeedPlugin", "您已超速!!");
    }

    // 这个任务的done返回一个错误
    // 注意第一个参数是node回调约定俗成的错误
    // 第二个参数才是Bail的返回值
    done(null, new Error("您已超速!!"));
  }, 2000);
});

accelerate.tapAsync("DamagePlugin", (newSpeed, done) => {
  // 3秒后检测是否损坏
  setTimeout(() => {
    if (newSpeed > 300) {
      console.log("DamagePlugin", "速度实在太快,车子快散架了。。。");
    }

    done();
  }, 3000);
});

accelerate.callAsync(500, (error, data) => {
  if (data) {
    console.log("任务执行出错:", data);
  } else {
    console.log("任务全部完成");
  }
  console.timeEnd("total time"); // 记录总共耗时
});

可以看到执行到任务2时,由于他返回了一个错误,所以最终的回调会立即执行,但是由于任务3之前已经同步开始了,所以他自己仍然会运行完,只是已经不影响最终结果了:

image-20210311142451224

AsyncSeriesHook

AsyncSeriesHook是异步串行hook,如果有多个任务,这多个任务之间是串行的,但是任务本身却可能是异步的,下一个任务必须等上一个任务done了才能开始:

const { AsyncSeriesHook } = require("tapable");

const accelerate = new AsyncSeriesHook(["newSpeed"]);

console.time("total time"); // 记录起始时间

accelerate.tapAsync("LoggerPlugin", (newSpeed, done) => {
  // 1秒后加速才完成
  setTimeout(() => {
    console.log("LoggerPlugin", `加速到${newSpeed}`);

    done();
  }, 1000);
});

accelerate.tapAsync("OverspeedPlugin", (newSpeed, done) => {
  // 2秒后检测是否超速
  setTimeout(() => {
    if (newSpeed > 120) {
      console.log("OverspeedPlugin", "您已超速!!");
    }
    done();
  }, 2000);
});

accelerate.tapAsync("DamagePlugin", (newSpeed, done) => {
  // 2秒后检测是否损坏
  setTimeout(() => {
    if (newSpeed > 300) {
      console.log("DamagePlugin", "速度实在太快,车子快散架了。。。");
    }

    done();
  }, 2000);
});

accelerate.callAsync(500, () => {
  console.log("任务全部完成");
  console.timeEnd("total time"); // 记录总共耗时
});

每个任务代码跟AsyncParallelHook是一样的,只是使用的Hook不一样,而最终效果的区别是:AsyncParallelHook所有任务同时开始,所以最终总耗时就是耗时最长的那个任务的耗时;AsyncSeriesHook的任务串行执行,下一个任务要等上一个任务完成了才能开始,所以最终总耗时是所有任务耗时的总和,上面这个例子就是1 + 2 + 2,也就是5秒:

image-20210311144738884

AsyncSeriesBailHook

AsyncSeriesBailHook就是在AsyncSeriesHook的基础上加上了Bail的逻辑,也就是中间任何一个任务返回不为undefined的值,终止执行,直接执行最后的回调,并且将这个返回值传给最终的回调:

const { AsyncSeriesBailHook } = require("tapable");

const accelerate = new AsyncSeriesBailHook(["newSpeed"]);

console.time("total time"); // 记录起始时间

accelerate.tapAsync("LoggerPlugin", (newSpeed, done) => {
  // 1秒后加速才完成
  setTimeout(() => {
    console.log("LoggerPlugin", `加速到${newSpeed}`);

    done();
  }, 1000);
});

accelerate.tapAsync("OverspeedPlugin", (newSpeed, done) => {
  // 2秒后检测是否超速
  setTimeout(() => {
    if (newSpeed > 120) {
      console.log("OverspeedPlugin", "您已超速!!");
    }

    // 这个任务的done返回一个错误
    // 注意第一个参数是node回调约定俗成的错误
    // 第二个参数才是Bail的返回值
    done(null, new Error("您已超速!!"));
  }, 2000);
});

accelerate.tapAsync("DamagePlugin", (newSpeed, done) => {
  // 2秒后检测是否损坏
  setTimeout(() => {
    if (newSpeed > 300) {
      console.log("DamagePlugin", "速度实在太快,车子快散架了。。。");
    }

    done();
  }, 2000);
});

accelerate.callAsync(500, (error, data) => {
  if (data) {
    console.log("任务执行出错:", data);
  } else {
    console.log("任务全部完成");
  }
  console.timeEnd("total time"); // 记录总共耗时
});

这个执行结果跟AsyncParallelBailHook的区别就是AsyncSeriesBailHook被阻断后,后面的任务由于还没开始,所以可以被完全阻断,而AsyncParallelBailHook后面的任务由于已经开始了,所以还会继续执行,只是结果已经不关心了。

image-20210311145241190

AsyncSeriesWaterfallHook

Waterfall的作用是将前一个任务的结果传给下一个任务,其他的跟AsyncSeriesHook一样的,直接来看代码吧:

const { AsyncSeriesWaterfallHook } = require("tapable");

const accelerate = new AsyncSeriesWaterfallHook(["newSpeed"]);

console.time("total time"); // 记录起始时间

accelerate.tapAsync("LoggerPlugin", (newSpeed, done) => {
  // 1秒后加速才完成
  setTimeout(() => {
    console.log("LoggerPlugin", `加速到${newSpeed}`);

    // 注意done的第一个参数会被当做error
    // 第二个参数才是传递给后面任务的参数
    done(null, "LoggerPlugin");
  }, 1000);
});

accelerate.tapAsync("Plugin2", (data, done) => {
  setTimeout(() => {
    console.log(`上一个插件是: ${data}`);

    done(null, "Plugin2");
  }, 2000);
});

accelerate.tapAsync("Plugin3", (data, done) => {
  setTimeout(() => {
    console.log(`上一个插件是: ${data}`);

    done(null, "Plugin3");
  }, 2000);
});

accelerate.callAsync(500, (error, data) => {
  console.log("最后一个插件是:", data);
  console.timeEnd("total time"); // 记录总共耗时
});

运行效果如下:

image-20210311150510851

总结

本文例子已经全部上传到GitHub,大家可以拿下来做个参考:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-usage

  1. tapablewebpack实现plugin的核心库,他为webpack提供了多种事件处理和流程控制的Hook
  2. 这些Hook主要有同步(Sync)和异步(Async)两种,同时还提供了阻断(Bail),瀑布(Waterfall),循环(Loop)等流程控制,对于异步流程还提供了并行(Paralle)和串行(Series)两种控制方式。
  3. tapable其核心原理还是事件的发布订阅模式,他使用tap来注册事件,使用call来触发事件。
  4. 异步hook支持两种写法:回调和Promise,注册和触发事件分别使用tapAsync/callAsynctapPromise/promise
  5. 异步hook使用回调写法的时候要注意,回调函数的第一个参数默认是错误,第二个参数才是向外传递的数据,这也符合node回调的风格。

这篇文章主要讲述了tapable的用法,后面我会写一篇文章来分析他的源码,点个关注不迷路,哈哈~

文章的最后,感谢你花费宝贵的时间阅读本文,如果本文给了你一点点帮助或者启发,请不要吝啬你的赞和GitHub小星星,你的支持是作者持续创作的动力。

欢迎关注我的公众号进击的大前端第一时间获取高质量原创~

“前端进阶知识”系列文章源码地址: https://github.com/dennis-jiang/Front-End-Knowledges

1270_300二维码_2.png

查看原文

我是好人 赞了文章 · 3月20日

webpack核心模块tapable用法解析

前不久写了一篇webpack基本原理和AST用法的文章,本来想接着写webpack plugin的原理的,但是发现webpack plugin高度依赖tapable这个库,不清楚tapable而直接去看webpack plugin始终有点雾里看花的意思。所以就先去看了下tapable的文档和源码,发现这个库非常有意思,是增强版的发布订阅模式发布订阅模式在源码世界实在是太常见了,我们已经在多个库源码里面见过了:

  1. reduxsubscribedispatch
  2. Node.jsEventEmitter
  3. redux-sagatakeput

这些库基本都自己实现了自己的发布订阅模式,实现方式主要是用来满足自己的业务需求,而tapable并没有具体的业务逻辑,是一个专门用来实现事件订阅或者他自己称为hook(钩子)的工具库,其根本原理还是发布订阅模式,但是他实现了多种形式的发布订阅模式,还包含了多种形式的流程控制。

tapable暴露多个API,提供了多种流程控制方式,连使用都是比较复杂的,所以我想分两篇文章来写他的原理:

  1. 先看看用法,体验下他的多种流程控制方式
  2. 通过用法去看看源码是怎么实现的

本文就是讲用法的文章,知道了他的用法,大家以后如果有自己实现hook或者事件监听的需求,可以直接拿过来用,非常强大!

本文例子已经全部上传到GitHub,大家可以拿下来做个参考:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-usage

tapable是什么

tapablewebpack的核心模块,也是webpack团队维护的,是webpack plugin的基本实现方式。他的主要功能是为使用者提供强大的hook机制,webpack plugin就是基于hook的。

主要API

下面是官方文档中列出来的主要API,所有API的名字都是以Hook结尾的:

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

这些API的名字其实就解释了他的作用,注意这些关键字:Sync, Async, Bail, Waterfall, Loop, Parallel, Series。下面分别来解释下这些关键字:

Sync:这是一个同步的hook

Async:这是一个异步的hook

BailBail在英文中的意思是保险,保障的意思,实现的效果是,当一个hook注册了多个回调方法,任意一个回调方法返回了不为undefined的值,就不再执行后面的回调方法了,就起到了一个“保险丝”的作用。

WaterfallWaterfall在英语中是瀑布的意思,在编程世界中表示顺序执行各种任务,在这里实现的效果是,当一个hook注册了多个回调方法,前一个回调执行完了才会执行下一个回调,而前一个回调的执行结果会作为参数传给下一个回调函数。

LoopLoop就是循环的意思,实现的效果是,当一个hook注册了回调方法,如果这个回调方法返回了true就重复循环这个回调,只有当这个回调返回undefined才执行下一个回调。

ParallelParallel是并行的意思,有点类似于Promise.all,就是当一个hook注册了多个回调方法,这些回调同时开始并行执行。

SeriesSeries就是串行的意思,就是当一个hook注册了多个回调方法,前一个执行完了才会执行下一个。

ParallelSeries的概念只存在于异步的hook中,因为同步hook全部是串行的。

下面我们分别来介绍下每个API的用法和效果。

同步API

同步API就是这几个:

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

前面说了,同步API全部是串行的,所以这几个的区别就在流程控制上。

SyncHook

SyncHook是一个最基础的hook,其使用方法和效果接近我们经常使用的发布订阅模式,注意tapable导出的所有hook都是类,基本用法是这样的:

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

因为SyncHook是一个类,所以使用new来生成一个实例,构造函数接收的参数是一个数组["arg1", "arg2", "arg3"],这个数组有三项,表示生成的这个实例注册回调的时候接收三个参数。实例hook主要有两个实例方法:

  1. tap:就是注册事件回调的方法。
  2. call:就是触发事件,执行回调的方法。

下面我们扩展下官方文档中小汽车加速的例子来说明下具体用法:

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

// 实例化一个加速的hook
const accelerate = new SyncHook(["newSpeed"]);

// 注册第一个回调,加速时记录下当前速度
accelerate.tap("LoggerPlugin", (newSpeed) =>
  console.log("LoggerPlugin", `加速到${newSpeed}`)
);

// 再注册一个回调,用来检测是否超速
accelerate.tap("OverspeedPlugin", (newSpeed) => {
  if (newSpeed > 120) {
    console.log("OverspeedPlugin", "您已超速!!");
  }
});

// 再注册一个回调,用来检测速度是否快到损坏车子了
accelerate.tap("DamagePlugin", (newSpeed) => {
  if (newSpeed > 300) {
    console.log("DamagePlugin", "速度实在太快,车子快散架了。。。");
  }
});

// 触发一下加速事件,看看效果吧
accelerate.call(500);

然后运行下看看吧,当加速事件出现的时候,会依次执行这三个回调:

image-20210309160302799

上面这个例子主要就是用了tapcall这两个实例方法,其中tap接收两个参数,第一个是个字符串,并没有实际用处,仅仅是一个注释的作用,第二个参数就是一个回调函数,用来执行事件触发时的具体逻辑。

accelerate.tap("LoggerPlugin", (newSpeed) =>
  console.log("LoggerPlugin", `加速到${newSpeed}`)
);

上述这种写法其实与webpack官方文档中对于plugin的介绍非常像了,因为webpackplguin就是用tapable实现的,第一个参数一般就是plugin的名字:

image-20210309154641835

call就是简单的触发这个事件,在webpackplugin中一般不需要开发者去触发事件,而是webpack自己在不同阶段会触发不同的事件,比如beforeRun, run等等,plguin开发者更多的会关注这些事件出现时应该进行什么操作,也就是在这些事件上注册自己的回调。

SyncBailHook

上面的SyncHook其实就是一个简单的发布订阅模式SyncBailHook就是在这个基础上加了一点流程控制,前面我们说过了,Bail就是个保险,实现的效果是,前面一个回调返回一个不为undefined的值,就中断这个流程。比如我们现在将前面这个例子的SyncHook换成SyncBailHook,然后在检测超速的这个插件里面加点逻辑,当它超速了就返回错误,后面的DamagePlugin就不会执行了:

const { SyncBailHook } = require("tapable");    // 使用的是SyncBailHook

// 实例化一个加速的hook
const accelerate = new SyncBailHook(["newSpeed"]);

accelerate.tap("LoggerPlugin", (newSpeed) =>
  console.log("LoggerPlugin", `加速到${newSpeed}`)
);

// 再注册一个回调,用来检测是否超速
// 如果超速就返回一个错误
accelerate.tap("OverspeedPlugin", (newSpeed) => {
  if (newSpeed > 120) {
    console.log("OverspeedPlugin", "您已超速!!");

    return new Error('您已超速!!');
  }
});

accelerate.tap("DamagePlugin", (newSpeed) => {
  if (newSpeed > 300) {
    console.log("DamagePlugin", "速度实在太快,车子快散架了。。。");
  }
});

accelerate.call(500);

然后再运行下看看:

image-20210309161001682

可以看到由于OverspeedPlugin返回了一个不为undefined的值,DamagePlugin被阻断,没有运行了。

SyncWaterfallHook

SyncWaterfallHook也是在SyncHook的基础上加了点流程控制,前面说了,Waterfall实现的效果是将上一个回调的返回值作为参数传给下一个回调。所以通过call传入的参数只会传递给第一个回调函数,后面的回调接受都是上一个回调的返回值,最后一个回调的返回值会作为call的返回值返回给最外层:

const { SyncWaterfallHook } = require("tapable");

const accelerate = new SyncWaterfallHook(["newSpeed"]);

accelerate.tap("LoggerPlugin", (newSpeed) => {
  console.log("LoggerPlugin", `加速到${newSpeed}`);

  return "LoggerPlugin";
});

accelerate.tap("Plugin2", (data) => {
  console.log(`上一个插件是: ${data}`);

  return "Plugin2";
});

accelerate.tap("Plugin3", (data) => {
  console.log(`上一个插件是: ${data}`);

  return "Plugin3";
});

const lastPlugin = accelerate.call(100);

console.log(`最后一个插件是:${lastPlugin}`);

然后看下运行效果吧:

image-20210309162008465

SyncLoopHook

SyncLoopHook是在SyncHook的基础上添加了循环的逻辑,也就是如果一个插件返回true就会一直执行这个插件,直到他返回undefined才会执行下一个插件:

const { SyncLoopHook } = require("tapable");

const accelerate = new SyncLoopHook(["newSpeed"]);

accelerate.tap("LoopPlugin", (newSpeed) => {
  console.log("LoopPlugin", `循环加速到${newSpeed}`);

  return new Date().getTime() % 5 !== 0 ? true : undefined;
});

accelerate.tap("LastPlugin", (newSpeed) => {
  console.log("循环加速总算结束了");
});

accelerate.call(100);

执行效果如下:

image-20210309163514680

异步API

所谓异步API是相对前面的同步API来说的,前面的同步API的所有回调都是按照顺序同步执行的,每个回调内部也全部是同步代码。但是实际项目中,可能需要回调里面处理异步情况,也可能希望多个回调可以同时并行执行,也就是Parallel。这些需求就需要用到异步API了,主要的异步API就是这些:

const {
    AsyncParallelHook,
    AsyncParallelBailHook,
    AsyncSeriesHook,
    AsyncSeriesBailHook,
    AsyncSeriesWaterfallHook
 } = require("tapable");

既然涉及到了异步,那肯定还需要异步的处理方式,tapable支持回调函数和Promise两种异步的处理方式。所以这些异步API除了用前面的tap来注册回调外,还有两个注册回调的方法:tapAsynctapPromise,对应的触发事件的方法为callAsyncpromise。下面分别来看下每个API吧:

AsyncParallelHook

AsyncParallelHook从前面介绍的命名规则可以看出,他是一个异步并行执行的Hook,我们先用tapAsync的方式来看下怎么用吧。

tapAsync和callAsync

还是那个小汽车加速的例子,只不过这个小汽车加速没那么快了,需要一秒才能加速完成,然后我们在2秒的时候分别检测是否超速和是否损坏,为了看出并行的效果,我们记录下整个过程从开始到结束的时间:

const { AsyncParallelHook } = require("tapable");

const accelerate = new AsyncParallelHook(["newSpeed"]);

console.time("total time"); // 记录起始时间

// 注意注册异步事件需要使用tapAsync
// 接收的最后一个参数是done,调用他来表示当前任务执行完毕
accelerate.tapAsync("LoggerPlugin", (newSpeed, done) => {
  // 1秒后加速才完成
  setTimeout(() => {
    console.log("LoggerPlugin", `加速到${newSpeed}`);

    done();
  }, 1000);
});

accelerate.tapAsync("OverspeedPlugin", (newSpeed, done) => {
  // 2秒后检测是否超速
  setTimeout(() => {
    if (newSpeed > 120) {
      console.log("OverspeedPlugin", "您已超速!!");
    }
    done();
  }, 2000);
});

accelerate.tapAsync("DamagePlugin", (newSpeed, done) => {
  // 2秒后检测是否损坏
  setTimeout(() => {
    if (newSpeed > 300) {
      console.log("DamagePlugin", "速度实在太快,车子快散架了。。。");
    }

    done();
  }, 2000);
});

accelerate.callAsync(500, () => {
  console.log("任务全部完成");
  console.timeEnd("total time"); // 记录总共耗时
});

上面代码需要注意的是,注册回调要使用tapAsync,而且回调函数里面最后一个参数会自动传入done,你可以调用他来通知tapable当前任务已经完成。触发任务需要使用callAsync,他最后也接收一个函数,可以用来处理所有任务都完成后需要执行的操作。所以上面的运行结果就是:

image-20210309171527773

从这个结果可以看出,最终消耗的时间大概是2秒,也就是三个任务中最长的单个任务耗时,而不是三个任务耗时的总额,这就实现了Parallel并行的效果。

tapPromise和promise

现在都流行Promise,所以tapable也是支持的,执行效果是一样的,只是写法不一样而已。要用tapPromise,需要注册的回调返回一个promise,同时触发事件也需要用promise,任务运行完执行的处理可以直接使用then,所以上述代码改为:

const { AsyncParallelHook } = require("tapable");

const accelerate = new AsyncParallelHook(["newSpeed"]);

console.time("total time"); // 记录起始时间

// 注意注册异步事件需要使用tapPromise
// 回调函数要返回一个promise
accelerate.tapPromise("LoggerPlugin", (newSpeed) => {
  return new Promise((resolve) => {
    // 1秒后加速才完成
    setTimeout(() => {
      console.log("LoggerPlugin", `加速到${newSpeed}`);

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

accelerate.tapPromise("OverspeedPlugin", (newSpeed) => {
  return new Promise((resolve) => {
    // 2秒后检测是否超速
    setTimeout(() => {
      if (newSpeed > 120) {
        console.log("OverspeedPlugin", "您已超速!!");
      }
      resolve();
    }, 2000);
  });
});

accelerate.tapPromise("DamagePlugin", (newSpeed) => {
  return new Promise((resolve) => {
    // 2秒后检测是否损坏
    setTimeout(() => {
      if (newSpeed > 300) {
        console.log("DamagePlugin", "速度实在太快,车子快散架了。。。");
      }

      resolve();
    }, 2000);
  });
});

// 触发事件使用promise,直接用then处理最后的结果
accelerate.promise(500).then(() => {
  console.log("任务全部完成");
  console.timeEnd("total time"); // 记录总共耗时
});

这段代码的逻辑和运行结果和上面那个是一样的,只是写法不一样:

image-20210309172537951

tapAsync和tapPromise混用

既然tapable支持这两种异步写法,那这两种写法可以混用吗?我们来试试吧:

const { AsyncParallelHook } = require("tapable");

const accelerate = new AsyncParallelHook(["newSpeed"]);

console.time("total time"); // 记录起始时间

// 来一个promise写法
accelerate.tapPromise("LoggerPlugin", (newSpeed) => {
  return new Promise((resolve) => {
    // 1秒后加速才完成
    setTimeout(() => {
      console.log("LoggerPlugin", `加速到${newSpeed}`);

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

// 再来一个async写法
accelerate.tapAsync("OverspeedPlugin", (newSpeed, done) => {
  // 2秒后检测是否超速
  setTimeout(() => {
    if (newSpeed > 120) {
      console.log("OverspeedPlugin", "您已超速!!");
    }
    done();
  }, 2000);
});

// 使用promise触发事件
// accelerate.promise(500).then(() => {
//   console.log("任务全部完成");
//   console.timeEnd("total time"); // 记录总共耗时
// });

// 使用callAsync触发事件
accelerate.callAsync(500, () => {
  console.log("任务全部完成");
  console.timeEnd("total time"); // 记录总共耗时
});

这段代码无论我是使用promise触发事件还是callAsync触发运行的结果都是一样的,所以tapable内部应该是做了兼容转换的,两种写法可以混用:

image-20210309173217034

由于tapAsynctapPromise只是写法上的不一样,我后面的例子就全部用tapAsync了。

AsyncParallelBailHook

前面已经看了SyncBailHook,知道带Bail的功能就是当一个任务返回不为undefined的时候,阻断后面任务的执行。但是由于Parallel任务都是同时开始的,阻断是阻断不了了,实际效果是如果有一个任务返回了不为undefined的值,最终的回调会立即执行,并且获取Bail任务的返回值。我们将上面三个任务执行时间错开,分别为1秒,2秒,3秒,然后在2秒的任务触发Bail就能看到效果了:

const { AsyncParallelBailHook } = require("tapable");

const accelerate = new AsyncParallelBailHook(["newSpeed"]);

console.time("total time"); // 记录起始时间

accelerate.tapAsync("LoggerPlugin", (newSpeed, done) => {
  // 1秒后加速才完成
  setTimeout(() => {
    console.log("LoggerPlugin", `加速到${newSpeed}`);

    done();
  }, 1000);
});

accelerate.tapAsync("OverspeedPlugin", (newSpeed, done) => {
  // 2秒后检测是否超速
  setTimeout(() => {
    if (newSpeed > 120) {
      console.log("OverspeedPlugin", "您已超速!!");
    }

    // 这个任务的done返回一个错误
    // 注意第一个参数是node回调约定俗成的错误
    // 第二个参数才是Bail的返回值
    done(null, new Error("您已超速!!"));
  }, 2000);
});

accelerate.tapAsync("DamagePlugin", (newSpeed, done) => {
  // 3秒后检测是否损坏
  setTimeout(() => {
    if (newSpeed > 300) {
      console.log("DamagePlugin", "速度实在太快,车子快散架了。。。");
    }

    done();
  }, 3000);
});

accelerate.callAsync(500, (error, data) => {
  if (data) {
    console.log("任务执行出错:", data);
  } else {
    console.log("任务全部完成");
  }
  console.timeEnd("total time"); // 记录总共耗时
});

可以看到执行到任务2时,由于他返回了一个错误,所以最终的回调会立即执行,但是由于任务3之前已经同步开始了,所以他自己仍然会运行完,只是已经不影响最终结果了:

image-20210311142451224

AsyncSeriesHook

AsyncSeriesHook是异步串行hook,如果有多个任务,这多个任务之间是串行的,但是任务本身却可能是异步的,下一个任务必须等上一个任务done了才能开始:

const { AsyncSeriesHook } = require("tapable");

const accelerate = new AsyncSeriesHook(["newSpeed"]);

console.time("total time"); // 记录起始时间

accelerate.tapAsync("LoggerPlugin", (newSpeed, done) => {
  // 1秒后加速才完成
  setTimeout(() => {
    console.log("LoggerPlugin", `加速到${newSpeed}`);

    done();
  }, 1000);
});

accelerate.tapAsync("OverspeedPlugin", (newSpeed, done) => {
  // 2秒后检测是否超速
  setTimeout(() => {
    if (newSpeed > 120) {
      console.log("OverspeedPlugin", "您已超速!!");
    }
    done();
  }, 2000);
});

accelerate.tapAsync("DamagePlugin", (newSpeed, done) => {
  // 2秒后检测是否损坏
  setTimeout(() => {
    if (newSpeed > 300) {
      console.log("DamagePlugin", "速度实在太快,车子快散架了。。。");
    }

    done();
  }, 2000);
});

accelerate.callAsync(500, () => {
  console.log("任务全部完成");
  console.timeEnd("total time"); // 记录总共耗时
});

每个任务代码跟AsyncParallelHook是一样的,只是使用的Hook不一样,而最终效果的区别是:AsyncParallelHook所有任务同时开始,所以最终总耗时就是耗时最长的那个任务的耗时;AsyncSeriesHook的任务串行执行,下一个任务要等上一个任务完成了才能开始,所以最终总耗时是所有任务耗时的总和,上面这个例子就是1 + 2 + 2,也就是5秒:

image-20210311144738884

AsyncSeriesBailHook

AsyncSeriesBailHook就是在AsyncSeriesHook的基础上加上了Bail的逻辑,也就是中间任何一个任务返回不为undefined的值,终止执行,直接执行最后的回调,并且将这个返回值传给最终的回调:

const { AsyncSeriesBailHook } = require("tapable");

const accelerate = new AsyncSeriesBailHook(["newSpeed"]);

console.time("total time"); // 记录起始时间

accelerate.tapAsync("LoggerPlugin", (newSpeed, done) => {
  // 1秒后加速才完成
  setTimeout(() => {
    console.log("LoggerPlugin", `加速到${newSpeed}`);

    done();
  }, 1000);
});

accelerate.tapAsync("OverspeedPlugin", (newSpeed, done) => {
  // 2秒后检测是否超速
  setTimeout(() => {
    if (newSpeed > 120) {
      console.log("OverspeedPlugin", "您已超速!!");
    }

    // 这个任务的done返回一个错误
    // 注意第一个参数是node回调约定俗成的错误
    // 第二个参数才是Bail的返回值
    done(null, new Error("您已超速!!"));
  }, 2000);
});

accelerate.tapAsync("DamagePlugin", (newSpeed, done) => {
  // 2秒后检测是否损坏
  setTimeout(() => {
    if (newSpeed > 300) {
      console.log("DamagePlugin", "速度实在太快,车子快散架了。。。");
    }

    done();
  }, 2000);
});

accelerate.callAsync(500, (error, data) => {
  if (data) {
    console.log("任务执行出错:", data);
  } else {
    console.log("任务全部完成");
  }
  console.timeEnd("total time"); // 记录总共耗时
});

这个执行结果跟AsyncParallelBailHook的区别就是AsyncSeriesBailHook被阻断后,后面的任务由于还没开始,所以可以被完全阻断,而AsyncParallelBailHook后面的任务由于已经开始了,所以还会继续执行,只是结果已经不关心了。

image-20210311145241190

AsyncSeriesWaterfallHook

Waterfall的作用是将前一个任务的结果传给下一个任务,其他的跟AsyncSeriesHook一样的,直接来看代码吧:

const { AsyncSeriesWaterfallHook } = require("tapable");

const accelerate = new AsyncSeriesWaterfallHook(["newSpeed"]);

console.time("total time"); // 记录起始时间

accelerate.tapAsync("LoggerPlugin", (newSpeed, done) => {
  // 1秒后加速才完成
  setTimeout(() => {
    console.log("LoggerPlugin", `加速到${newSpeed}`);

    // 注意done的第一个参数会被当做error
    // 第二个参数才是传递给后面任务的参数
    done(null, "LoggerPlugin");
  }, 1000);
});

accelerate.tapAsync("Plugin2", (data, done) => {
  setTimeout(() => {
    console.log(`上一个插件是: ${data}`);

    done(null, "Plugin2");
  }, 2000);
});

accelerate.tapAsync("Plugin3", (data, done) => {
  setTimeout(() => {
    console.log(`上一个插件是: ${data}`);

    done(null, "Plugin3");
  }, 2000);
});

accelerate.callAsync(500, (error, data) => {
  console.log("最后一个插件是:", data);
  console.timeEnd("total time"); // 记录总共耗时
});

运行效果如下:

image-20210311150510851

总结

本文例子已经全部上传到GitHub,大家可以拿下来做个参考:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-usage

  1. tapablewebpack实现plugin的核心库,他为webpack提供了多种事件处理和流程控制的Hook
  2. 这些Hook主要有同步(Sync)和异步(Async)两种,同时还提供了阻断(Bail),瀑布(Waterfall),循环(Loop)等流程控制,对于异步流程还提供了并行(Paralle)和串行(Series)两种控制方式。
  3. tapable其核心原理还是事件的发布订阅模式,他使用tap来注册事件,使用call来触发事件。
  4. 异步hook支持两种写法:回调和Promise,注册和触发事件分别使用tapAsync/callAsynctapPromise/promise
  5. 异步hook使用回调写法的时候要注意,回调函数的第一个参数默认是错误,第二个参数才是向外传递的数据,这也符合node回调的风格。

这篇文章主要讲述了tapable的用法,后面我会写一篇文章来分析他的源码,点个关注不迷路,哈哈~

文章的最后,感谢你花费宝贵的时间阅读本文,如果本文给了你一点点帮助或者启发,请不要吝啬你的赞和GitHub小星星,你的支持是作者持续创作的动力。

欢迎关注我的公众号进击的大前端第一时间获取高质量原创~

“前端进阶知识”系列文章源码地址: https://github.com/dennis-jiang/Front-End-Knowledges

1270_300二维码_2.png

查看原文

赞 16 收藏 11 评论 0

我是好人 回答了问题 · 2月21日

Vue-i18n国际化js维护方案

之前写过一个工具,可以比较直观的维护messages,可以参考这篇文章

关注 4 回答 3

我是好人 发布了文章 · 1月21日

[Vue]多重文字描边组件

效果图

效果

安装

yarn add vue-stroke-text
npm i vue-stroke-text

引入

import StrokeText from 'vue-stroke-text'

// 全局注册
Vue.component(StrokeText.name,StrokeText)

// 或者页面内注册
export default {
    components:{
        StrokeText,
    }
}

使用

<template>
    <stroke-text class="my-stroke-text" text="测试文字" :strokes="strokes" />
</template>
<script>
export default {
    data: () => ({
        // 这里按照数组顺序直接设置每一层的描边,务必按照描边宽度从小到大来设置。
        // 值的写法就是 -webkit-text-stroke 属性的写法
        strokes: [
            '0.2em red',
            '0.4em green',
            '0.6em black',
        ]
    })
}
</script>
<style>
.my-stroke-text {
    font-size:24px;
}
</style>

项目地址

github
npm

查看原文

赞 0 收藏 0 评论 0

我是好人 赞了回答 · 2020-10-16

前端通过将URL指向下载地址下载文件如何知道什么时候下载成功?

没办法哈哈。但是你可以利用缓存!

1.通过ajax请求,这是可以监听进度的。

2.ajax成功后,打开:window.location.href

axios({
    url: 'download'
}).then(() => {
    window.location.href = 'download';
});

关注 6 回答 6

我是好人 回答了问题 · 2020-06-29

(Javascript)如何判断鼠标是否落在高亮区域?

document.getSelection()可以拿到当前选中的节点信息。
anchorNode是第一个选中的节点
extentNode是最后选择的节点
应该可以从这里下手吧。

关注 4 回答 3

我是好人 赞了文章 · 2020-06-23

Vue 项目中使用国际化, 并配置动态切换语言的方法

主要由以下几个模块组成由 :

  • src\main.js
  • src\locales\index.js
  • src\locales\zh_CN.json
  • src\utils\config.js

# src\main.js

import i18n from '@/locales/index.js'

new Vue({
  el: '#app',
  i18n,
  router,
  store,
  render: h => h(App)
})

# src\locales\index.js

import Cookies from 'js-cookie'
import VueI18n from 'vue-i18n'
import Vue from 'vue'

const data = {}
const locale = Cookies.get('hb_lang') || 'en_US'
const readDir = ['en_US', 'zh_CN', 'th_TH']
for (let i = 0; i < readDir.length; i++) {
  data[readDir[i]] = require(`./${readDir[i]}.json`)
}

Vue.use(VueI18n)
const i18n = new VueI18n({
  locale,
  fallbackLocale: locale, // 语言环境中不存在相应massage键时回退到指定语言
  messages: data
})

export default i18n

# src\locales\zh_CN.json

示例项目包涵中英泰三国语言, 这里仅抽出中文作为示例 :

{
  "欢迎登录": "欢迎登录",
  "参数配置":"参数配置",
  "折价币种":"折价币种"
}

调用方法 :
<h1 class="slogan">{{ $t('欢迎登录') }}</h1>

# src\utils\config.js

import Cookies from 'js-cookie'
import i18n from '@/locales/index.js'
const Key = 'hb_lang'

export function get() {
  return Cookies.get(Key)
}

export function set(data) {
  i18n.locale = data
  return Cookies.set(Key, data)
}

export function remove() {
  return Cookies.remove(Key)
}

其中 , 当需要动态切换语言时,调用 set 方法即可, 例如:

import { set as setLanguage } from '@/utils/config.js'

setLanguage('en_US')

# 注意事项

以上配置须臾结合 Vue{{}} 进行编辑, 例如上文所提到的 :
<h1 class="solutions">{{ $t('solutions') }}</h1>

倘若像下面这样则会导致切换语言时, 无法动态即时更新文案 :

// 不要这样写, 解决方法在下面
<template>
  <div>
    <div class="solutions">{{ solutions }}</div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      solutions : this.$t('solutions')
    }
  }
}
</script>

解决方法 :

<template>
  <div>
    <div class="solutions">{{ solutions }}</div>
  </div>
</template>

<script>
export default {
  watch: {
    '$store.state.lang'(language) {
      this.init()
    }
  },
  data() {
    return {
      solutions : this.$t('solutions')
    }
  },
  created() {
    this.init()
  },
  methods: {
    init(){
      this.solutions = this.$t('solutions')
    }
  },
}
</script>

# 同系列的其他文章

查看原文

赞 14 收藏 11 评论 2

我是好人 回答了问题 · 2019-10-14

解决jquery如何实现当div内只存在一个a标签时,隐藏该div,存在2个或2个以上显示该div

为何不在渲染的时候去判断,直接不要渲染出来。

关注 2 回答 2

我是好人 赞了文章 · 2019-10-11

高效前端开发 - Visual Studio Code

本文是根据我在公司演讲(2019年8月)的高效开发主题PPT重新总结发布的一篇文章。有兴趣了解PPT的可以前往百度网盘下载:高效开发 - VSCode.pptx,提取码: yfkb

Visual Studio Code(后面简称VSCode)已经出来有几年了,为什么还要写这篇?原因是,我觉得这个编辑器强大到你不及时去了解尝试新的插件,你将没有办法时刻保持最高效的开发状态。也许本篇很多内容你已熟悉,但是我相信你依旧能从本文中受益。

VSCode简介

适合自己的编辑器能改变你的工作方式和效率,如果你也在用VSCode不妨思考一下。
  • Q1:一个编辑器真的值得花时间来介绍吗、还能提高效率?
  • Q2:你的VSCode内置终端是CMD?Powershell还是?
  • Q3:你了解你装的每个插件的用途吗?
  • Q4:创建一个临时测试用的脚本,你会怎么操作最快速?

先看一张近几年的几款常见的IDE发展趋势

IDE发展趋势

显然VSCode突飞猛进,确实他在众多前端开发IDE中一直在更强大。来自官方的Slogan:“Visual Studio Code 重新定义了代码编辑”

我简单总结了以下几点:

  • 免费、开源、多平台
  • 智能提示,代码片段,快速补全
  • 方便的调试能力
  • 内置Git
  • 丰富的插件

常用插件推荐

快乐程序员必备插件

作为一名快乐的前端开发工程师,必不可少的插件如下:

当你发现开发的乐趣大大的,效率自然提高了。咳咳咳!好了不开玩笑了。上面几款插件确实对一部分有用,但重磅推荐的是下面这些。

实用开发插件推荐

🎉🎉🎉强烈推荐的几款插件:

🎉🎉比较推荐的几款插件:

  • Better Comments:非常醒目的注释,让代码更容易阅读。
  • Bookmarks:打记号标签,通过快捷键快速在很长的代码中切换多个位置。
  • Live Server:快速启动一个本地服务器,对于编译好的静态项目可以快速访问。
  • Path Autocomplete:文件路径补全,超级好用。
  • Prettier:代码格式化工具,也还行,上手快速,配置简单。
  • Version Lens:针对package.json的npm包依赖进行版本检测。

其他可选插件(就不做过多的介绍了):

还有些我觉得也还不错,但是基本上是disabled状态的插件,也不过多介绍了。有兴趣可以搜索一下:

Quokkacarbon-now-shPolacodeColor HighlightMarkdown All in OneCode RunnerLeetCodeJavaScript (ES6) code snippetsPythonPlantUML等。

推荐卸载的插件

下面几个插件,我已经很久没用了,一般来说是曾经出现过严重异常但作者没修复,或已经有更好的插件替代了。

关于插件的补充说明

为什么我强烈推荐的插件就几个,为什么很多不错的插件我安装了却要disable。因为插件确实会占用编辑器的性能,装的太多很可能造成编辑器使用异常,甚至同类插件存在冲突都有可能。我在演讲PPT之前,找了公司许多前端了解了他们开发的习惯,很多开发者安装插件并不知道具体用途或者安装了用了几次就不用了。那么我给的插件安装建议如下:

  • 明确你所用的插件用途
  • 针对不常用的插件进行关闭
  • 针对某些项目才使用的插件请在workspace启用
  • 不要同时启用多个类似功能的插件,比如格式化代码插件

这样尽量能保持开发环境稳定。这就是Q3的解答了,还满意吗?

编辑器设置

常用的快捷键,真的需要用心去学习,尽可能多记一些,这不仅是VSCode提升效率开发的方法,任何工具都是需要的。(以下内容Windows将command换成Ctrl即可)

  • command + k, command + s:通过这个组合键,多看看快捷键
  • command + b:侧栏展开收缩
  • command + j:面板(问题、输出、调试、终端面板)展开收缩
  • command + ,:修改设置
  • command + shift + p:显示所有指令,等待输入执行
  • command + shift + e:显示文件侧栏
  • command + shift + f:显示搜索侧栏
  • command + shift + s:显示调试侧栏
  • Option + command + s:全部保存
  • ...

还有很多代码上的快捷键,包括收缩、注释、多选(多行、内容)等等,网上介绍太多了,大家有兴趣可以进一步了解。

补充说明

如果你发现某些快捷键不能用了。那一定是有其他软件占用了全局快捷键,只能慢慢排查了,我知道的Windows下有款软件可以查看系统快捷键使用情况,叫做Windows Hotkey Explorer,大家自行下载一下。(兴趣阅读:关于sublime text 3(3103)版本Ctrl+Alt+P无法正常使用解决办法

VSCode高级设置

通过command + ,,我们来修改编辑器的默认设置,这里我不太喜欢他的可视化设置界面,我将他切换到代码模式,手动添加如下代码:

{
  "editor.wordSeparators": "`~!@#$%^&*()=+[{]}\\|;:'\",.<>/?",
  "terminal.integrated.shell.windows": "C:\\Program Files\\Git\\bin\\bash.exe",
  "files.associations": {
    "*.tag": "html",
    "*.css": "css",
    "*.jsp": "html",
    "*.ejs": "html",
    "*.wxml": "html",
    "*.wxss": "css"
  }
}

上面三个设置说明

  • 分隔符去掉了,因为Html代码中的Css类名常用到,如果双击不能选中完整类名,好痛苦。
  • 命令行修改原因?如果在Windows下开发,我无法忍受Windows的命令行或者PS需要按Ctrl + C后还要Y一下。所以换成了Git Bash,请注意你的Git安装路径进行调整。这就是Q2的解释!
  • 其他语言的页面,需要使用HTML语法高亮配对的话,该设置可以针对不同文件后缀的文件做相同的语言模式开发。

上面是一些简单的配置,没有配置经验的可以先试试效果。接下来会有些复杂的设置。

结合插件的配置:

很多插件都有丰富的配置项来提高成产效率,下面举几个例子。

Prettier

编码风格需要全局配置的话,你就可以尝试如下配置:

{
  "prettier.printWidth": 160,
  "prettier.singleQuote": true,
  "prettier.semi": false,
}

Path Autocomplete

项目中如果有内置文件夹映射到@,比如Nuxt、或者手动配置webpack的目录alias,那么下面这段配置很好用了:

{
  "path-autocomplete.pathMappings": {
    "@": "${folder}/client",
  }
}

那么你在Nuxt项目中输入@,就能自动映射到项目目录的client,并提示client内的文件夹了。

Vetur

如果需要配合eslint,并保存自动格式化代码可以尝试下面设置:

{
  "eslint.enable": true,
  "editor.formatOnSave": false,
  // "eslint.autoFixOnSave": true, // 旧版使用,最新的请用下面的设置。
  "editor.codeActionsOnSave": {
    "source.fixAll": true
  },
  "eslint.validate": [
    {
      "language": "vue",
      "autoFix": true
    },
    {
      "language": "html",
      "autoFix": true
    },
    {
      "language": "javascript",
      "autoFix": true
    }
  ],
  "html.format.enable": false,
  "javascript.format.enable": false,
  "vetur.format.defaultFormatter.css": "prettier"
}

以上配置结合个人习惯进行调整即可。

特别说明:ESLint插件大约于2019年12月更新,故"eslint.autoFixOnSave": true配置不再有效,请更改为上面配置,具体可参考https://github.com/microsoft/...

其他技巧

该部分主要是PPT演示实操,故文章简单介绍一下。

针对开头的几个问题

JavaScript调试

  • 自定义Launch.json
  • Code Runner插件解决
  • Debugger for Chrome插件解决

前者建议创建一个专用的测试目录作为项目目录,配置好Launch.json,例如我这里配置如下:

{
  "version": "1.0.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Launch file",
      "program": "${file}"
    }
  ]
}

这段配置写好后,你就可以对单个JS文件进行代码调试了。如果经常测试代码片段,那就启用Code Runner插件吧。

如果需要浏览器中的页面调试,Debugger for Chrome或者Live Server启动一个页面来调试也很方便了。

侧栏搜索进行内容查找

你或许知道对项目进行全局代码搜索,但是项目文件过多,搜索速度明显就慢了。提升搜索效率有很多方法:

默认的搜索只能看到一个输入框,也许你知道输入框左侧箭头点一下就可以实现替换功能了,然后右侧下面三个点,有更多高级的功能哦~
  • 通过设置包含的文件排除的文件来提升搜索效率,这里支持通配符*
  • 通过大小写识别精确内容搜索正则表达式来更准确的搜索想要的内容。

快速打开项目中的文件

通过快捷键command + p,可以看到最近打开的文件,输入文件名可以非常方便的打开想要找到的文件。

快速打开项目

通过快捷键control+ r,可以快速选择需要打开最近或者想要查看的项目。

还有好多技巧其实都是离不开快捷键的,这里不多说了,大家自己多多探索。

思考及总结

认真阅读的你,我相信收获还是不少的。对于Q1的疑问,心中也有了答案。

其实还有非常重要的一点,要不断增加自己对编辑器的熟悉程度,一定要关注每次VSCode的更新日志,虽然每次都是英文的,可能看不懂,但是尽可能的过一遍,经常会有惊喜。

当然如果有兴趣,尝试自己写插件,满足个性需求也是很棒的,或者自己写snippet之类。

网络上关于VSCode相关介绍、技巧数不胜数,我还是写了这篇文章,我也是为了推动大家更好的使用这款编辑器为目的,希望能真正意义上提高前端开发效率。同时分享一个github仓:令人惊叹的VSCode

最后提出一个问题:

经历了这么多年的前端开发,我常用的编辑器也从Frontpage -> Dreamweaver(MX...) -> Sublime Text发展到现在VSCode。那么,重度依赖VSCode真的好吗?😁😁😁

查看原文

赞 6 收藏 4 评论 2

认证与成就

  • 获得 97 次点赞
  • 获得 12 枚徽章 获得 1 枚金徽章, 获得 1 枚银徽章, 获得 10 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-01-21
个人主页被 1.6k 人浏览