揭开 Webpack 工作流程面纱:Tapable 模块

更新日期: 2022-01-17阅读: 815标签: Webpack

webpack 工程相当庞大,但是 Webpack 本质上是一种事件流机制,通过事件流将各种插件串联起来,最终完成 webpack 的全流程,而实现事件流机制的核心是今天要讲的Tapable 模块。Webpack 负责编译的 Compiler 和创建 Bundle 的 Compilation 都是继承自 Tapable。所以在讲 Webpack 工作流程之前,我们需要先掌握 Tapable。

事件监听和发射器

我们都知道 Node.js 特点提到事件驱动,这是因为 Node.js 本身利用 JavaScript 的语言特点实现了自定义的事件回调,Node.js 内部一个事件发射器 EventEmitter,通过这个类,可以进行事件监听与发射,这个也是 Node.js 的核心模块,很多 Node.js 内部模块都是继承自它,或者引用了它。

const EventEmitter = require('events').EventEmitter;
const event = new EventEmitter();
event.on('event_name', (arg) => {
  console.log('event_name fire', arg);
});
setTimeout(function () {
  event.emit('event_name', 'hello world');
}, 1000);

上面代码就是事件发射器的用法。

webpack 核心库 Tapable 的原理和 EventEmitter 类似,但是功能更强大,包括多种类型,通过事件的注册和监听,触发 webpack 生命周期中的函数方法,在 Webpack 中,tapable 都是放到对象的hooks上,所以我们叫他们钩子。翻阅 webpack 的源码时,会发现很多类似下面的代码:

// webpack 4.29.6
// lib/compiler
class Compiler extends Tapable {
  constructor(context) {
    super();
    this.hooks = {
      shouldEmit: new SyncBailHook(['compilation']),
      done: new AsyncSeriesHook(['stats']),
      additionalPass: new AsyncSeriesHook([]),
      beforeRun: new AsyncSeriesHook(['compiler']),
      run: new AsyncSeriesHook(['compiler']),
      emit: new AsyncSeriesHook(['compilation']),
      afterEmit: new AsyncSeriesHook(['compilation']),

      thisCompilation: new SyncHook(['compilation', 'params']),
      compilation: new SyncHook(['compilation', 'params']),
      normalModuleFactory: new SyncHook(['normalModuleFactory']),
      contextModuleFactory: new SyncHook(['contextModulefactory']),

      beforeCompile: new AsyncSeriesHook(['params']),
      compile: new SyncHook(['params']),
      make: new AsyncParallelHook(['compilation']),
      afterCompile: new AsyncSeriesHook(['compilation']),

      watchRun: new AsyncSeriesHook(['compiler']),
      failed: new SyncHook(['error']),
      invalid: new SyncHook(['filename', 'changeTime']),
      watchClose: new SyncHook([]),

      environment: new SyncHook([]),
      afterEnvironment: new SyncHook([]),
      afterPlugins: new SyncHook(['compiler']),
      entryOption: new SyncBailHook(['context', 'entry']),
    };
  }
}

这些代码就是一个类或者函数完整生命周期需要**「走过的路」**,所有 Webpack 代码虽然代码量很大,但是从hook找生命周期事件点,然后通过 hook 名称,基本就可以猜出大概流程。

Tapable 中 Hook 的类型

在Tapable 的文档中显示了,Tapable 分为以下类型:

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

Hook 类型可以分为同步(Sync)和异步(Async),异步又分为并行和串行。


根据使用方式来分,又可以分为Basic、Waterfal、Bail和Loop四类,每类 Hook 都有自己的使用要点:

类型使用要点
Basic基础类型,不关心监听函数的返回值,不根据返回值做事情
Bail保险式,只要监听函数中有返回值(不为undefined),则跳过之后的监听函数
Waterfal瀑布式,上一步的返回值继续交给下一步处理和使用
Loop循环类型,如果该监听函数返回 true 则这个监听函数会反复执行,如果返回 undefined 则退出循环

Basic 类型 Hook

基础类型包括SyncHook、AsyncParallelHook和AsyncSeriesHook,这类 Hook 不关心函数的返回值,会一直执行到底。下面以SyncHook为例来说明下:

const {SyncHook} = require('tapable');
// 所有的构造函数都接收一个可选的参数,这个参数是一个参数名的字符串数组
// 1. 这里array的字符串随便填写,但是array的长度必须与实际要接受参数个数保持一致;
// 2. 如果回调不接受参数,可以传入空数组。
// 后面类型都是这个规则,不再做过多说明
const hook = new SyncHook(['name']);

// 添加监听
hook.tap('1', (arg0, arg1) => {
  // tap 的第一个参数是用来标识`call`传入的参数
  // 因为new的时候只的array长度为1
  // 所以这里只得到了`call`传入的第一个参数,即Webpack
  // arg1 为 undefined
  console.log(arg0, arg1, 1);
  return '1';
});
hook.tap('2', (arg0) => {
  console.log(arg0, 2);
});
hook.tap('3', (arg0) => {
  console.log(arg0, 3);
});

// 传入参数,触发监听的函数回调
// 这里传入两个参数,但是实际回调函数只得到一个
hook.call('Webpack', 'Tapable');
// 执行结果:
/*
Webpack undefined 1 // 传入的参数需要和new实例的时候保持一致,否则获取不到多传的参数
Webpack 2
Webpack 3
*/

通过上面的代码可以得出结论:

  • 在实例化SyncHook传入的数组参数实际是只用了长度,跟实际内容没有关系
  • 执行call时,入参个数跟实例化时数组长度相关
  • 回调栈是按照「先入先出」顺序执行的(这里叫回调队列更合适,队列是先入先出)
  • 功能跟 EventEmitter 类似

详细的流程图如下:


再来看下AsyncSeriesHook的示例:

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

const hook = new AsyncSeriesHook(['name']);
hook.tapAsync('one', (name, cb) => {
  console.log('one', name);
  setTimeout(() => {
    console.log('one timeout');
    cb();
  }, 100);
});
hook.tapAsync('two', (name, cb) => {
  console.log('two', name);
  cb();
});
hook.callAsync('asyncHook', (endArgs) => {
  console.log('end');
  console.log('endArgs', endArgs);
});
// 执行结果:
/*
one asyncHook
one timeout
two asyncHook
end
endArgs undefined
*/

如果对代码 setTimeout 部分进行修改:

setTimeout(() => {
  console.log('one timeout');
  cb(1); // <- 回调
}, 100);
// 执行结果
/*
one asyncHook
one timeout
end
err 1
*/

说明在AsyncSeriesHook流程中只要任何地方回调了cb传入参数,则直接跳过后续流程,直接进入callAsync的回调,从而结束流程。

Bail 类型 Hook

Bail类型的 Hook 包括:SyncBailHook、AsyncSeriesBailHook、AsyncParallelBailHook,Bail类型的 Hook 也是按回调栈顺序一次执行回调,但是如果其中一个回调函数返回结果result !== undefined 则退出回调栈调。代码示例如下:。

const {SyncBailHook} = require('tapable');
const hook = new SyncBailHook(['name']);
hook.tap('1', (name) => {
  console.log(name, 1);
});
hook.tap('2', (name) => {
  console.log(name, 2);
  return 'stop';
});
hook.tap('3', (name) => {
  console.log(name, 3);
});
hook.call('hello');

/* output
hello 1
hello 2
 */

通过上面的代码可以得出结论:

  1. BailHook 中的回调是顺序执行的
  2. 调用call传入的参数会被每个回调函数都获取
  3. 当回调函数返回undefined 才会停止回调栈的调用

详细的流程图如下:


SyncBailHook类似Array.find,找到(或者发生)一件事情就停止执行;AsyncParallelBailHook类似Promise.race这里竞速场景,只要有一个回调解决了一个问题,全部都解决了。

Waterfall 类型 Hook

Waterfall类型 Hook 包括 SyncWaterfallHook、AsyncSeriesWaterfallHook。类似Array.reduce效果,如果上一个回调函数的结果 result !== undefined,则会被作为下一个回调函数的第一个参数。代码示例如下:

const {SyncWaterfallHook} = require('tapable');
const hook = new SyncWaterfallHook(['arg0', 'arg1']);
hook.tap('1', (arg0, arg1) => {
  console.log(arg0, arg1, 1);
  return 1;
});
hook.tap('2', (arg0, arg1) => {
  console.log(arg0, arg1, 2);
  return 2;
});
hook.tap('3', (arg0, arg1) => {
  // 这里 arg0 = 2
  console.log(arg0, arg1, 3);
  // 等同于 return undefined
});
hook.tap('4', (arg0, arg1) => {
  // 这里 arg0 = 2 还是2
  console.log(arg0, arg1, 4);
});
hook.call('Webpack', 'Tapable');
/* console log output
Webpack Tapable 1
1 'Tapable' 2
2 'Tapable' 3
2 'Tapable' 4 */

通过上面的代码可以得出结论:

  1. WaterfallHook 的回调函数接受的参数来自于上一个函数结果
  2. 调用call传入的第一个参数会被上一个函数的undefined结果给替换
  3. 当回调函数返回undefined 不会停止回调栈的调用

详细的流程图如下:


Loop 类型 Hook

这类 Hook 只有一个SyncLoopHook(虽然 Tapable 1.1.1 版本中存在AsyncSeriesLoopHook,但是并没有将它 export 出来),LoopHook执行特点是不停的循环执行回调函数,直到所有函数结果 result === undefined。为了更加直观的展现 LoopHook 的执行过程,我对示例代码做了一下丰富:

const {SyncLoopHook} = require('tapable');
const hook = new SyncLoopHook(['name']);
let callbackCalledCount1 = 0;
let callbackCalledCount2 = 0;
let callbackCalledCount3 = 0;
let intent = 0;
hook.tap('callback 1', (arg) => {
  callbackCalledCount1++;
  if (callbackCalledCount1 === 2) {
    callbackCalledCount1 = 0;
    intent -= 4;
    intentLog('</callback-1>');
    return;
  } else {
    intentLog('<callback-1>');
    intent += 4;
    return 'callback-1';
  }
});

hook.tap('callback 2', (arg) => {
  callbackCalledCount2++;
  if (callbackCalledCount2 === 2) {
    callbackCalledCount2 = 0;
    intent -= 4;
    intentLog('</callback-2>');
    return;
  } else {
    intentLog('<callback-2>');
    intent += 4;
    return 'callback-2';
  }
});

hook.tap('callback 3', (arg) => {
  callbackCalledCount3++;
  if (callbackCalledCount3 === 2) {
    callbackCalledCount3 = 0;
    intent -= 4;
    intentLog('</callback-3>');
    return;
  } else {
    intentLog('<callback-3>');
    intent += 4;
    return 'callback-3';
  }
});

hook.call('args');

function intentLog(...text) {
  console.log(new Array(intent).join(' '), ...text);
}
/* output
 <callback-1>
 </callback-1>
 <callback-2>
    <callback-1>
    </callback-1>
 </callback-2>
 <callback-3>
    <callback-1>
    </callback-1>
    <callback-2>
        <callback-1>
        </callback-1>
    </callback-2>
 </callback-3>
 */

通过上面的代码可以得出结论:

  1. LoopHook 中的回调返回undefined(没有 return 其实就是undefined)才会跳出循环
  2. 所说的循环,起点是第一个回调栈的函数

详细的流程图如下:


Tapable 在 Webpack 中的应用

了解了 Tapable 的类型和基本使用方法,我们可能产生疑惑,这些类型在 Webpack 中是怎么被应用的,又是为什么要舍近求远的写个专门的库来实现异步,EventEmitter 和 Promise 不香吗?下面我们来看下 Webpack 内部的应用。

在 Webpack 中用的最多的是AsyncSeriesHook,我们摘录了 Webpack Compiler 一段代码:

// 为了好理解,对代码进行必要的精简
// 定义hooks
this.hooks = Object.freeze({
  beforeRun: new AsyncSeriesHook(['compiler']),
  run: new AsyncSeriesHook(['compiler']),
  done: new AsyncSeriesHook(['stats']),
  afterDone: new SyncHook(['stats']),
  failed: new SyncHook(['error']),
});

const finalCallback = (err, stats) => {
  // ...
  if (err) {
    this.hooks.failed.call(err);
  }
  if (callback !== undefined) callback(err, stats);
  // 触发 afterDone
  this.hooks.afterDone.call(stats);
};
const onCompiled = (err, compilation) => {
  if (err) return finalCallback(err);

  if (this.hooks.shouldEmit.call(compilation) === false) {
    // ...
    // 生成stats实例
    const stats = new Stats(compilation);
    this.hooks.done.callAsync(stats, (err) => {
      if (err) return finalCallback(err);
      return finalCallback(null, stats);
    });
    return;
  }
  // ...
  this.hooks.done.callAsync(stats, (err) => {
    if (err) return finalCallback(err);
    this.cache.storeBuildDependencies(compilation.buildDependencies, (err) => {
      if (err) return finalCallback(err);
      return finalCallback(null, stats);
    });
  });
};
const run = () => {
  this.hooks.beforeRun.callAsync(this, (err) => {
    if (err) return finalCallback(err);

    this.hooks.run.callAsync(this, (err) => {
      if (err) return finalCallback(err);

      this.readRecords((err) => {
        if (err) return finalCallback(err);

        this.compile(onCompiled);
      });
    });
  });
};
// ==开始执行==
run();

在上面精简的代码中:

  1. 执行是从run开始,依次调用 Tapable 的 Hooks;
  2. 用的最多的是AsyncSeriesHook这个
  3. 我们看到整个代码出现最多的是if (err) return finalCallback(err);

Webpack 内部的 Tapable 的使用,实际是实现了一个异步操作流程,因为使用了AsyncSeriesHook,所以整个流程都是串行的异步,并且任何函数中遇见错误,回调Callback传回错误(finalCallback(err)),则流程立即停止。

这里实际是实现了一个符合 Node.js 的错误优先原则异步流程,这种用法和实现,保证了异步的同时,也很好的控制了流程中遇见的错误,即当错误在任何环节发生的时候,就可以调用回调函数直接结束整个流程。看到这里不得不说这种设计真的很赞。

Node.js 的错误优先原则: Node.js 核心 api 暴露的大多数异步方法都遵循称为错误优先回调的惯用模式。 使用这种模式,回调函数作为参数传给方法。 当操作完成或出现错误时,回调函数将使用 Error 对象(如果有)作为第一个参数传入。 如果没有出现错误,则第一个参数将作为 null 传入。

Tapable 的原理解析

Tapable 的执行流程可以分为四步:

  1. 使用tap*对事件进行注册绑定,根据类型不同,提供三种绑定的方式:tap、tapPromise、tapAsync,其中tapPromise、tapAsync为异步类 Hook 的绑定方法
  2. 使用call*对事件进行触发,根据类型不同,也提供了三种触发的方式:call、promise、callAsync
  3. 生成对应类型的代码片段(要执行的代码实际是拼字符串拼出来的)
  4. 生成第三步生成的代码片段

下面以SyncHook源码为例,分析下整个流程。先来看下lib/SyncHook.js 主要代码:

class SyncHook extends Hook {
  // 错误处理,防止调用者调用异步钩子
  tapAsync() {
    throw new Error('tapAsync is not supported on a SyncHook');
  }
  tapPromise() {
    throw new Error('tapPromise is not supported on a SyncHook');
  }
  // 实现入口
  compile(options) {
    factory.setup(this, options);
    return factory.create(options);
  }
}

首先所有的 Hook 都是继承自Hook类,针对同步 Hook 的事件绑定,如SyncHook、SyncBailHook、SyncLoopHook、SyncWaterfallHook, 会在子类中覆写基类Hook中 tapAsync 和 tapPromise 方法,这样做可以防止使用者在同步 Hook 中误用异步方法。

下面我按照执行流程的四个步骤来分析下源码,看一下一个完整的流程中,都是调用了什么方法和怎么实现的。

绑定事件

SyncHook中绑定事件是下面的代码。

hook.tap('evt1', (arg0) => {
  console.log(arg0, 2);
});
hook.tap('evt2', (arg0) => {
  console.log(arg0, 3);
});

下面我们来看下tap的实现,因为SyncHook是继承子Hook,所以我们找到lib/Hook.js中 tap 的实现代码:

tap(options, fn) {
    // 实际调用了_tap
    this._tap("sync", options, fn);
}
_tap(type, options, fn) {
    // 这里主要进行了一些参数的类型判断
    if (typeof options === "string") {
        options = {
            name: options
        };
    } else if (typeof options !== "object" || options === null) {
        throw new Error("Invalid tap options");
    }
    if (typeof options.name !== "string" || options.name === "") {
        throw new Error("Missing name for tap");
    }
    if (typeof options.context !== "undefined") {
        deprecateContext();
    }
    options = Object.assign({ type, fn }, options);
    // 这里是注册了Interceptors(拦截器)
    options = this._runRegisterInterceptors(options);
    // 参数处理完之后,调用了_insert,这是关键代码
    this._insert(options);
}

通过查阅Hook.tap和Hook._tap的代码,发现主要是做一些参数处理的工作,而主要的实现是在Hook._insert实现的:

// tapable/lib/Hook.js
_insert(item) {
    this._resetCompilation();
    let before;
    if (typeof item.before === "string") {
        before = new Set([item.before]);
    } else if (Array.isArray(item.before)) {
        before = new Set(item.before);
    }
    let stage = 0;
    if (typeof item.stage === "number") {
        stage = item.stage;
    }
    // 这里根据 stage 对事件进行一个优先级排序
    let i = this.taps.length;
    while (i > 0) {
        i--;
        const x = this.taps[i];
        this.taps[i + 1] = x;
        const xStage = x.stage || 0;
        if (before) {
            if (before.has(x.name)) {
                before.delete(x.name);
                continue;
            }
            if (before.size > 0) {
                continue;
            }
        }
        if (xStage > stage) {
            continue;
        }
        i++;
        break;
    }
    // 这是找到了回调栈
    this.taps[i] = item;
}

_insert的代码主要目的是将传入的事件推入this.taps数组,等同于:

hook.tap('event', callback);
// → 即
this.taps.push({
  type: 'sync',
  name: 'event',
  fn: callback,
});

在基类lib/Hook.js的constructor中,可以找到一些变量初始化的代码:

class Hook {
  constructor(args = []) {
    // 这里存入初始化的参数
    this._args = args;
    // 这里就是回调栈用到的数组
    this.taps = [];
    // 拦截器数组
    this.interceptors = [];
    this.call = this._call;
    this.promise = this._promise;
    this.callAsync = this._callAsync;
    // 这个比较重要,后面拼代码会用
    this._x = undefined;
  }
}

这样绑定回调函数就完成了,下面看下触发回调的时候发生了什么。

事件触发

在事件触发,我们使用同syncHook的call方法触发一个事件:

hook.call(1, 2);

这里的call方法,实际是通过Object.defineProperties添加到Hook.prototype上面的:

// tapable/lib/Hook.js
function createCompileDelegate(name, type) {
  return function lazyCompileHook(...args) {
    this[name] = this._createCall(type);
    return this[name](...args);
  };
}

Object.defineProperties(Hook.prototype, {
  _call: {
    value: createCompileDelegate('call', 'sync'),
    configurable: true,
    writable: true,
  },
  _promise: {
    value: createCompileDelegate('promise', 'promise'),
    configurable: true,
    writable: true,
  },
  _callAsync: {
    value: createCompileDelegate('callAsync', 'async'),
    configurable: true,
    writable: true,
  },
});

在上面的代码中,Hook.prototype通过对象定义属性方法Object.defineProperties定义了三个属性方法:_call、_promise、_callAsync,这三个属性的value都是通过 createCompileDelegate返回的一个名为lazyCompileHook的函数,从名字上面来猜测是「懒编译」,当我们真正调用call方法的时候,才会编译出真正的call函数。

call函数编译的用到的是_createCall方法,这个是在 Hook 类定义的时候就定义的方法,_createCall实际最终调用了compile方法,而通过Hook.js代码来看,compile是个需要子类重写实现的方法:

// tapable/lib/Hook.js
compile(options) {
    throw new Error("Abstract: should be overriden");
}

_createCall(type) {
    return this.compile({
        taps: this.taps,
        interceptors: this.interceptors,
        args: this._args,
        type: type
    });
}

所以,在 Hook 中绕了一圈,我们又回到了SyncHook的类,我们再看下 SyncHook 的代码:

// lib/SyncHook.js
const HookCodeFactory = require('./HookCodeFactory');

class SyncHookCodeFactory extends HookCodeFactory {
  content({onError, onDone, rethrowIfPossible}) {
    return this.callTapsSeries({
      onError: (i, err) => onError(err),
      onDone,
      rethrowIfPossible,
    });
  }
}

const factory = new SyncHookCodeFactory();

class SyncHook extends Hook {
  tapAsync() {
    throw new Error('tapAsync is not supported on a SyncHook');
  }

  tapPromise() {
    throw new Error('tapPromise is not supported on a SyncHook');
  }

  compile(options) {
    factory.setup(this, options);
    return factory.create(options);
  }
}

SyncHook 的compile来自是HookCodeFactory的子类SyncHookCodeFactory。在lib/HookCodeFactory.js找到setup方法:

// lib/HookCodeFactory
setup(instance, options) {
    instance._x = options.taps.map(t => t.fn);
}

这里的instance实际就是SyncHook的实例,而_x就是我们之前绑定事件时候最后的_x。

最后factory.create(options)调用了HookCodeFactory的create方法,这个方法就是实际拼接可执行 JavaScript 代码片段的,具体看下实现:

// lib/HookCodeFactory.js
create(options) {
    this.init(options);
    let fn;
    switch (this.options.type) {
        case "sync":
            fn = new Function(
                this.args(),
                '"use strict";\n' +
                    this.header() +
                    this.content({
                        onError: err => `throw ${err};\n`,
                        onResult: result => `return ${result};\n`,
                        resultReturns: true,
                        onDone: () => "",
                        rethrowIfPossible: true
                    })
            );
            break;
        case "async":
            fn = new Function(
                this.args({
                    after: "_callback"
                }),
                '"use strict";\n' +
                    this.header() +
                    this.content({
                        onError: err => `_callback(${err});\n`,
                        onResult: result => `_callback(null, ${result});\n`,
                        onDone: () => "_callback();\n"
                    })
            );
            break;
        case "promise":
            let errorHelperUsed = false;
            const content = this.content({
                onError: err => {
                    errorHelperUsed = true;
                    return `_error(${err});\n`;
                },
                onResult: result => `_resolve(${result});\n`,
                onDone: () => "_resolve();\n"
            });
            let code = "";
            code += '"use strict";\n';
            code += "return new Promise((_resolve, _reject) => {\n";
            if (errorHelperUsed) {
                code += "var _sync = true;\n";
                code += "function _error(_err) {\n";
                code += "if(_sync)\n";
                code += "_resolve(Promise.resolve().then(() => { throw _err; }));\n";
                code += "else\n";
                code += "_reject(_err);\n";
                code += "};\n";
            }
            code += this.header();
            code += content;
            if (errorHelperUsed) {
                code += "_sync = false;\n";
            }
            code += "});\n";
            fn = new Function(this.args(), code);
            break;
    }
    this.deinit();
    return fn;
}

上面create代码中的重要参数是type,而type是由 Hook 类在 createCompileDelegate("call", "sync")的时候传入进去的,所以调用 call方法,实际type为sync,在 create 中会进入到case 'sync'的分支,在switch中用到最重要的content实际是在class SyncHookCodeFactory extends HookCodeFactory的时候定义的。这里我们就不继续追踪代码生成的逻辑实现了,我们可以直接在最后将 fn的源码console.log出来:console.log(fn.toString()),大致可以得到下面的代码:

// 调用 call 的代码
const hook = new SyncHook(['argName0', 'argName1']);
hook.tap('evtName', (arg0) => {
  console.log(arg0, 1);
});
hook.call('Webpack', 'Tapable');
// 最终得到的源码是:
function anonymous(argName0, argName1) {
  'use strict';
  var _context;
  var _x = this._x;
  var _fn0 = _x[0];
  _fn0(argName0, argName1);
}

上面的_fn0实际就是我们tap绑定的回调函数,argName0和argsName1就是我们实例化SyncHook传入的形参,而我们实际只是在tap的回调中用了arg0一个参数,所以输出的结果是Webpack 1。

总结

Tapable 是 Webpack 的核心模块,Webpack 的所有工作流程都是通过 Tapable 来实现的。Tapable 本质上是提供了多种类型的事件绑定机制,根据不同的流程特点可以选择不同类型的 Hook 来使用。Tapable 的核心实现在绑定事件阶段跟我们平时的自定义 JavaScript 事件绑定(例如 EventEmitter)没有太大区别,但是在事件触发执行的时候,会临时生成可以执行的函数代码片段。通过这种实现方式,Tapable 实现了强大的事件流程控制能力,也增加了如 waterfall/parallel 系列方法,实现了异步/并行等事件流的控制能力。

作者:三水清
链接:https://juejin.cn/post/7039898741075083271

链接: https://www.fly63.com/article/detial/11027

浅谈Webpack打包工具的应用

webpack 在前端工程中随处可见,当前流行的 vue, react, weex 等解决方案都推崇 webpack 作为打包工具。前端工具云集的时代,这是你值得选择的之一。

一步一步webpack,webpack的学习入门

webpack是前端工程构建的一套工具,为什么一个程序称之为一套呢,是因为webpack其实是npm的一个模块,使用起来的话,这期间还需要很多其它模块来进行支持,所以我称之为一套工具。

如何写 Webpack 配置文件

本文从一个小Demo开始,通过不断增加功能来说明webpack的基本配置,只针对新手。webpack基本的配置就可以熟悉了,会引入loader,配置loader选项,会设置alias,会用plugins差不多。

WebPack中Plugins的使用和整理,以及常用的Plugins插件

Plugins是webpack的基础,我们都知道webpage的plugin是基于事件机制工作的,这样最大的好处是易于扩展。讲解如果扩展内置插件和其他插件,以及我们常用的Plugins插件

大多数项目中会用到的webpack小技巧

webpack技巧的总结:进度汇报、压缩、复数文件打包、分离app文件与第三方库文件、资源映射、输出css文件、开发模式、分析包的大小、更小的react项目、更小的Lodash、引入文件夹中所有文件、清除extract-text-webpack-plugin日志。

优化Webpack构建性能的几点建议

Webpack 作为目前最流行的前端构建工具之一,在 vue/react 等 Framework 的生态圈中都占据重要地位。在开发现代 Web 应用的过程中,Webpack 和我们的开发过程和发布过程都息息相关,如何改善 Webpack 构建打包的性能也关系到我们开发和发布部署的效率。

Webpack 4正式发布了!

新版 Webpack 中我们所做的每一个更新目的都在于此,为了当大家在使用 Webpack 的时候敏捷连续毫无顿挫感。 webpack 4 进行构建性能测试,得出的结果非常有趣。结果很惊人,构建时间降低了 60%-98%!

Webpack 4.0.0不再支持 Node.js 4

Webpack 是一个现代 JavaScript 应用程序的模块打包器 (module bundler) 。当 Webpack 处理应用程序时,它会递归地构建一个依赖关系图,其中包含应用程序需要的每个模块

我当初为什么写webpack_Tobias Koppers

Tobias Koppers是一位自由软件开发者,家住德国纽伦堡。他因写出webpack这个已有数百万开发者使用的开源软件而名噪一时。他目前专注于JavaScript和开源项目。以下是我对他个人的专访,希望对大家有所启发。

webpack项目轻松混用css module

本文讲述css-loader开启css模块功能之后,如何与引用的npm包中样式文件不产生冲突。比如antd-mobilenpm包的引入。在不做特殊处理的前提下,样式文件将会被转译成css module。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!