webpack 之 tapable 学习

前言

webpack 大家应该都耳熟能详了。个人感觉,webpack的本质就是让一堆的 LoaderPluginwebpack的可支配范围内,有序可控的执行,最终生成一堆可在浏览器中执行的 code 和 一些状态信息。而这些 LoaderPlugin,有用户自定义的,也有webpack 自己内部定义的。

Loader 的运行机制,不是这篇文章讲述的内容,有需要的朋友,可以看下我之前的这篇文章:webpack之 loader。

webpack的设计思想还是很好的,我觉得这个思想和渐进增强有异曲同工之妙。它自己实现一套打包的主流程,然后在 ComplierComplation 对象上暴露出一些钩子,这些钩子起到了类似于生命周期的作用,允许用户在不同的打包阶段通过钩子来增加用户所需的各种各样的功能。webpack聪明的将一些不确定因素抛给使用者去处理,也就是:标准我来定,细节和扩展你自己弄。

举个栗子,Complier对象上的钩子,就有这么多 compiler钩子

那么这些钩子是什么呢?它们其实都是 tapable 的实例。

那么我们怎么调用这些钩子呢?通过编写 webpack Plugin

今天我们来分析下 webpack 插件机制的基石 - tapable。考虑到 wepback4,是使用 tapable1.1.0 版本,所以这篇文章就用 1.1.0 的版本来分析。

注意:1.x.x版本和0.x.x版本,使用方式是有区别的,代码也被重构过。

如果对 tapable 还未有了解的朋友,可以参考下这里:

[email protected]

这才是官方的tapable中文文档

概念与用法

概念

我觉得tapable 整个库其实就是一套 发布订阅模式 的实现,类似 nodejs 的 EventEmitter

有的道友说是生产者消费者模式, 发布订阅模式本质上也是一种生产者消费者模式,至于他们的区别,应该就是发布订阅模式的功能更单一,而生产者消费者模式的抽象级别更高。到底是 发布订阅模式 还是 生产者消费者模式,我不能确定,有待考究,聪明的你如果知道的话,可以评论告诉下我哦。

用法

tapable 支持三种方式注册插件名称 ,分别是 tap, tapAsync, tapPromise

tap 表示使用的同步钩子,tapAsynctapPromise 表示使用的是异步钩子。

与此对应的,支持三种调用方式 call, callAsyncpromise,注意需要一一对应。

SyncHook 举个栗子

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

// 初始化时传入参数名称
const myHook = new SyncHook(['name', 'age']);

// 添加事件
myHook.tap('pluginName1', (name, age) => console.log('pluginName1', name, age))
myHook.tap('pluginName2', (name, age) => console.log('pluginName2', name, age))

// 触发
myHook.call('jk', 26);

// 输出
// pluginName1 jk 26
// pluginName2 jk 26
复制代码

有木有发现和我们使用 addEventListener 添加事件非常相似?是的,就是这么像。所以不要怕它,我们只需要注意, 声明 Hook 时,传入预置的参数名称,然后用 tap监听事件,用 call传入参数触发事件。

用起来和 jQueryontrigger 一个意思,当然内部处理流程是不一样。

这里我们用的是 SyncHook,还有 Async 类型的 Hook,也就是异步钩子,用起来也是类似的。

tapAsync监听,callAsync 触发

tapPromise监听,promise 触发

代码如下:

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

class Model {
    constructor() {
        this.hooks = {
            asyncHook: new AsyncParallelHook(['name']),
            promiseHook: new AsyncParallelHook(['age'])
        };
    }

    callAsyncHook(name, callback) {
        this.hooks.asyncHook.callAsync(name, err => {
            if (err) return callback(err);
            callback(null);
        });
    }

    callPromiseHook(age) {
        return this.hooks.promiseHook.promise(age).then(res => console.log(res));
    }
}

const model = new Model();

// Async 方式监听事件
model.hooks.asyncHook.tapAsync('AsyncPluginName', (name, callback) => {
    const pluginName = 'AsyncPluginName'; 
    setTimeout( () => {
        console.log(pluginName, name);
    }, 2000);
});
model.callAsyncHook('jk');
// 2秒后输出:AsyncPluginName jk

// Promise 方式监听事件
model.hooks.promiseHook.tapPromise('PromisePluginName', (age) => {
    const pluginName = 'PromisePluginName';
    return new Promise((resolve, reject) => {        
        setTimeout(() => {
            console.log(pluginName, age);
            resolve(pluginName);
        }, 4000);
    });
});
model.callPromiseHook(26);
// 4秒后输出:PromisePluginName 26
复制代码

实践:编写一个 Plugin

Webpack Plugin 有一套固定格式,这里以 Async event hooks 举例

其他的类型,可参考官方文档

const pluginName = 'HelloWorldPlugin';

class HelloWorldPlugin {
    apply(complier) {
        compiler.hooks.someHook.tapAsync(
            pluginName,
            (compilation, callback) => {
                // Do something async... 
                setTimeout( () => {
                    console.log('Done with async work...');
                    callback();
                }, 1000);
            }
        )
    }
}

module.exports = HelloWorldPlugin;
复制代码
有的同学可能会好奇,为什么在 webpack 的 plugins 参数里,配置一些插件的实例,就会出现神奇的效果。

因为 webpack 在初始化 时,会遍历 plugins 参数中的实例,依次调用实例的 apply 方法,并将 complier 作为参数。

源码出自: webpack\lib\webpack.js

到这里应该能明白,为什么 webpack 的插件需要按照那样的格式去写了。

基于 HTMLWebpackPlugin 扩展一个 Plugin

目录结构如图

MyPlugin.js 代码如下,我这里是直接 copy HTMLWebpackPlugin 官方文档的。

const HtmlWebpackPlugin = require('html-webpack-plugin');

const pluginName = 'MyPlugin';

class MyPlugin {
    apply(compiler) {
        compiler.hooks.compilation.tap(pluginName, (compilation) => {
            console.log('The compiler is starting a new compilation...')

            // Staic Plugin interface |compilation |HOOK NAME | register listener 
            HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tapAsync(
                pluginName, // <-- Set a meaningful name here for stacktraces
                (data, cb) => {
                    // Manipulate the content
                    data.html += 'The Magic Footer'
                    // Tell webpack to move on
                    cb(null, data)
                }
            )
        })
    }
}

module.exports = MyPlugin;
复制代码

注意一点:HTMLWebpackPlugin 默认安装 3.2.0 版本,这个版本还只是支持旧版的插件写法,没有 HtmlWebpackPlugin.getHooks 这个方法。我这里安装的是最新的master分支。

一切就绪后,npm run prod,可看到 dist/index.html的内容如下:

一个简单的插件算是写好并运行成功了。

写在最后

希望本文能对读者有帮助。

如果有错误的地方,还请指出。

谢谢阅读。

代码在此:webpack-plugin-test

参考

[email protected]

Writing a Plugin

html-webpack-plugin

这才是官方的tapable中文文档

不满足于只会使用系列: tapable

生产者消费者模式与订阅发布者模式的区别

推荐两篇好文:

Webpack基本架构浅析

Webpack 核心模块 tapable 解析

转载于:https://juejin.im/post/5c96e286e51d450227417691

你可能感兴趣的:(webpack,javascript,ViewUI)