24 浅谈Tapable

浅谈Tapable

Tapable是Webpack中的核心工具,Webpack中许多对象扩展自Tapable类。这个类暴露Tap,TapAsync和TapPromise等方法,可以使用这些方法,注入自定义的构建步骤,这些步骤将在整个编译过程中不同时机触发。

插件类型

通过使用Tapable中提供的不同钩子(Hook)和Tap(Tap,TapAsync,TabPromise)方法,插件可以有着多种不同的方式运行,Compiler hooks分别记录了使用哪些Tapable钩子,指出哪些Tap方法可用。

例如,当钩入compile阶段时,只能使用同步的Tap方法:

compiler.hooks.compile.tap('MyPlugin', params => {
    
    
    console.log('以同步方式触及 compile 钩子。')
})

当然,在可以使用异步钩子的地方,我们还可以使用TapAsync和TabPromise方法:

compiler.hooks.run.tapAsync('MyPlugin', (compiler, callback) => {
    
    
    console.log('以异步方式触及 run 钩子。')
    callback()
})

compiler.hooks.run.tapPromise('MyPlugin', compiler => {
    
    
    return new Promise(resolve => setTimeout(resolve, 1000)).then(() => {
    
    
        console.log('以具有延迟的异步方式触及 run 钩子')
    })
})

为了给其他插件的编译添加一个新的钩子,来 Tap(触及) 到这些插件的内部,直接从Tapable中 require 所需的钩子类(hook class),然后给Compiler对象创建新的hook:

const SyncHook = require('tapable').SyncHook;
// 定义新的hook
if (compiler.hooks.myCustomHook) {
    
    
    throw new Error('Already in use')
}
compiler.hooks.myCustomHook = new SyncHook(['a', 'b', 'c'])
// 在你想要触发钩子的位置/时机下调用……
compiler.hooks.myCustomHook.call(a, b, c);

Compiler钩子

Compiler 模块是Webpack的支柱引擎,它通过 CLI 或 Node API 传递的所有选项,创建出一个Compilation实例。它扩展(extend)自 Tapable 类,以便注册和调用插件。其提供了许多生命周期钩子函数,可以通过如下方式访问相关钩子函数:

compiler.hooks.someHook.tap(/*...*/)

取决于不同的钩子类型,也可以在某些钩子上访问TapAsync 和 TapPromise。

Compilation钩子

Compilation 模块会被 Compiler 用来创建新的编译构建。Compilation 实例能够访问所有的模块和它们的依赖(大部分是循环依赖)。它会对应用程序的依赖图中所有模块进行字面上的编译。在编译阶段,模块会被加载(loaded)、封存(sealed)、优化(optimized)、分块(chunked)、哈希(hashed)和重新创建(restored)。

Compilation类扩展(extend)自 Tapable,也提供了生命周期钩子函数。可以按照Compiler钩子的使用方式,调用Tap:

compilation.hooks.someHook.tap(/*...*/)

和Compiler用法相同,取决于不同的钩子类型,也可以在某些钩子上访问TapAsync和TapPromise。

Resolver

Resolver是由enhanced-resolve package创建出来的,该对象用于解析处理相关模块的路径问题。Resolver 类继承了Tapable类,并且使用Tapable 提供的一些钩子。所有的Compiler实例都可以接触到的Resolver实例。

compiler.resolverFactory.plugin('resolver [type]', resolver => {
    
    
    resolver.hooks.resolve.tapAsync('MyPlugin', params => {
    
    
        // ...
    })
})

Compiler 类有三种类型的内置Resolver:

  1. Normal:通过绝对路径或相对路径,解析一个模块。
  2. Context:通过给定的context解析一个模块。
  3. Loader:解析一个webpack loader。

根据需要,所有这些Compile 用到的内置Resolver,都可以通过插件进行自定义。其中 [type] 是上面提到的三个Resolver之一,指定为:normal,context和loader。

Parser

Parser 是用来解析由Webpack处理过的每个模块。Parser 也是扩展自Tapable的Webpack类,并且提供多种 Tapable 钩子,插件作者可以使用它来自定义解析过程。

以下示例中,Parser位于normalModuleFactory钩子中,因此需要调用额外钩子来进行获取:

ompiler.hooks.normalModuleFactory.tap(factory => {
    
    
    factory.hooks.parser.tap((parser, options) => {
    
    
        parser.hooks.someHook.tap(/*...*/)
    })
})

和Compiler用法相同,取决于不同的钩子类型,也可以在某些钩子上访问TapAsync和TapPromise。

Tapable中钩子的分类

Tapable中钩子总体可以分成两类,一类是同步钩子,同步钩子只能串行使用,另一类就是异步钩子,异步钩子又分为异步串行和异步并行。整体划分如下图所示:
在这里插入图片描述
Tapable中相关钩子的实现就是发布/订阅模式的实现,相比传统的发布/订阅,Tapable进行了更细粒化的操作。

相关钩子的使用

下面介绍Tapable中相关钩子的使用方法及其特点。

SyncHook

新建脚本文件如下:

// 引入同步钩子
const SyncHook = require('tapable').SyncHook;

// 创建一个钩子实例 ame和age为订阅参数
const myHook = new SyncHook(['name', 'age']);
// 订阅
myHook.tap('task1', (name, job) => {
    
    
    console.log('task1', name, job)
});
myHook.tap('task2', (name, job) => {
    
    
    console.log('task2', name, job)
})
// 发布 传入发布值
myHook.call('Render', 'bug制造者')

运行结果如下:
task1 Render bug制造者
task2 Render bug制造者

可以看出,在SyncHook中所有的注册事件都是同步串行执行的。

SyncBailHook

新建脚本文件如下:

// 引入同步钩子
const SyncBailHook = require('tapable').SyncBailHook;

// 创建一个钩子实例 name和age为订阅参数
const myHook = new SyncBailHook(['name', 'age']);
// 订阅
myHook.tap('task1', (name, job) => {
    
    
    console.log('task1', name, job)
});
myHook.tap('task2', (name, job) => {
    
    
    console.log('task2', name, job);
    return '返回不为undefined以外的任何值'
})
myHook.tap('task3', (name, job) => {
    
    
    console.log('task3', name, job);
})
// 发布
myHook.call('Render', 'bug制造者') // 传入发布值:

结果如下:
task1 Render bug制造者
task2 Render bug制造者

可以看出SyncBailHook在其中一个注册事件返回不为undefined的值后,就会中断后面的注册事件执行。

SyncWaterfallHook

新建脚本文件如下:

// 引入同步钩子
const SyncWaterfallHook = require('tapable').SyncWaterfallHook;

// 创建一个钩子实例 name和age为订阅参数
const myHook = new SyncWaterfallHook(['name', 'age']);
// 订阅
myHook.tap('task1', (name, job) => {
    
    
    console.log('task1', name, job)
});
myHook.tap('task2', (name, job) => {
    
    
    console.log('task2', name, job);
    // 返回值作为下一个注册事件的回调函数入参
    return 'tapable';
});
myHook.tap('task3', (name, job) => {
    
    
    console.log('task3', name, job)
});
// 发布
myHook.call('Render', 'bug制造者') // 传入发布值

结果如下:
task1 Render bug制造者
task2 Render bug制造者
task3 tapable bug制造者

可以看出SyncWaterfallHook中的一个注册事件的返回值,会作为下一个注册事件回调函数的入参。

AsyncParallelHook

新建脚本文件如下:

// 引入异步钩子
const AsyncParallelBailHook = require('tapable').AsyncParallelBailHook;

// 创建一个钩子实例
const myHook = new AsyncParallelBailHook(['name', 'age']);
// 订阅
myHook.tapAsync('task1', (name, job, callback) => {
    
    
    setTimeout(() => {
    
    
        console.log('task1', name, job, Date.now());
        callback();
    }, 1000)
});
myHook.tapAsync('task2', (name, job, callback) => {
    
    
    setTimeout(() => {
    
    
        console.log('task2', name, job, Date.now());
        callback();
    }, 1000)
});
// 发布
myHook.callAsync('Render', 'bug制造者', () => {
    
    
    console.log('异步并行执行')
})

结果如下:
task1 Render bug制造者 1599576050627
task2 Render bug制造者 1599576050641
异步并行执行

Task1和Task2执行的时间间隔差不多是一秒(代码执行也需要时间,误差可以接受),可以说明AsyncParallelHook是异步并行的,在这里我们注册事件使用了TapAsync,发布的时候使用CallAsync,实现了相关操作。

还可以使用tapPromise和promise来实现异步并行操作:

// 引入异步钩子
const AsyncParallelBailHook = require('tapable').AsyncParallelBailHook;

// 创建一个钩子实例
const myHook = new AsyncParallelBailHook(['name', 'age']);
// 订阅
myHook.tapAsync('task1', (name, job) => {
    
    
    return new Promise((resolve, reject) => {
    
    
        setTimeout(() => {
    
    
            console.log('task1', name, job, Date.now());
        }, 1000)
    })
});
myHook.tapAsync('task2', (name, job, callback) => {
    
    
    return new Promise((resolve, reject) => {
    
    
        setTimeout(() => {
    
    
            console.log('task2', name, job, Date.now());
        }, 1000)
    })
});
// 发布
myHook.promise('Render', 'bug制造者')

结果如下:
task1 Render bug制造者 1599576251096
task2 Render bug制造者 1599576251107

到这里我们简单的介绍了几个钩子的用法和知道了其特点,当然Tapable远不止于此,比如还可以设置钩子拦截器,让我们的钩子更加充满活力。希望读者可以私下的深入去了解一下。如果读者对钩子的原理感兴趣,可以前往https://www.npmjs.com/package/tapable进行查阅。

模拟Tapable钩子实现

在这里实现几个简易的钩子,主要实现其核心部分,以便更好的掌握它,从而对Webpack内部实现机制进行更深入的了解。

SyncHook简易实现

// 创建同步钩子
class SyncHook {
    
    
    constructor(arg) {
    
    
        this.arg = arg;
        this.tasks = [];
    }
    tap(name, fn) {
    
    
        this.tasks.push(fn);
    }
    call(...arg) {
    
    
        this.tasks.forEach(fn => fn(...arg))
    }
}

// 创建一个钩子实例
const myHook = new SyncHook(['name', 'age']);
myHook.tap('task1', (name, job) => {
    
    
    console.log('task1', name, job)
});
myHook.tap('task2', (name, job) => {
    
    
    console.log('task2', name, job)
})
// 发布
myHook.call('Render', 'bug制造者') // 传入发布值

SyncWaterfallHook简易实现

// 创建同步钩子
class SyncBailHook {
    
    
    constructor(arg) {
    
    
        this.arg = arg;
        this.tasks = [];
    }
    tap(name, fn) {
    
    
        this.tasks.push(fn);
    }
    call(...arg) {
    
    
        let result;
        for (let i = 0, len = this.tasks.length; i < len; i++) {
    
    
            if (i === 0) {
    
    
                result = this.tasks[i](...arg);
            } else {
    
    
                if (result !== undefined) {
    
    
                    break;
                } else {
    
    
                    result = this.tasks[i](...arg);
                }
            }
        }
    }
}

// 创建一个钩子实例
const myHook = new SyncBailHook(['name', 'age']);
// 订阅
myHook.tap('task1', (name, job) => {
    
    
    console.log('task1', name, job)
});
myHook.tap('task2', (name, job) => {
    
    
    console.log('task2', name, job);
    return '返回不为undefined以外的任何值'
})
myHook.tap('task3', (name, job) => {
    
    
    console.log('task3', name, job);
})
// 发布
myHook.call('Render', 'bug制造者') // 传入发布值

到这里我们通过Tapable中相关钩子的使用,了解了其特点,应该对Tapable有了一个大概的认知,因为Tapable,才使得我们的Webpack变得如此灵活。

本章节提供案例源码下载:https://gitee.com/mvc_ydb/webpack/blob/master/tapable-demo.zip

猜你喜欢

转载自blog.csdn.net/sinat_41212418/article/details/121844383
24