Tapable, the cornerstone of webpack

Time:2021-5-5

Webpack builds its complex and huge process management system based on tapable. The architecture based on tapable not only decouples the process nodes and the specific implementation of the process, but also ensures the powerful scalability of webpack; Learning and mastering tapable is helpful for us to have a deep understanding of webpack.

1、 What is a tapable?

The tapable package expose many Hook classes,which can be used to create hooks for plugins.

Tapable provides some hook classes for creating plug-ins.

I think tapable is an event based process management tool.

2、 Principle and implementation process of tapable architecture

Tapable released v2.0 on September 18, 2020. This article is also based on v2.0.

2.1 code architecture

Tapable has two base classes: hook and hookcodefactory. The hook class defines the hook   Interface (hook interface),   The function of hookcodefactoruy class is to dynamically generate a process control function. The way to generate functions is through the familiar new   Function(arg,functionBody)。

Tapable, the cornerstone of webpack

2.2 implementation process

Tapable dynamically generates an executable function to control the execution of the hook function. Let’s take the use of synchook as an example. For example, we have such a piece of code:

//Synchook use
import { SyncHook } from '../lib';
const syncHook = new SyncHook();
syncHook.tap('x', () => console.log('x done'));
syncHook.tap('y', () => console.log('y done'));

The above code only registers the hook function. In order for the function to be executed, the event (execution call) needs to be triggered

syncHook.call();

Synchook. Call() generates such a dynamic function when it is called:

function anonymous() {
    "use strict";
    var _context;
    var _x = this._x;
    var _fn0 = _x[0];
    _fn0();
    var _fn1 = _x[1];
    _fn1();
}

The code of this function is very simple: take the function out of an array and execute it in turn. Note: the dynamic functions generated by different calling methods are different. If the calling code is changed to:

syncHook.callAsync( () => {console.log('all done')} )

Then the final generated dynamic function is as follows:

function anonymous(_callback) {
    "use strict";
    var _context;
    var _x = this._x;
    var _fn0 = _x[0];
    var _hasError0 = false;
    try {
        _fn0();
    } catch(_err) {
        _hasError0 = true;
        _callback(_err);
    }
    if(!_hasError0) {
        var _fn1 = _x[1];
        var _hasError1 = false;
        try {
            _fn1();
        } catch(_err) {
            _hasError1 = true;
            _callback(_err);
        }
        if(!_hasError1) {
            _callback();
        }
    }
}

Compared with the previous dynamic functions, this dynamic function is more complex, but if you look at it carefully, the execution logic is also very simple: it takes the function out of the array and executes it in turn; But this time there are two more logics:

  • error handling
  • After the functions in the array are executed, the callback function is executed

Through the study of the final generated dynamic function, it is not difficult to find that the template feature of dynamic function is very prominent. In the previous example, we only registered X and Y2 hooks. This template ensures that when we register any hook, the dynamic function can be easily generated, which has very strong extensibility.

So how are these dynamic functions generated? In fact, the generation process of hook is the same. Hook.tap just completes parameter preparation. The real dynamic function generation is after calling (after the tap is turned on). The whole process is as follows:

Tapable, the cornerstone of webpack

3、 Hook type

In tapablev2, a total of 12 types of hooks are provided. Next, we can understand these hook classes provided by tapable by combing how to execute the hook and when to complete the callback.

3.1 SyncHook

Hook functions are executed in sequence; If there is a hook callback, the hook callback is executed at the end.

const syncHook = new SyncHook();
syncHook.tap('x', () => console.log('x done'));
syncHook.tap('y', () => console.log('y done'));
syncHook.callAsync(() => { console.log('all done') });
 
/*
Output:
x done
y done
all done
*/

3.2 SyncBailHook

Hook functions are executed in order. If the hook of a certain step returns non undefined, the subsequent hook will not be executed; If there is a hook callback, the hook callback is executed directly.

const hook = new SyncBailHook();
 
hook.tap('x', () => {
  console.log('x done');
  return false; //  Returned non undefined, y will not execute
});
hook.tap('y', () => console.log('y done'));
hook.callAsync(() => { console.log('all done') });
 
/*
Output:
x done
all done
*/

3.3 SyncWaterfallHook

Hook functions are executed in sequence. The parameter of the latter hook is the return value of the previous hook. Finally, the hook callback is executed.

const hook = new SyncWaterfallHook(['count']);
 
hook.tap('x', (count) => {
    let result = count + 1;
    console.log('x done', result);
    return result;
});
hook.tap('y', (count) => {
    let result = count * 2;
    console.log('y done', result);
    return result;
});
hook.tap('z', (count) => {
    console.log('z done & show result', count);
});
hook.callAsync(5, () => { console.log('all done') });
 
/*
Output:
x done 6
y done 12
z done & show result 12
all done
*/

3.4 SyncLoopHook

Hook functions are executed in sequence. The hook of each step is executed in a loop until the return value is undefined, and then the next hook is executed. The hook callback is finally executed.

const hook = new SyncLoopHook();
 
let flag = 0;
let flag1 = 5;
 
hook.tap('x', () => {
    flag = flag + 1;
 
    If (flag > = 5) {// execute 5 times, then y
        console.log('x done');
        return undefined;
    } else {
        console.log('x loop');
        return true;
    }
});
hook.tap('y', () => {
    flag1 = flag1 * 2;
 
    If (flag1 > = 20) {// execute twice, then execute Z
        console.log('y done');
        return undefined;
    } else {
        console.log('y loop');
        return true;
    }
});
hook.tap('z', () => {
    console.log('z done'); //  Z returns undefined directly, so it is executed only once
    return undefined;
});
 
hook.callAsync(() => { console.log('all done') });
 
/*
Output:
x loop
x loop
x loop
x loop
x done
y loop
x done
y done
z done
all done
 */

3.5  AsyncParallelHook

Hook functions are executed asynchronously and in parallel. The hook callback is executed only after all hook callbacks are returned.

const hook = new AsyncParallelHook(['arg1']);
const start = Date.now();
 
hook.tapAsync('x', (arg1, callback) => {
    console.log('x done', arg1);
 
    setTimeout(() => {
        callback();
    }, 1000)
});
hook.tapAsync('y', (arg1, callback) => {
    console.log('y done', arg1);
 
    setTimeout(() => {
        callback();
    }, 2000)
});
hook.tapAsync('z', (arg1, callback) => {
    console.log('z done', arg1);
 
    setTimeout(() => {
        callback();
    }, 3000)
});
 
hook.callAsync(1, () => {
    console.log(`all done。  Time spent: ${date. Now () - start} ';
});
 
/*
Output:
x done 1
y done 1
z done 1
all done。  Time: 3006
*/

3.6 AsyncSeriesHook

When all hook functions are executed asynchronously and serially, the sequence of hook execution will be ensured. After the end of the previous hook, the next one will start. The hook callback is finally executed.

const hook = new AsyncSeriesHook(['arg1']);
const start = Date.now();
 
hook.tapAsync('x', (arg1, callback) => {
    console.log('x done', ++arg1);
 
    setTimeout(() => {
        callback();
    }, 1000)
});
hook.tapAsync('y', (arg1, callback) => {
    console.log('y done', arg1);
 
    setTimeout(() => {
        callback();
    }, 2000)
});
 
hook.tapAsync('z', (arg1, callback) => {
    console.log('z done', arg1);
 
    setTimeout(() => {
        callback();
    }, 3000)
});
 
hook.callAsync(1, () => {
    console.log(`all done。  Time spent: ${date. Now () - start} ';
});
 
/*
Output:
x done 2
y done 1
z done 1
all done。  Time: 6008
*/

3.7 AsyncParallelBailHook

Hooks are executed asynchronously in parallel, that is, all hooks are executed, but as long as a hook returns non undefined, the hook callback will be executed directly.

const hook = new AsyncParallelBailHook(['arg1']);
const start = Date.now();
 
hook.tapAsync('x', (arg1, callback) => {
    console.log('x done', arg1);
 
    setTimeout(() => {
        callback();
    }, 1000)
});
hook.tapAsync('y', (arg1, callback) => {
    console.log('y done', arg1);
 
    setTimeout(() => {
        callback(true);
    }, 2000)
});
 
hook.tapAsync('z', (arg1, callback) => {
    console.log('z done', arg1);
 
    setTimeout(() => {
        callback();
    }, 3000)
});
 
hook.callAsync(1, () => {
    console.log(`all done。  Time spent: ${date. Now () - start} ';
});
/*
Output:
x done 1
y done 1
z done 1
all done。  Time consuming: 2006
 */

3.8 AsyncSeriesBailHook

Hook functions are executed asynchronously and serially. However, as long as a hook returns non undefined, the hook callback will be executed, that is to say, some hooks may not be executed.

const hook = new AsyncSeriesBailHook(['arg1']);
const start = Date.now();
 
hook.tapAsync('x', (arg1, callback) => {
    console.log('x done', ++arg1);
 
    setTimeout(() => {
        callback(true); //  Y won't do it
    }, 1000);
});
hook.tapAsync('y', (arg1, callback) => {
    console.log('y done', arg1);
 
    setTimeout(() => {
        callback();
    }, 2000);
});
 
hook.callAsync(1, () => {
    console.log(`all done。  Time spent: ${date. Now () - start} ';
});
 
/*
Output:
x done 2
all done。  Time: 1006
 */

3.9 AsyncSeriesWaterfallHook

All hook functions are executed asynchronously and serially, and the parameters returned by the previous hook will be passed to the next hook. Hook callbacks are executed after all hook callbacks are returned.

const hook = new AsyncSeriesWaterfallHook(['arg']);
const start = Date.now();
 
hook.tapAsync('x', (arg, callback) => {
    console.log('x done', arg);
 
    setTimeout(() => {
        callback(null, arg + 1);
    }, 1000)
},);
 
hook.tapAsync('y', (arg, callback) => {
    console.log('y done', arg);
 
    setTimeout(() => {
        callback(null, true); //  It doesn't stop z from executing
    }, 2000)
});
 
hook.tapAsync('z', (arg, callback) => {
    console.log('z done', arg);
    callback();
});
 
hook.callAsync(1, (x, arg) => {
    console.log(`all done, arg: ${arg}。  Time spent: ${date. Now () - start} ';
});
 
/*
Output:
x done 1
y done 2
z done true
all done, arg: true。  Time: 3010
 */

3.10 AsyncSeriesLoopHook

All hook functions are executed asynchronously and serially. In a certain step, the hook function will be executed circularly until it returns non undefined before the next hook starts. Hook callbacks are executed after all hook callbacks are completed.

const hook = new AsyncSeriesLoopHook(['arg']);
const start = Date.now();
let counter = 0;
 
hook.tapAsync('x', (arg, callback) => {
    console.log('x done', arg);
    counter++;
 
    setTimeout(() => {
        if (counter >= 5) {
            callback(null, undefined); //  Start execution y
        } else {
            callback(null, ++arg); // callback(err, result)
        }
    }, 1000)
},);
 
hook.tapAsync('y', (arg, callback) => {
    console.log('y done', arg);
 
    setTimeout(() => {
        callback(null, undefined);
    }, 2000)
});
 
hook.tapAsync('z', (arg, callback) => {
    console.log('z done', arg);
    callback(null, undefined);
});
 
hook.callAsync('AsyncSeriesLoopHook', (x, arg) => {
    console.log(`all done, arg: ${arg}。  Time spent: ${date. Now () - start} ';
});
 
/*
x done AsyncSeriesLoopHook
x done AsyncSeriesLoopHook
x done AsyncSeriesLoopHook
x done AsyncSeriesLoopHook
x done AsyncSeriesLoopHook
y done AsyncSeriesLoopHook
z done AsyncSeriesLoopHook
all done, arg: undefined。  Time: 7014
*/

3.11 HookMap

The main function is hook grouping, which is convenient for hook group batch call.

const hookMap = new HookMap(() => new SyncHook(['x']));
 
hookMap.for('key1').tap('p1', function() {
    console.log('key1-1:', ...arguments);
});
hookMap.for('key1').tap('p2', function() {
    console.log('key1-2:', ...arguments);
});
hookMap.for('key2').tap('p3', function() {
    console.log('key2', ...arguments);
});
 
const hook = hookMap.get('key1');
 
if( hook !== undefined ) {
    hook.call('hello', function() {
        console.log('', ...arguments)
    });
}
 
/*
Output:
key1-1: hello
key1-2: hello
*/

3.12 MultiHook

Multihook is mainly used to batch register hook functions with hook.

const syncHook = new SyncHook(['x']);
const syncLoopHook = new SyncLoopHook(['y']);
const mutiHook = new MultiHook([syncHook, syncLoopHook]);
 
//Register the same function with multiple hooks
mutiHook.tap('plugin', (arg) => {
    console.log('common plugin', arg);
});
 
//Executive function
for (const hook of mutiHook.hooks) {
    hook.callAsync('hello', () => {
        console.log('hook all done');
    });
}

The above hooks can be abstracted into the following categories:

  • xxxBailHook:Whether to execute the next hook depends on whether the return value of the previous hook function is undefined or not: if a step returns non undefined, the subsequent hook will not be executed.
  • xxxWaterfallHook:The return value of the previous hook function is the parameter of the next function.
  • xxxLoopHook:The hook function loops until the return value is undefined.

Note that the return value of hook function is compared with undefined value, not with false value (null, false)

Hook can also be divided into synchronous and asynchronous

  • syncXXX:Synchronization hook
  • asyncXXX:Asynchronous hook

Hook instances have taps by default,   tapAsync,   Tappromise three registration hook callback methods, different registration methods generate different dynamic functions. Of course, not all hooks support these methods. For example, synchook does not support tapasync,   tapPromise。

Hook has call by default,   Call async, promise to execute the callback. But not all hooks have these methods. For example, synchook does not support callasync and promise.

4、 Practical application

4.1 encapsulation of jQuery. Ajax () class based on tapable

Let’s review the general usage of jQuery. Ajax() (I don’t worry about the correctness of every parameter)

jQuery.ajax({
    url: 'api/request/url',
    beforeSend: function(config) {
        return config; //  Returning false will cancel the request
    },
    success: function(data) {
        //The logic of success
    }
    error: function(err) {
        //Failure logic
    },
    complete: function() {
        //The logic that success and failure will be executed
    }
});

The whole process of jquery.ajax does the following things:

  • Before the request is actually sent, beforeSend provides a hook for request configuration preprocessing. If the preprocessing function returns false, the sending of this request can be cancelled.
  • If the request is successful (after the server data is returned), execute the success function logic.
  • If the request fails, the error function logic is executed.
  • Finally, the complete function logic is executed uniformly, regardless of whether the request succeeds or fails.

Tapable, the cornerstone of webpack

At the same time, we learn from Axios, change beforeSend to transformrequest, add transformresponse, add unified request loading and default error handling. At this time, our entire Ajax process is as follows:

Tapable, the cornerstone of webpack

Implementation of 4.2 simple version

const { SyncHook, AsyncSeriesWaterfallHook } = require('tapable');
 
class Service {
    constructor() {
        this.hooks = {
            loading:  new SyncHook(['show']),
            transformRequest: new AsyncSeriesWaterfallHook(['config', 'transformFunction']),
            request: new SyncHook(['config']),
            transformResponse: new AsyncSeriesWaterfallHook(['config', 'response', 'transformFunction']),
            success: new SyncHook(['data']),
            fail: new SyncHook(['config', 'error']),
            finally: new SyncHook(['config', 'xhr'])
        };
 
        this.init();
    }
    init() {
        //Task logic after decoupling
        this.hooks.loading.tap('LoadingToggle', (show) => {
            if (show) {
                Console. Log ('show Ajax loading ');
            } else {
                Console. Log ('close Ajax loading ');
            }
        });
 
        this.hooks.transformRequest.tapAsync('DoTransformRequest', (
            config,
            transformFunction= (d) => {
                d.__transformRequest = true;
                return d;
            },
            cb
        ) => {
            Console.log (` transformrequest Interceptor: origin: ${JSON. Stringify (config)}; ');
            config = transformFunction(config);
            Console.log (` transformrequest Interceptor: After: ${JSON. Stringify (config)}; ');
            cb(null, config);
        });
 
        this.hooks.transformResponse.tapAsync('DoTransformResponse', (
            config,
            data,
            transformFunction= (d) => {
                d.__transformResponse = true;
                return d;
            },
            cb
        ) => {
            Console.log (` transformresponse Interceptor: origin: ${JSON. Stringify (config)}; ');
            data = transformFunction(data);
            Console.log (` transformresponse Interceptor: After: ${JSON. Stringify (data)} ');
            cb(null, data);
        });
 
        this.hooks.request.tap('DoRequest', (config) => {
            Console.log (` send request configuration: ${JSON. Stringify (config)} '));
 
            //Analog data return
            const sucData = {
                code: 0,
                data: {
                    list: ['X50 Pro', 'IQOO Neo'],
                    user: 'jack'
                },
                Message: 'request succeeded'
            };
 
            const errData = {
                code: 100030,
                Message: 'not logged in, please log in again'
            };
 
            if (Date.now() % 2 === 0) {
                this.hooks.transformResponse.callAsync(config, sucData, undefined, () => {
                    this.hooks.success.callAsync(sucData, () => {
                        this.hooks.finally.call(config, sucData);
                    });
                });
            } else {
                this.hooks.fail.callAsync(config, errData, () => {
                    this.hooks.finally.call(config, errData);
                });
            }
        });
    }
    start(config) {
        this.config = config;
 
        /*
            Calling custom tandem process through hook
            1. Transformrequest first
            2. Handling loading
            3. Initiate a request
         */
        this.hooks.transformRequest.callAsync(this.config, undefined, () => {
            this.hooks.loading.callAsync(this.config.loading, () => {
            });
 
            this.hooks.request.call(this.config);
        });
    }
}
 
const s = new Service();
 
s.hooks.success.tap('RenderList', (res) => {
    const { data } = res;
    Console.log (` list data: ${JSON. Stringify (data. List)} ');
});
 
s.hooks.success.tap('UpdateUserInfo', (res) => {
    const { data } = res;
    Console.log (` user information: ${JSON. Stringify (data. User)} ');
});
 
s.hooks.fail.tap('HandlerError', (config, error) => {
    Console. Log (` request failed, config = ${JSON. Stringify (config)}, error = ${JSON. Stringify (error)} ');
});
 
s.hooks.finally.tap('DoFinally', (config, data) => {
    console.log(`DoFinally,config=${JSON.stringify(config)},data=${JSON.stringify(data)}`);
});
 
s.start({
    base: '/cgi/cms/',
    loading: true
});
 
/*
Output returned successfully:
Transformrequest Interceptor: origin: {"base": "/ CGI / CMS /", "loading": true};
Transformrequest Interceptor: After: {base ': "/ CGI / CMS /", "loading": true__ transformRequest":true};
Show Ajax loading
Send request configuration: {"base": "/ CGI / CMS /", "loading": true, "__ transformRequest":true}
Transformresponse Interceptor: origin: {base ': "/ CGI / CMS /", "loading": true, "__ transformRequest":true};
Transformresponse Interceptor: After: {"code": 0, "data": {"list": ["X50 Pro", "iqoo Neo"], "user": "Jack"}, "message": "request succeeded", "user": "request failed", "user": "request failed"__ transformResponse":true}
List data: ["X50 Pro", "iqoo Neo"]
User information: "Jack"
DoFinally,config={"base":"/cgi/cms/","loading":true,"__ Transformrequest ": true}, data = {" code ": 0," data ": {" list ": [" X50 Pro "," iqoo Neo "]," user ":" Jack "}," message ":" request succeeded "__ transformResponse":true}
*/

The above code, we can continue to optimize: each process point is abstracted into an independent plug-in, and finally connected in series. For example, the processing of loading display is independent of loadingplugin.js, and the return of preprocessing transformresponse is independent of transformresponseplugin.js

Tapable, the cornerstone of webpack

This structure is basically the same as the famous webpack organization plug-in. Next, let’s take a look at the application of tapable in webpack and see why tapable can be called the cornerstone of webpack.

4.3 application of tapable in webpack

  • In webpack, everything is a hook.
  • Webpack connects these plug-ins through tapable to form a fixed process.
  • Tapable decouples the process tasks and concrete implementation, and provides powerful expansion ability: you can insert your own logic when you get the hook( We usually write webpack plug-ins, that is to find the corresponding hook, and then register our own hook function. In this way, we can easily insert our custom logic into the webpack task flow.

If you need strong process management ability, you can consider architecture design based on tapable.

5、 Summary

  • Tapable is a process management tool.
  • There are 10 types of hook, which can make it easy for us to implement complex business processes.
  • The core principle of tapable is based on configuration, through new   Function mode, real-time dynamic generation function expression to execute, so as to complete the logic
  • Tapable realizes process control by connecting process nodes in series to ensure the accuracy and order of the process.
  • Each process node can register hook function arbitrarily, which provides powerful expansion ability.
  • Tapable is the cornerstone of webpack. It supports the huge plug-in system of webpack and ensures the orderly operation of these plug-ins.
  • If you are also working on a complex process system (task system), consider using tapable to manage your process.

Author: vivo Ou Fujun

Recommended Today

Analysis of super comprehensive MySQL statement locking (Part 1)

A series of articles: Analysis of super comprehensive MySQL statement locking (Part 1) Analysis of super comprehensive MySQL statement locking (Part 2) Analysis of super comprehensive MySQL statement locking (Part 2) Preparation in advance Build a system to store heroes of the Three KingdomsheroTable: CREATE TABLE hero ( number INT, name VARCHAR(100), country varchar(100), PRIMARY […]