Webpack builds its complex and huge process management system based on Tapable. The tapable architecture not only decouples the process nodes and the concrete implementation of the process, but also ensures the powerful expansion ability of Webpack. Learning to master Tapable helps us understand Webpack in depth.

What is tapable?

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

Tapable provides hook classes for creating plug-ins.

Personally, TAPable is an event-based process management tool.

Ii. Tapable architecture principle and execution process

Tapable was released in 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. The HookCodeFactoruy class dynamically generates a process control function. The way to generate functions is through the familiar New Function(ARG, functionBody).

2.2 Execution Process

Tapable dynamically generates an executable function to control the execution of the hook function. Let’s take SyncHook as an example. Let’s say we have code like this:

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

The above code only registers the hook function, but in order for the function to be executed, the event needs to be raised (to execute the call)

syncHook.call();
Copy the code

Syncook.call () generates a dynamic function like this when called:

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

The code for this function is very simple: it simply takes the functions from an array and executes them. Note: the resulting dynamic function is different depending on how you call it. If the calling code is changed to:

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

So the resulting dynamic function looks like this:

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(); }}}Copy the code

This dynamic function is a bit more complex than the previous one, but if you look at it carefully, the execution logic is also very simple: again, take the functions from an array and execute them one by one; Only this time there are two more logics:

  • Error handling
  • After the function in the array completes execution, the callback function is executed

By studying the resulting dynamic functions, it is not difficult to find that the template characteristics of dynamic functions are very prominent. In the previous example, we registered only X and y2 hooks. This template ensures that dynamic functions can be easily generated when we register any hooks.

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

Three, Hook type detailed explanation

There are 12 types of hooks available in Tapablev2. Next, understand the Hook classes provided by Tapable in terms of how the Hook executes and when the Hook completion callback executes.

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 */
Copy the code

3.2 SyncBailHook

The hook functions are executed in sequence. If a step returns a non-undefined hook, the following hook is not executed. If there is a Hook callback, execute the Hook callback directly.

const hook = new SyncBailHook();
 
hook.tap('x'.() = > {
  console.log('x done');
  return false; // if undefined is returned, y will not be executed
});
hook.tap('y'.() = > console.log('y done'));
hook.callAsync(() = > { console.log('all done')});/* Output: x done all done */
Copy the code

3.3 SyncWaterfallHook

The hook functions are all executed in sequence. The latter hook argument 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 */
Copy the code

3.4 SyncLoopHook

The hook functions are all executed in sequence. Each hook is looped until undefined is returned and the next hook is executed. The Hook callback is executed last.

const hook = new SyncLoopHook();
 
let flag = 0;
let flag1 = 5;
 
hook.tap('x'.() = > {
    flag = flag + 1;
 
    if (flag >= 5) { // Run five times, and then run y
        console.log('x done');
        return undefined;
    } else {
        console.log('x loop');
        return true; }}); hook.tap('y'.() = > {
    flag1 = flag1 * 2;
 
    if (flag1 >= 20) { // Execute 2 times, and 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 only 1 time
    return undefined;
});
 
hook.callAsync(() = > { console.log('all done')});X loop x loop x loop x loop x done y loop x done y done z done all done */
Copy the code

3.5  AsyncParallelHook

Hook functions are executed asynchronously and in parallel. Hook callbacks are not executed until all Hook callbacks have 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:The ${Date.now() - start}`);
});
 
/* Output: x done 1 y done 1 z done 1 all done. Time: 3006 */
Copy the code

3.6 AsyncSeriesHook

Hook functions are executed asynchronously and sequentially, so that the sequence of hooks is guaranteed. The Hook callback is executed last.

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:The ${Date.now() - start}`);
});
 
/* Output: x done 2 y done 1 z done 1 all done. Time: 6008 */
Copy the code

3.7 AsyncParallelBailHook

Hooks are executed asynchronously in parallel, that is, both hooks are executed, but Hook callbacks are executed directly whenever one of the hooks returns non-undefined.

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:The ${Date.now() - start}`);
});
/* Output: x done 1 y done 1 z done 1 all done. Time: 2006 */
Copy the code

3.8 AsyncSeriesBailHook

The hook function executes asynchronously in serial. However, Hook callbacks are executed whenever one of the hooks returns non-undefined, which means that 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 will not execute
    }, 1000);
});
hook.tapAsync('y'.(arg1, callback) = > {
    console.log('y done', arg1);
 
    setTimeout(() = > {
        callback();
    }, 2000);
});
 
hook.callAsync(1.() = > {
    console.log(` all done. Time:The ${Date.now() - start}`);
});
 
/* Output: x done 2 all done. Time: 1006 */
Copy the code

3.9 AsyncSeriesWaterfallHook

Hook functions are executed asynchronously and sequentially, with the parameters returned by the previous hook passed to the next hook. Hook callbacks are not executed until all Hook callbacks have 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); // Does not block the execution of z
    }, 2000)}); hook.tapAsync('z'.(arg, callback) = > {
    console.log('z done', arg);
    callback();
});
 
hook.callAsync(1.(x, arg) = > {
    console.log(`all done, arg: ${arg}. Time:The ${Date.now() - start}`);
});
 
X done 1 y done 2 z done true all done, arg: true Time: 3010 */
Copy the code

3.10 AsyncSeriesLoopHook

The hook function is executed asynchronously and sequentially. The hook function will loop until it returns undefined, and then the next hook will start. 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); // Execute 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:The ${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 */
Copy the code

3.11 HookMap

The main function is Hook grouping, convenient 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) }); }Key1-1: hello key1-2: hello */
Copy the code

3.12 MultiHook

Multihooks are used to batch register Hook functions with hooks.

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);
});
 
// Execute the function
for (const hook of mutiHook.hooks) {
    hook.callAsync('hello'.() = > {
        console.log('hook all done');
    });
}
Copy the code

The above hooks can be abstracted into the following categories:

  • **xxxBailHook: ** Determines whether to execute the next hook depending on whether the previous hook function returns undefined: If one step returns non-undefined, the next hook is not executed.

  • **xxxWaterfallHook: ** The value returned by the previous hook function is the argument to the next function.

  • **xxxLoopHook: ** The hook function loops until it returns undefined.

Note that the hook function returns a value that is evaluated against undefined, not false (null, false).

Hooks can also be classified as synchronous and asynchronous:

  • **syncXXX: ** Sync hook

  • **asyncXXX: ** Async hook

By default, Hook instances have tap, tapAsync, and tapPromise methods for registering Hook callbacks. Dynamic functions generated by different registering methods are different. Of course, not all hooks support these methods. For example, SyncHook does not support tapAsync and tapPromise.

Hooks have call, callAsync, and Promise to perform callbacks by default. But not all hooks have these methods. SyncHook doesn’t support callAsync and Promise, for example.

Fourth, practical application

4.1 Implement jquery.Ajax () encapsulation based on Tapable

Let’s review the general usage of jquery.ajax () :

jQuery.ajax({
    url: 'api/request/url'.beforeSend: function(config) {
        return config; // Returning false cancels the request
    },
    success: function(data) {
        // Success logic
    }
    error: function(err) {
        // Failed logic
    },
    complete: function() {
        // Success and failure will execute the logic}});Copy the code

The jQuery. Ajax process does a few things:

  • BeforeSend provides a hook for request configuration preprocessing before the request is actually sent. If the preprocessor returns false, the request can be canceled.
  • Execute the SUCCESS function logic after the request succeeds (server data returns).
  • If the request fails, the error function logic is executed.
  • Finally, the complete function logic is executed uniformly, whether the request succeeds or fails.

At the same time, we learn from axios, change beforeSend to transformRequest, add transformResponse, add uniform request loading and default error handling, then the whole Ajax process is as follows:

4.2 Simple version implementation

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(); Tap ('LoadingToggle', (show) => {if (show) {console.log(' show ajax-loading'); } else {console.log(' off 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); }); Tap ('DoRequest', (config) => {console.log(' send request configuration: ${json.stringify (config)} '); Const sucData = {code: 0, data: {list: ['X50 Pro', 'IQOO Neo'], user: 'jack'}, message: 'request successful'}; 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; /* Call a custom series process with a Hook. Handling loading 3. Initiate request * / this. Hooks. TransformRequest. CallAsync (enclosing the 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 info: ${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 }); TransformRequest interceptor: Origin:{"base":"/cgi/ CMS /","loading":true}; TransformRequest interceptor: after:{"base":"/cgi/ CMS /","loading":true,"__transformRequest":true}; {"base":"/cgi/ CMS /","loading":true,"__transformRequest":true} transformResponse interceptor: Origin:{"base":"/cgi/cms/","loading":true,"__transformRequest":true}; TransformResponse interceptor: After: {" code ": 0," data ": {" a list" : [" X50 Pro "and" IQOO Neo "], "user" : "jack"}, "message" : "request is successful," "__transformResponse" : true} tabular data: ["X50 Pro","IQOO Neo"] "jack" DoFinally,config={"base":"/cgi/cms/","loading":true,"__transformRequest":true},data={"code":0,"data":{"list":["X50 Pro IQOO ", "Neo"], "user" : "jack"}, "message" : "request", "success __transformResponse" : true} * /Copy the code

We can further refine the above code by abstracting each process point into a separate plug-in and then concatenating it. Such as processing loading show independent LoadingPlugin. Js, returned to pretreatment transformResponse independent into TransformResponsePlugin. Js, so that we can get such a structure:

This structure is basically the same as the well-known Webpack plugin organization. Let’s take a look at tapable in Webpack and see why tapable is a cornerstone of Webpack.

4.3 Application of Tapable in Webpack

  • In Webpack, everything is a Hook.
  • Webpack strings these plug-ins together through tapable to form a fixed flow.
  • Tapable decouples the process task from the concrete implementation, while providing great extensibility: Hook in hand, you can plug in your own logic. We usually write Webpack plug-in, is to find the corresponding Hook to register our own Hook function. This makes it easy to insert our custom logic into the Webpack task flow.

If you need strong process management skills, consider tapable architecture.

Five, the summary

  • Tapable is a process management tool.
  • There are 10 types of Hooks that make it easy to implement complex business processes.
  • The core principle of Tapable is based on configuration and through new Function method, Function expression is dynamically generated in real time to execute, so as to complete logic
  • Tapable realizes process control by connecting process nodes in series to ensure the accurate and orderly process.
  • Each process node can register hook functions at will, providing powerful extensibility.
  • Tapable is the cornerstone of Webpack, which supports the huge plug-in system of Webpack and ensures the orderly running 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