浅谈Webpack核心模块tapable解析

本文介绍了Webpack核心模块tapable,分享给大家,具体如下:

前言

Webpack 是一个现代 JavaScript 应用程序的静态模块打包器,是对前端项目实现自动化和优化必不可少的工具,Webpack 的 loader (加载器)和 plugin (插件)是由 Webpack 开发者和社区开发者共同贡献的,而目前又没有比较系统的开发文档,想写加载器和插件必须要懂 Webpack 的原理,即看懂 Webpack 的源码, tapable 则是 Webpack 依赖的核心库,可以说不懂 tapable 就看不懂 Webpack 源码,所以本篇会对 tapable 提供的类进行解析和模拟。

tapable 介绍

Webpack 本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是 tapable ,Webpack 中最核心的,负责编译的 Compiler 和负责创建 bundles 的 Compilation 都是 tapable 构造函数的实例。

打开 Webpack 4.0 的源码中一定会看到下面这些以 Sync 、 Async 开头,以 Hook 结尾的方法,这些都是 tapable 核心库的类,为我们提供不同的事件流执行机制,我们称为 “钩子”。

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

上面的实现事件流机制的 “钩子” 大方向可以分为两个类别,“同步” 和 “异步”,“异步” 又分为两个类别,“并行” 和 “串行”,而 “同步” 的钩子都是串行的。

Sync 类型的钩子

1、SyncHook

SyncHook 为串行同步执行,不关心事件处理函数的返回值,在触发事件之后,会按照事件注册的先后顺序执行所有的事件处理函数。

// SyncHook 钩子的使用
const { SyncHook } = require("tapable");

// 创建实例
let syncHook = new SyncHook(["name", "age"]);

// 注册事件
syncHook.tap("1", (name, age) => console.log("1", name, age));
syncHook.tap("2", (name, age) => console.log("2", name, age));
syncHook.tap("3", (name, age) => console.log("3", name, age));

// 触发事件,让监听函数执行
syncHook.call("panda", 18);

// 1 panda 18
// 2 panda 18
// 3 panda 18

在 tapable 解构的 SyncHook 是一个类,注册事件需先创建实例,创建实例时支持传入一个数组,数组内存储事件触发时传入的参数,实例的 tap 方法用于注册事件,支持传入两个参数,第一个参数为事件名称,在 Webpack 中一般用于存储事件对应的插件名称(名字随意,只是起到注释作用), 第二个参数为事件处理函数,函数参数为执行 call 方法触发事件时所传入的参数的形参。

// 模拟 SyncHook 类
class SyncHook {
  constructor(args) {
    this.args = args;
    this.tasks = [];
  }
  tap(name, task) {
    this.tasks.push(task);
  }
  call(...args) {
    // 也可在参数不足时抛出异常
    if (args.length < this.args.length) throw new Error("参数不足");

    // 传入参数严格对应创建实例传入数组中的规定的参数,执行时多余的参数为 undefined
    args = args.slice(0, this.args.length);

    // 依次执行事件处理函数
    this.tasks.forEach(task => task(...args));
  }
}

tasks 数组用于存储事件处理函数, call 方法调用时传入参数超过创建 SyncHook 实例传入的数组长度时,多余参数可处理为 undefined ,也可在参数不足时抛出异常,不灵活,后面的例子中就不再这样写了。

2、SyncBailHook

SyncBailHook 同样为串行同步执行,如果事件处理函数执行时有一个返回值不为空(即返回值为 undefined ),则跳过剩下未执行的事件处理函数(如类的名字,意义在于保险)。

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

// 创建实例
let syncBailHook = new SyncBailHook(["name", "age"]);

// 注册事件
syncBailHook.tap("1", (name, age) => console.log("1", name, age));

syncBailHook.tap("2", (name, age) => {
  console.log("2", name, age);
  return "2";
});

syncBailHook.tap("3", (name, age) => console.log("3", name, age));

// 触发事件,让监听函数执行
syncBailHook.call("panda", 18);

// 1 panda 18
// 2 panda 18

通过上面的用法可以看出, SyncHook 和 SyncBailHook 在逻辑上只是 call 方法不同,导致事件的执行机制不同,对于后面其他的 “钩子”,也是 call 的区别,接下来实现 SyncBailHook 类。

// 模拟 SyncBailHook 类
class SyncBailHook {
  constructor(args) {
    this.args = args;
    this.tasks = [];
  }
  tap(name, task) {
    this.tasks.push(task);
  }
  call(...args) {
    // 传入参数严格对应创建实例传入数组中的规定的参数,执行时多余的参数为 undefined
    args = args.slice(0, this.args.length);

    // 依次执行事件处理函数,如果返回值不为空,则停止向下执行
    let i = 0, ret;
    do {
      ret = this.tasks[i++](...args);
    } while (!ret);
  }
}

在上面代码的 call 方法中,我们设置返回值为 ret ,第一次执行后没有返回值则继续循环执行,如果有返回值则立即停止循环,即实现 “保险” 的功能。

3、SyncWaterfallHook

SyncWaterfallHook 为串行同步执行,上一个事件处理函数的返回值作为参数传递给下一个事件处理函数,依次类推,正因如此,只有第一个事件处理函数的参数可以通过 call 传递,而 call 的返回值为最后一个事件处理函数的返回值。

// SyncWaterfallHook 钩子的使用
const { SyncWaterfallHook } = require("tapable");

// 创建实例
let syncWaterfallHook = new SyncWaterfallHook(["name", "age"]);

// 注册事件
syncWaterfallHook.tap("1", (name, age) => {
  console.log("1", name, age);
  return "1";
});

syncWaterfallHook.tap("2", data => {
  console.log("2", data);
  return "2";
});

syncWaterfallHook.tap("3", data => {
  console.log("3", data);
  return "3"
});

// 触发事件,让监听函数执行
let ret = syncWaterfallHook.call("panda", 18);
console.log("call", ret);

// 1 panda 18
// 2 1
// 3 2
// call 3

SyncWaterfallHook 名称中含有 “瀑布”,通过上面代码可以看出 “瀑布” 形象生动的描绘了事件处理函数执行的特点,与 SyncHook 和 SyncBailHook 的区别就在于事件处理函数返回结果的流动性,接下来看一下 SyncWaterfallHook 类的实现。

// 模拟 SyncWaterfallHook 类
class SyncWaterfallHook {
  constructor(args) {
    this.args = args;
    this.tasks = [];
  }
  tap(name, task) {
    this.tasks.push(task);
  }
  call(...args) {
    // 传入参数严格对应创建实例传入数组中的规定的参数,执行时多余的参数为 undefined
    args = args.slice(0, this.args.length);

    // 依次执行事件处理函数,事件处理函数的返回值作为下一个事件处理函数的参数
    let [first, ...others] = this.tasks;
    return reduce((ret, task) => task(ret), first(...args));
  }
}

上面代码中 call 的逻辑是将存储事件处理函数的 tasks 拆成两部分,分别为第一个事件处理函数,和存储其余事件处理函数的数组,使用 reduce 进行归并,将第一个事件处理函数执行后的返回值作为归并的初始值,依次调用其余事件处理函数并传递上一次归并的返回值。

4、SyncLoopHook

SyncLoopHook 为串行同步执行,事件处理函数返回 true 表示继续循环,即循环执行当前事件处理函数,返回 undefined 表示结束循环, SyncLoopHook 与 SyncBailHook 的循环不同, SyncBailHook 只决定是否继续向下执行后面的事件处理函数,而 SyncLoopHook 的循环是指循环执行每一个事件处理函数,直到返回 undefined 为止,才会继续向下执行其他事件处理函数,执行机制同理。

// SyncLoopHook 钩子的使用
const { SyncLoopHook } = require("tapable");

// 创建实例
let syncLoopHook = new SyncLoopHook(["name", "age"]);

// 定义辅助变量
let total1 = 0;
let total2 = 0;

// 注册事件
syncLoopHook.tap("1", (name, age) => {
  console.log("1", name, age, total1);
  return total1++ < 2 ? true : undefined;
});

syncLoopHook.tap("2", (name, age) => {
  console.log("2", name, age, total2);
  return total2++ < 2 ? true : undefined;
});

syncLoopHook.tap("3", (name, age) => console.log("3", name, age));

// 触发事件,让监听函数执行
syncLoopHook.call("panda", 18);

// 1 panda 18 0
// 1 panda 18 1
// 1 panda 18 2
// 2 panda 18 0
// 2 panda 18 1
// 2 panda 18 2
// 3 panda 18

通过上面的执行结果可以清楚的看到 SyncLoopHook 的执行机制,但有一点需要注意,返回值必须严格是 true 才会触发循环,多次执行当前事件处理函数,必须严格返回 undefined ,才会结束循环,去执行后面的事件处理函数,如果事件处理函数的返回值不是 true 也不是 undefined ,则会死循环。

在了解 SyncLoopHook 的执行机制以后,我们接下来看看 SyncLoopHook 的 call 方法是如何实现的。

// 模拟 SyncLoopHook 类
class SyncLoopHook {
  constructor(args) {
    this.args = args;
    this.tasks = [];
  }
  tap(name, task) {
    this.tasks.push(task);
  }
  call(...args) {
    // 传入参数严格对应创建实例传入数组中的规定的参数,执行时多余的参数为 undefined
    args = args.slice(0, this.args.length);

    // 依次执行事件处理函数,如果返回值为 true,则继续执行当前事件处理函数
    // 直到返回 undefined,则继续向下执行其他事件处理函数
    this.tasks.forEach(task => {
      let ret;
      do {
        ret = this.task(...args);
      } while (ret === true || !(ret === undefined));
    });
  }
}

在上面代码中可以看到 SyncLoopHook 类 call 方法的实现更像是 SyncHook 和 SyncBailHook 的 call 方法的结合版,外层循环整个 tasks 事件处理函数队列,内层通过返回值进行循环,控制每一个事件处理函数的执行次数。

注意:在 Sync 类型 “钩子” 下执行的插件都是顺序执行的,只能使用 tab 注册。

Async 类型的钩子

Async 类型可以使用 tap 、 tapSync 和 tapPromise 注册不同类型的插件 “钩子”,分别通过 call 、 callAsync 和 promise 方法调用,我们下面会针对 AsyncParallelHook 和 AsyncSeriesHook 的 async 和 promise 两种方式分别介绍和模拟。

1、AsyncParallelHook

AsyncParallelHook 为异步并行执行,通过 tapAsync 注册的事件,通过 callAsync 触发,通过 tapPromise 注册的事件,通过 promise 触发(返回值可以调用 then 方法)。

(1) tapAsync/callAsync

callAsync 的最后一个参数为回调函数,在所有事件处理函数执行完毕后执行。

// AsyncParallelHook 钩子:tapAsync/callAsync 的使用
const { AsyncParallelHook } = require("tapable");

// 创建实例
let asyncParallelHook = new AsyncParallelHook(["name", "age"]);

// 注册事件
console.time("time");
asyncParallelHook.tapAsync("1", (name, age, done) => {
  settimeout(() => {
    console.log("1", name, age, new Date());
    done();
  }, 1000);
});

asyncParallelHook.tapAsync("2", (name, age, done) => {
  settimeout(() => {
    console.log("2", name, age, new Date());
    done();
  }, 2000);
});

asyncParallelHook.tapAsync("3", (name, age, done) => {
  settimeout(() => {
    console.log("3", name, age, new Date());
    done();
    console.timeEnd("time");
  }, 3000);
});

// 触发事件,让监听函数执行
asyncParallelHook.callAsync("panda", 18, () => {
  console.log("complete");
});

// 1 panda 18 2018-08-07T10:38:32.675Z
// 2 panda 18 2018-08-07T10:38:33.674Z
// 3 panda 18 2018-08-07T10:38:34.674Z
// complete
// time: 3005.060ms

异步并行是指,事件处理函数内三个定时器的异步操作最长时间为 3s ,而三个事件处理函数执行完成总共用时接近 3s ,所以三个事件处理函数是几乎同时执行的,不需等待。

所有 tabAsync 注册的事件处理函数最后一个参数都为一个回调函数 done ,每个事件处理函数在异步代码执行完毕后调用 done 函数,则可以保证 callAsync 会在所有异步函数都执行完毕后执行,接下来看一看 callAsync 是如何实现的。

// 模拟 AsyncParallelHook 类:tapAsync/callAsync
class AsyncParallelHook {
  constructor(args) {
    this.args = args;
    this.tasks = [];
  }
  tabAsync(name, task) {
    this.tasks.push(task);
  }
  callAsync(...args) {
    // 先取出最后传入的回调函数
    let finalCallback = args.pop();

    // 传入参数严格对应创建实例传入数组中的规定的参数,执行时多余的参数为 undefined
    args = args.slice(0, this.args.length);

    // 定义一个 i 变量和 done 函数,每次执行检测 i 值和队列长度,决定是否执行 callAsync 的回调函数
    let i = 0;
    let done = () => {
      if (++i === this.tasks.length) {
        finalCallback();
      }
    };

    // 依次执行事件处理函数
    this.tasks.forEach(task => task(...args, done));
  }
}

在 callAsync 中,将最后一个参数(所有事件处理函数执行完毕后执行的回调)取出,并定义 done 函数,通过比较 i 和存储事件处理函数的数组 tasks 的 length 来确定回调是否执行,循环执行每一个事件处理函数并将 done 作为最后一个参数传入,所以每个事件处理函数内部的异步操作完成时,执行 done 就是为了检测是不是该执行 callAsync 的回调,当所有事件处理函数均执行完毕满足 done 函数内部 i 和 length 相等的条件时,则调用 callAsync 的回调。

(2) tapPromise/promise

要使用 tapPromise 注册事件,对事件处理函数有一个要求,必须返回一个 Promise 实例,而 promise 方法也返回一个 Promise 实例, callAsync 的回调函数在 promise 方法中用 then 的方式代替。

// AsyncParallelHook 钩子:tapPromise/promise 的使用
const { AsyncParallelHook } = require("tapable");

// 创建实例
let asyncParallelHook = new AsyncParallelHook(["name", "age"]);

// 注册事件
console.time("time");
asyncParallelHook.tapPromise("1", (name, age) => {
  return new Promise((resolve, reject) => {
    settimeout(() => {
      console.log("1", name, age, new Date());
      resolve("1");
    }, 1000);
  });
});

asyncParallelHook.tapPromise("2", (name, age) => {
  return new Promise((resolve, reject) => {
    settimeout(() => {
      console.log("2", name, age, new Date());
      resolve("2");
    }, 2000);
  });
});

asyncParallelHook.tapPromise("3", (name, age) => {
  return new Promise((resolve, reject) => {
    settimeout(() => {
      console.log("3", name, age, new Date());
      resolve("3");
      console.timeEnd("time");
    }, 3000);
  });
});

// 触发事件,让监听函数执行
asyncParallelHook.promise("panda", 18).then(ret => {
  console.log(ret);
});

// 1 panda 18 2018-08-07T12:17:21.741Z
// 2 panda 18 2018-08-07T12:17:22.736Z
// 3 panda 18 2018-08-07T12:17:23.739Z
// time: 3006.542ms
// [ '1', '2', '3' ]

上面每一个 tapPromise 注册事件的事件处理函数都返回一个 Promise 实例,并将返回值传入 resolve 方法,调用 promise 方法触发事件时,如果所有事件处理函数返回的 Promise 实例结果都成功,会将结果存储在数组中,并作为参数传递给 promise 的 then 方法中成功的回调,如果有一个失败就是将失败的结果返回作为参数传递给失败的回调。

// 模拟 AsyncParallelHook 类 tapPromise/promise
class AsyncParallelHook {
  constructor(args) {
    this.args = args;
    this.tasks = [];
  }
  tapPromise(name, task) {
    this.tasks.push(task);
  }
  promise(...args) {
    // 传入参数严格对应创建实例传入数组中的规定的参数,执行时多余的参数为 undefined
    args = args.slice(0, this.args.length);

    // 将所有事件处理函数转换成 Promise 实例,并发执行所有的 Promise
    return Promise.all(this.tasks.map(task => task(...args)));
  }
}

其实根据上面对于 tapPromise 和 promise 使用的描述就可以猜到, promise 方法的逻辑是通过 Promise.all 来实现的。

2、AsyncSeriesHook

AsyncSeriesHook 为异步串行执行,与 AsyncParallelHook 相同,通过 tapAsync 注册的事件,通过 callAsync 触发,通过 tapPromise 注册的事件,通过 promise 触发,可以调用 then 方法。

(1) tapAsync/callAsync

与 AsyncParallelHook 的 callAsync 方法类似, AsyncSeriesHook 的 callAsync 方法也是通过传入回调函数的方式,在所有事件处理函数执行完毕后执行 callAsync 的回调函数。

// AsyncSeriesHook 钩子:tapAsync/callAsync 的使用
const { AsyncSeriesHook } = require("tapable");

// 创建实例
let asyncSeriesHook = new AsyncSeriesHook(["name", "age"]);

// 注册事件
console.time("time");
asyncSeriesHook.tapAsync("1", (name, age, next) => {
  settimeout(() => {
    console.log("1", name, age, new Date());
    next();
  }, 1000);
});

asyncSeriesHook.tapAsync("2", (name, age, next) => {
  settimeout(() => {
    console.log("2", name, age, new Date());
    next();
  }, 2000);
});

asyncSeriesHook.tapAsync("3", (name, age, next) => {
  settimeout(() => {
    console.log("3", name, age, new Date());
    next();
    console.timeEnd("time");
  }, 3000);
});

// 触发事件,让监听函数执行
asyncSeriesHook.callAsync("panda", 18, () => {
  console.log("complete");
});

// 1 panda 18 2018-08-07T14:40:52.896Z
// 2 panda 18 2018-08-07T14:40:54.901Z
// 3 panda 18 2018-08-07T14:40:57.901Z
// complete
// time: 6008.790ms

异步串行是指,事件处理函数内三个定时器的异步执行时间分别为 1s 、 2s 和 3s ,而三个事件处理函数执行完总共用时接近 6s ,所以三个事件处理函数执行是需要排队的,必须一个一个执行,当前事件处理函数执行完才能执行下一个。

AsyncSeriesHook 类的 tabAsync 方法注册的事件处理函数参数中的 next 可以与 AsyncParallelHook 类中 tabAsync 方法参数的 done 进行类比,同为回调函数,不同点在于 AsyncSeriesHook 与 AsyncParallelHook 的 callAsync 方法的 “并行” 和 “串行” 的实现方式。

// 模拟 AsyncSeriesHook 类:tapAsync/callAsync
class AsyncSeriesHook {
  constructor(args) {
    this.args = args;
    this.tasks = [];
  }
  tabAsync(name, task) {
    this.tasks.push(task);
  }
  callAsync(...args) {
    // 先取出最后传入的回调函数
    let finalCallback = args.pop();

    // 传入参数严格对应创建实例传入数组中的规定的参数,执行时多余的参数为 undefined
    args = args.slice(0, this.args.length);

    // 定义一个 i 变量和 next 函数,每次取出一个事件处理函数执行,并维护 i 的值
    // 直到所有事件处理函数都执行完,调用 callAsync 的回调
    // 如果事件处理函数中没有调用 next,则无法继续
    let i = 0;
    let next = () => {
      let task = this.tasks[i++];
      task ? task(...args, next) : finalCallback();
    };
    next();
  }
}

AsyncParallelHook 是通过循环依次执行了所有的事件处理函数, done 方法只为了检测是否已经满足条件执行 callAsync 的回调,如果中间某个事件处理函数没有调用 done ,只是不会调用 callAsync 的回调,但是所有的事件处理函数都执行了。

而 AsyncSeriesHook 的 next 执行机制更像 Express 和 Koa 中的中间件,在注册事件的回调中如果不调用 next ,则在触发事件时会在没有调用 next 的事件处理函数的位置 “卡死”,即不会继续执行后面的事件处理函数,只有都调用 next 才能继续,而最后一个事件处理函数中调用 next 决定是否调用 callAsync 的回调。

(2) tapPromise/promise

与 AsyncParallelHook 类似, tapPromise 注册事件的事件处理函数需要返回一个 Promise 实例, promise 方法最后也返回一个 Promise 实例。

// AsyncSeriesHook 钩子:tapPromise/promise 的使用
const { AsyncSeriesHook } = require("tapable");

// 创建实例
let asyncSeriesHook = new AsyncSeriesHook(["name", "age"]);

// 注册事件
console.time("time");
asyncSeriesHook.tapPromise("1", (name, age) => {
  return new Promise((resolve, reject) => {
    settimeout(() => {
      console.log("1", name, age, new Date());
      resolve("1");
    }, 1000);
  });
});

asyncSeriesHook.tapPromise("2", (name, age) => {
  return new Promise((resolve, reject) => {
    settimeout(() => {
      console.log("2", name, age, new Date());
      resolve("2");
    }, 2000);
  });
});

asyncParallelHook.tapPromise("3", (name, age) => {
  return new Promise((resolve, reject) => {
    settimeout(() => {
      console.log("3", name, age, new Date());
      resolve("3");
      console.timeEnd("time");
    }, 3000);
  });
});

// 触发事件,让监听函数执行
asyncSeriesHook.promise("panda", 18).then(ret => {
  console.log(ret);
});

// 1 panda 18 2018-08-07T14:45:52.896Z
// 2 panda 18 2018-08-07T14:45:54.901Z
// 3 panda 18 2018-08-07T14:45:57.901Z
// time: 6014.291ms
// [ '1', '2', '3' ]

分析上面的执行过程,所有的事件处理函数都返回了 Promise 的实例,如果想实现 “串行”,则需要让每一个返回的 Promise 实例都调用 then ,并在 then 中执行下一个事件处理函数,这样就保证了只有上一个事件处理函数执行完后才会执行下一个。

// 模拟 AsyncSeriesHook 类 tapPromise/promise
class AsyncSeriesHook {
  constructor(args) {
    this.args = args;
    this.tasks = [];
  }
  tapPromise(name, task) {
    this.tasks.push(task);
  }
  promise(...args) {
    // 传入参数严格对应创建实例传入数组中的规定的参数,执行时多余的参数为 undefined
    args = args.slice(0, this.args.length);

    // 将每个事件处理函数执行并调用返回 Promise 实例的 then 方法
    // 让下一个事件处理函数在 then 方法成功的回调中执行
    let [first, ...others] = this.tasks;
    return others.reduce((promise, task) => {
      return promise.then(() => task(...args));
    }, first(...args));
  }
}

上面代码中的 “串行” 是使用 reduce 归并来实现的,首先将存储所有事件处理函数的数组 tasks 解构成两部分,第一个事件处理函数和存储其他事件处理函数的数组 others ,对 others 进行归并,将第一个事件处理函数执行后返回的 Promise 实例作为归并的初始值,这样在归并的过程中上一个值始终是上一个事件处理函数返回的 Promise 实例,可以直接调用 then 方法,并在 then 的回调中执行下一个事件处理函数,直到归并完成,将 reduce 最后返回的 Promise 实例作为 promise 方法的返回值,则实现 promise 方法执行后继续调用 then 来实现后续逻辑。

对其他异步钩子补充

在上面 Async 异步类型的 “钩子中”,我们只着重介绍了 “串行” 和 “并行”( AsyncParallelHook 和 AsyncSeriesHook )以及回调和 Promise 的两种注册和触发事件的方式,还有一些其他的具有一定特点的异步 “钩子” 我们并没有进行分析,因为他们的机制与同步对应的 “钩子” 非常的相似。

AsyncParallelBailHook 和 AsyncSeriesBailHook 分别为异步 “并行” 和 “串行” 执行的 “钩子”,返回值不为 undefined ,即有返回值,则立即停止向下执行其他事件处理函数,实现逻辑可结合 AsyncParallelHook 、 AsyncSeriesHook 和 SyncBailHook 。

AsyncSeriesWaterfallHook 为异步 “串行” 执行的 “钩子”,上一个事件处理函数的返回值作为参数传递给下一个事件处理函数,实现逻辑可结合 AsyncSeriesHook 和 SyncWaterfallHook 。

总结

在 tapable 源码中,注册事件的方法 tab 、 tapSync 、 tapPromise 和触发事件的方法 call 、 callAsync 、 promise 都是通过 compile 方法快速编译出来的,我们本文中这些方法的实现只是遵照了 tapable 库这些 “钩子” 的事件处理机制进行了模拟,以方便我们了解 tapable ,为学习 Webpack 原理做了一个铺垫,在 Webpack 中,这些 “钩子” 的真正作用就是将通过配置文件读取的插件与插件、加载器与加载器之间进行连接,“并行” 或 “串行” 执行,相信在我们对 tapable 中这些 “钩子” 的事件机制有所了解之后,再重新学习 Webpack 的源码应该会有所头绪。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 关于webpack2和模块打包的新手指南(小结)

    webpack已成为现代Web开发中最重要的工具之一.它是一个用于JavaScript的模块打包工具,但是它也可以转换所有的前端资源,例如HTML和CSS,甚至是图片.它可以让你更好地控制应用程序所产生的HTTP请求数量.允许你使用其他资源的特性(例如Jade.Sass和ES6).webpack还可以让你轻松地从npm下载包. 本文主要针对那些刚接触webpack的同学,将介绍初始设置和配置.模块.加载器.插件.代码分割和热模块替换. 在继续学习下面的内容之前需要确保你的电脑中已经安装了Node

  • 详解用webpack把我们的业务模块分开打包的方法

    webpack我自己还在摸索学习中,今天给大家分享个用webpack把我们的业务模块分开打包的方法,顺便留个笔记 如何用webpack打包这3个js? 只需修改webpack的配置文件webpack.config.js: // entry是入口文件,可以多个,代表要编译那些js entry:['./src/main.js','./src/login.js','./src/reg.js'], 这样就可以全部打包,最终生成./build/js/build.js 1,那么如果我们想最后生成不同的文件,

  • 浅谈Webpack 是如何加载模块的

    Webpack 在前端开发中作为模块打包工具非常受开发者的青睐,丰富的 loader 使它可以实现各种各样的功能.本文将通过 webpack 来打包一个 js 文件,看看 webpack 是如何加载各个模块的. 两个简单的源文件 为了方便分析 webpack 加载模块的原理,我们准备了两个文件: hello.js const hello = { say: arg => { console.info('hello ' + arg || 'world'); } }; export default h

  • webpack组织模块打包Library的原理及实现

    之前一篇文章分析了Webpack打包JS模块的基本原理,所介绍的案例是最常见的一种情况,即多个JS模块和一个入口模块,打包成一个bundle文件,可以直接被浏览器或者其它JavaScript引擎执行,相当于直接编译生成一个完整的可执行的文件.不过还有一种很常见的情况,就是我们要构建发布一个JavaScript的库,比如你在npm社区发布自己的库,这时Webpack就需要相应的配置,编译生成的代码也会略有不同. 和之前一篇文章一样,本文主要分析的是Webpack的生成代码,并结合它来说明编译库时W

  • 详解webpack模块化管理和打包工具

    本篇文章主要介绍了详解webpack模块化管理和打包工具,小编觉得挺不错的,现在分享给大家,也给大家做个参考.一起跟随小编过来看看吧 Webpack简介 webpack是当下最热门的前端资源模块化管理和打包工具. 它可以将许多松散的模块按照依赖和规则打包成符合生产环境部署的前端资源.还可以将按需加载的模块进行代码分隔,等到实际 需要的时候再异步加载.通过 loader  的转换,任何形式的资源都可以视作模块,比如 CommonJs 模块. AMD 模块. ES6 模块.CSS.图片. JSON.

  • 详解webpack模块加载器兼打包工具

     什么是 webpack? webpack是近期最火的一款模块加载器兼打包工具,它能把各种资源,例如JS(含JSX).coffee.样式(含less/sass).图片等都作为模块来使用和处理. 我们可以直接使用 require(XXX) 的形式来引入各模块,即使它们可能需要经过编译(比如JSX和sass),但我们无须在上面花费太多心思,因为 webpack 有着各种健全的加载器(loader)在默默处理这些事情,这块我们后续会提到. 你可以不打算将其用在你的项目上,但没有理由不去掌握它,因为以近

  • 详解react-webpack2-热模块替换[HMR]

    本文介绍了react-webpack2-热模块替换[HMR],分享给大家,具体如下: 模块热替换功能会在应用程序运行过程中替换.添加或删除模块,而无需重新加载页面.这使得你可以在独立模块变更后,无需刷新整个页面,就可以更新这些模块,极大地加速了开发时间. babel 配置 需要先下载 npm install --save-dev react-hot-loader@3.0.0-beta.6 然后在 .babelrc 中配置 { "presets": [ ["es2015&quo

  • webpack 模块热替换原理

    全称是Hot Module ReplaceMent(HMR),理解成热模块替换或者模块热替换都可以吧,和.net中的热插拔一个意思,就是在运行中对程序的模块进行更新.这个功能主要是用于开发过程中,对生产环境没有任何帮助(这一点区别.net热插拔).效果上就是界面的无刷新更新. HMR基于WDS,style-loader可以通过它来实现无刷新更新样式.但是对于JavaScript模块就需要做一点额外的处理,怎么处理继续往下看.因为HMR是用于开发环境的,所以我们修改下配置,做两份准备.一个用于生产

  • webpack配置sass模块的加载的方法

    webpack管理的项目,我们希望用sass定义样式,为了正常编译,需要做如下配置.这里不讲webpack的入门,入门的文章,我推荐这篇<webpack入门>. 为了使用sass,我们需要安装sass的依赖包 //在项目下,运行下列命令行 npm install --save-dev sass-loader //因为sass-loader依赖于node-sass,所以还要安装node-sass npm install --save-dev node-sass 当然了,使用样式的话,css-lo

  • webpack external模块的具体使用

    这篇文章讨论Webpack打包library时经常需要用到的一个选项external,它用于避免将一些很通用的模块打包进你发布的library里,而是选择把它们声明成external的模块,在你的library被上层使用后,在最后阶段由Webpack统一把这个external的依赖模块打包进来. external选项一般都是用在打包library上面,如果不是library而是一个最终的app的发布JS文件,那external也没有什么意义.关于Webpack打包library的分析和一些选项的

随机推荐