Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【webpack进阶系列】webpack插件骨架-Tapable #100

Open
amandakelake opened this issue Feb 1, 2020 · 0 comments
Open

【webpack进阶系列】webpack插件骨架-Tapable #100

amandakelake opened this issue Feb 1, 2020 · 0 comments

Comments

@amandakelake
Copy link
Owner

GitHub - webpack/tapable: Just a little module for plugins.

Tapable总览

Tap 的英文单词解释,除了最常用的 点击 手势之外,还有一个意思是 水龙头 —— 在 webpack 中指的是后一种

Webpack 可以认为是一种基于事件流的编程范例,内部的工作流程都是基于 插件 机制串接起来,整个骨架基于Tapable,Tapable 是 Eventemitter 的升级版本,包含了 同步/异步 发布订阅模型, 在它的异步模型里又分为 串行/并行

8C48C3DA-6528-4737-9144-262038CB80A4

基础使用

GitHub - webpack/tapable: Just a little module for plugins.

1、同步与异步

① 同步钩子 tap/call

import { SyncHook } from 'tapable';

class FrontEnd {
    constructor() {
        this.hooks = {
            framework: new SyncHook(['framework']),
        };
    }

    learnFramework(framework) {
        this.hooks.framework.call(framework);
    }
}

const Xiaobai = new FrontEnd();

// 注册钩子回调
Xiaobai.hooks.framework.tap('learn framework', framework => {
    console.log(`Xiaobai learn ${framework}`);
});

// 触发钩子
Xiaobai.learnFramework('vue');
Xiaobai.learnFramework('react');

// Xiaobai learn vue
// Xiaobai learn react

② 异步钩子 tapAsync/callAsync + tapPromise/promise

import { AsyncParallelHook } from 'tapable';

class FrontEnd {
    constructor() {
        this.hooks = {
            bundler: new AsyncParallelHook(['bundler']), // 异步钩子,先忽略Parallel
        };
    }

    learnBundler(bundler) {
        console.time('learnBundler');
		  // 使用callAsync触发钩子回调
        this.hooks.bundler.callAsync(bundler, () => {
            console.timeEnd('learnBundler');
        });
    }

    learnBundlerByTapPromise(bundler) {
        console.time('learnBundlerByTapPromise');
		  // 触发钩子,返回的是一个promise
        return this.hooks.bundler.promise(bundler);
    }
}

const Xiaobai = new FrontEnd();

// 使用tapAsync注册钩子
Xiaobai.hooks.bundler.tapAsync('learn bundler', (bundler, callback) => {
    console.log(`Xiaobai learn ${bundler}`);
    setTimeout(() => {
        callback();
    }, 1000);
});
Xiaobai.learnBundler('webpack');
// Xiaobai learn webpack
// learnBundler: 1012.31689453125ms

// 使用tapPromise注册钩子
Xiaobai.hooks.bundler.tapPromise('learn bundler by tapPromise', bundler => {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve();
        }, 2000);
    });
});
Xiaobai.learnBundlerByTapPromise('rollup').then(() => {
    console.timeEnd('learnBundlerByTapPromise');
});
// Xiaobai learn rollup
// learnBundlerByTapPromise: 2001.529052734375ms

2、串行与并行(Series/Parallel)

① 串行 Series

import { AsyncSeriesHook } from 'tapable';

class FrontEnd {
    constructor() {
        this.hooks = {
            bundler: new AsyncSeriesHook(['bundler']),
        };
    }

    learnBundler(bundler) {
        this.hooks.bundler.callAsync(bundler, () => {
            console.timeEnd('costTime');
        });
    }
}

const Xiaobai = new FrontEnd();

Xiaobai.hooks.bundler.tapAsync('learn bundler1', (bundler, callback) => {
    console.log(`Xiaobai learn ${bundler} for the first time`);
    setTimeout(() => {
        callback();
    }, 1000);
});

Xiaobai.hooks.bundler.tapAsync('learn bundler2', (bundler, callback) => {
    console.log(`Xiaobai learn ${bundler} for the second time`);
    setTimeout(() => {
        callback();
    }, 2000);
});

console.time('costTime');
Xiaobai.learnBundler('webpack');

// Xiaobai learn webpack for the first time
// 1s后 -> Xiaobai learn webpack for the second time
// 2s后 -> costTime: 3010.48583984375ms

bundler钩子是AsyncSeriesHook的实例,这是一个异步串行钩子
bundler钩子注册了两个回调(在webpack中理解为注册了两个插件),只有当两个插件串行执行完了才会回到callAsync的callback中,计算最后的时间,差不多是两个插件的延时时长相加(1s+2s -> 3s)

② 并行 Parallel

把上面代码中的AsyncSeriesHook换成AsyncParallelHook,其他不变

import { AsyncParallelHook } from 'tapable';

class FrontEnd {
    constructor() {
        this.hooks = {
            bundler: new AsyncParallelHook(['bundler']),
        };
    }

    learnBundler(bundler) {
        this.hooks.bundler.callAsync(bundler, () => {
            console.timeEnd('costTime');
        });
    }
}

const Xiaobai = new FrontEnd();

Xiaobai.hooks.bundler.tapAsync('learn bundler1', (bundler, callback) => {
    console.log(`Xiaobai learn ${bundler} for the first time`);
    setTimeout(() => {
        callback();
    }, 2000);
});

Xiaobai.hooks.bundler.tapAsync('learn bundler2', (bundler, callback) => {
    console.log(`Xiaobai learn ${bundler} for the second time`);
    setTimeout(() => {
        callback();
    }, 1000);
});

console.time('costTime');
Xiaobai.learnBundler('webpack');

// Xiaobai learn webpack for the first time
// Xiaobai learn webpack for the second time
// 所有插件中最长的延时时间后 -> costTime: 2005.30908203125ms

bundler钩子是AsyncParallelHook 的实例,这是一个异步并行钩子
bundler钩子注册了两个回调(在webpack中理解为注册了两个插件),只有当两个插件并行执行完了才会回到callAsync的callback中,计算最后的时间,差不多是两个插件的耗时最长的那个

3、钩子类型(Basic/Waterfall/Bail)

① Basic(基础类型)

这个没什么好说的,啥特异功能都没

② Waterfall(瀑布流)

import { SyncWaterfallHook } from 'tapable';

class FrontEnd {
    constructor() {
        this.hooks = {
            framework: new SyncWaterfallHook(['framework']),
        };
    }

    learnFramework(framework) {
        this.hooks.framework.call(framework);
    }
}

const Xiaobai = new FrontEnd();

// 上一个插件的返回值会当做下一个插件的入参
Xiaobai.hooks.framework.tap('learn framework', (framework) => {
    console.log(`${framework} is easy, let's play vue2`);
    return 'vue2';
});

Xiaobai.hooks.framework.tap('learn framework', (arg) => {
    console.log(`${arg} is also easy, let's play react`);
    return 'react'
});

Xiaobai.hooks.framework.tap('learn framework', (arg) => {
    console.log(`${arg} is amazing`);
});

Xiaobai.learnFramework('vue1');

③ Bail(熔断)

import { SyncBailHook } from 'tapable';

class FrontEnd {
    constructor() {
        this.hooks = {
            framework: new SyncBailHook(),
        };
    }

    learnFramework() {
        this.hooks.framework.call();
    }
}

const Xiaobai = new FrontEnd();

// 只要其中一个插件有非undefined返回(null也算非undefined) 剩余插件全部停止执行
Xiaobai.hooks.framework.tap('learn framework', () => {
    console.log('vue is easy');
    return undefined;
});

Xiaobai.hooks.framework.tap('learn framework', () => {
    console.log('react is easy');
    return null;
});

// 上面return null, 'Angular is easy'不会被打印
Xiaobai.hooks.framework.tap('learn framework', () => {
    console.log('Angular is easy');
    return true;
});

Xiaobai.learnFramework();

④ Loop(TODO)

官方都还没实现,不折腾了

看完上面几个例子,剩下的就是组合,详细同学们都能够举一反一,触类旁通,基础用法就不再一一演示了,可以自行动手尝试一下

实现SyncHook

下面我们来简单实现一个SyncHook
本质上是一个简单的发布订阅,Tap用来收集方法,call执行所有方法

class SyncHook {

	constructor(limit = []) {
		this.limit = limit;
		this.tasks = [];
	}
	
	tap(name, task) {
		this.tasks.push(task);
	}
	
	call(...args) {
		// 看下下面的图就知道这里的用处了
		const param = args.slice(0, this.limit.length);
		this.tasks.forEach(task => task(...param));
	}
}

C71DCAA0-20E8-40AF-B4FF-D7D3F93949DE

* limit 参数长度校验
* tasks 收集订阅
* tap 像tasks中添加方法
* call 先检验参数,然后再执行所有的已订阅方法

SyncBailHook, SyncWaterfallHook, SyncLoopHook, AsyncParallelHook, AsyncParallelBailHook, AsyncSeriesHook, AsyncSeriesBailHook, AsyncSeriesWaterfallHook等等都是基于上面的基础实现,加上同步、异步、并行、串行等变化,应用于不同的场景,有同学已经把所有类型都实现了一遍,此处不再写一遍,但还是建议参考着亲手实现一遍,加深理解
脑壳疼的Webpack-tapable - 掘金

Tapable主要应用在Webpack的各个生命周期中,对它有个基础的了解后,才能比较容易理解webpack的插件应用,但也正是因为Tapable的灵活机制,webpack的源码读起来会比较跳,需要一定耐心和不断梳理总结才能慢慢掌握

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant