Webpack详解-你不知道的Tapable

Tapable是webpack事件流的核心

安装

npm i --save-dev tapable

基本用法

Tapable提供了很多种hook,这里介绍webpack中compile类用到的hook,更多请参照Tapable Hooks

const {
    SyncHook,
    SyncBailHook,
    AsyncParallelHook,
    AsyncSeriesHook
} = require("tapable");

class App {
    constructor() {
        //param:描述需要传入的参数,无实际意义
        this.hooks = {
            syncHook: new SyncHook(["param"]),
            syncBailHook: new SyncBailHook(["param"]),      
            asyncParallelHook: new AsyncParallelHook(["param"]),
            asyncSeriesHook: new AsyncSeriesHook(["param"])
        };
    }
}

const app = new App();
//param:参数无实际意义,接收参数的个数需要与实例的参数个数一致

//同步:
//注册事件:tap 触发事件:call
//所有事件回调函数接收相同参数
app.hooks.syncHook.tap("CountLog", param => console.log(`收到的值:${param}`));     
app.hooks.syncHook.tap("CurrentCount", param => console.log(`当前值:${param}`));
app.hooks.syncHook.call(100);
// 收到的值:100
// 当前值:100

//当某个事件回调函数返回任何非undefined的值便终止执行,call的返回值为最后一个事件回调函数的返回值
app.hooks.syncBailHook.tap("p1", param => {
    console.log(`syncBailHook p1收到的值:${param}`);
    if(param > 50) return param;     //终止
});
app.hooks.syncBailHook.tap("p2", param => {
    console.log(`syncBailHook p2收到的值:${param}`);
});
app.hooks.syncBailHook.call(Math.floor(Math.random() * 100) + 1);
// p1收到的值:13
// p2收到的值:13

// p1收到的值:94

//异步(并行):
//注册事件:tapAsync tapPromise 触发事件:callAsync promise
//事件回调函数的最后一个参数为callback,当callback的参数不为undefined时,立即调用callAsync传入的回调函数,后续事件回调函数的callback再次调用则不触发callAsync的回调函数,当所有事件回调函数的callback都没传入err时,callAsync的回调函数在所有事件回调函数的callback执行完成后执行
app.hooks.asyncParallelHook.tapAsync("p1", (param, callback) => {
    console.log(`asyncParallelHook p1收到的值:${param}`);
    callback();
});
app.hooks.asyncParallelHook.tapAsync("p2", (param, callback) => {
    setTimeout(() => {
        console.log(`asyncParallelHook p2收到的值:${param}`);
        callback('p2');
    }, 3000);
});
app.hooks.asyncParallelHook.tapAsync("p3", (param, callback) => {
    console.log(`asyncParallelHook p3收到的值:${param}`);
    callback('p3');       //p3先发生错误,程序不会被终止,但是p2的错误将不再触发callAsync回调函数
});
app.hooks.asyncParallelHook.callAsync(1, err =>{
    console.log(`错误:${err}`);
});
// asyncParallelHook p1收到的值:1
// asyncParallelHook p3收到的值:1
// 错误:p3
// asyncParallelHook p2收到的值:1

//事件回调函数必须返回一个promise,调用reject后立即调用promise.catch,后续调用reject不触发promise.catch,没有reject调用则执行promise.then
app.hooks.asyncParallelHook.tapPromise("p1", (param) => {
    return new Promise((resolve, reject) => {
        console.log(`asyncParallelHook p1收到的值:${param}`);
        resolve();
    });
});
app.hooks.asyncParallelHook.tapPromise("p2", (param) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(`asyncParallelHook p2收到的值:${param}`);
            reject('p2');
        }, 3000);
    });
});
app.hooks.asyncParallelHook.tapPromise("p3", (param) => {
    return new Promise((resolve, reject) => {
        console.log(`asyncParallelHook p3收到的值:${param}`);
        reject('p3');
    });
});
app.hooks.asyncParallelHook.promise(1).then(res => {
    console.log(res);
}).catch(err => {
    console.log(`错误:${err}`);
});
// asyncParallelHook p1收到的值:1
// asyncParallelHook p3收到的值:1
// 错误:p3
// asyncParallelHook p2收到的值:1

//异步(串行):
//注册事件:tapAsync tapPromise 触发事件:callAsync promise
//事件回调函数的最后一个参数为callback,当callback的参数不为undefined时,立即调用callAsync传入的回调函数,并终止后续事件回调函数执行,当所有事件回调函数的callback都没传入err时,callAsync的回调函数在所有事件回调函数的callback执行完成后执行
app.hooks.asyncSeriesHook.tapAsync("p1", (param, callback) => {
    console.log(`asyncSeriesHook p1收到的值:${param}`);
    callback();
});
app.hooks.asyncSeriesHook.tapAsync("p2", (param, callback) => {
    setTimeout(() => {
        console.log(`asyncSeriesHook p2收到的值:${param}`);
        callback('p2');
    }, 3000);
});
app.hooks.asyncSeriesHook.tapAsync("p3", (param, callback) => {
    console.log(`asyncSeriesHook p3收到的值:${param}`);
    callback('p3');        //串行等待p2的执行结束,p2发生错误调用callAsync回调函数并终止程序执行
});
app.hooks.asyncSeriesHook.callAsync(1, err => {
    console.log(`错误:${err}`);
});

// asyncSeriesHook p1收到的值:1
// asyncSeriesHook p2收到的值:1
// 错误:p2


webpack的工作流程

  • initialize:根据shell和config参数调用createCompiler方法得到compiler对象,这里会预先加载所有的plugins,并调用插件的apply方法,传入compiler对象

    这里我们拿到compiler对象,基于上面的tapable的知识,我们就可以在webpack的每个生命周期去做我们的事情。

 //部分hooks
 ...
/** @type {SyncHook<[]>} */
initialize: new SyncHook([]),
/** @type {SyncBailHook<[Compilation], boolean>} */
shouldEmit: new SyncBailHook(["compilation"]),
/** @type {AsyncSeriesHook<[Stats]>} */
done: new AsyncSeriesHook(["stats"]),
/** @type {SyncHook<[Stats]>} */
afterDone: new SyncHook(["stats"]),
/** @type {AsyncSeriesHook<[]>} */
additionalPass: new AsyncSeriesHook([]),
/** @type {AsyncSeriesHook<[Compiler]>} */
beforeRun: new AsyncSeriesHook(["compiler"]),
/** @type {AsyncSeriesHook<[Compiler]>} */
run: new AsyncSeriesHook(["compiler"]),
/** @type {AsyncSeriesHook<[Compilation]>} */
emit: new AsyncSeriesHook(["compilation"]),
/** @type {AsyncSeriesHook<[string, AssetEmittedInfo]>} */
assetEmitted: new AsyncSeriesHook(["file", "info"]),
/** @type {AsyncSeriesHook<[Compilation]>} */
afterEmit: new AsyncSeriesHook(["compilation"])
...
  • entryOption: webpack开始读取配置文件的Entries,递归遍历所有的入口文件
  • run:程序即将进入构建环节
  • compile:程序即将创建compilation实例对象,compilation对象也有许多钩子,并且可以操作打包后的文件
    • 从webpack的源码Compiler.js中我们可以看到,当生成compilation对象时会调用两个钩子
    • 因此我们在自己的plugin中便可以很容易的拿到compile和compilation两个对象,来完成我们自己的操作
    newCompilation(params) {
        const compilation = this.createCompilation(params);
        compilation.name = this.name;
        compilation.records = this.records;
        //生成compilation对象时会触发以下两个钩子
        this.hooks.thisCompilation.call(compilation, params);
        this.hooks.compilation.call(compilation, params);
        return compilation;
    }

    compile(callback) {
        const params = this.newCompilationParams();
        this.hooks.beforeCompile.callAsync(params, err => {
            if (err) return callback(err);

            this.hooks.compile.call(params);

            const compilation = this.newCompilation(params);

            ......
        }
    }

    //自己的插件
     class MyPlugin{
         apply(compiler) {
             compiler.hooks.thisCompilation.tap('getCompilation', (compilation, compilationParams)=> {
                 //do someting
             });
         }
     }
  • make:compilation实例启动对代码的编译和构建
  • emit:所有打包生成的文件内容已经在内存中按照相应的数据结构处理完毕,下一步会将文件内容输出到文件系统,emit钩子会在生成文件之前执行(通常想操作打包后的文件可以在emit阶段编写plugin实现)
  • assetEmitted:执行 Compiler 的 emitAssets 方法把所有的chunk到文件输出到 output 的目录中后触发
  • done:编译后的文件已经输出到目标目录,整体代码的构建工作结束时触发

compilation下的钩子含义如下:

  • buildModule:在模块构建开始之前触发,这个钩子下可以用来修改模块的参数
  • seal:构建工作完成了,compilation对象停止接收新的模块时触发
  • optimize:优化阶段开始时触发

compiler进入make阶段后,compilation实例被创建出来,它会先触发buildModule阶段定义的钩子,此时- compilation实例依次进入每一个入口文件(entry),加载相应的loader对代码编译

代码编译完成后,再将编译好的文件内容调用 acorn 解析生成AST语法树,按照此方法继续递归、重复执行该过程

所有模块和和依赖分析完成后,compilation进入seal 阶段,对每个chunk进行整理,接下来进入optimize阶段,开启代码的优化和封装

附上一张完整流程图

图片来源于网络
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 222,627评论 6 517
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 95,180评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 169,346评论 0 362
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 60,097评论 1 300
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 69,100评论 6 398
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,696评论 1 312
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 41,165评论 3 422
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 40,108评论 0 277
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,646评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,709评论 3 342
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,861评论 1 353
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,527评论 5 351
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,196评论 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,698评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,804评论 1 274
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 49,287评论 3 379
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,860评论 2 361

推荐阅读更多精彩内容

  • webpack 核心 核心概述 entry 入口文件:js 代码文件,可执行的 node 模块或打包的入口文件。 ...
    coolheadedY阅读 3,487评论 1 6
  • 写在前面:本文是webpack的一个学习笔记,涉及webpack打包流程、plugin、loader的编写、以及实...
    Bbang呀_阅读 428评论 0 2
  • webpack简介 tapable(webpack控制事件流的超级管家) Tapable的核心功能就是依据不同的钩...
    稚儿擎瓜_细犬逐蝶阅读 471评论 0 0
  • webpack是目前最为流行的打包工具之一,其配置简单,功能强大,拥有丰富的加载器和插件系统,为前端开发者提供了诸...
    风起云涌Hal阅读 1,944评论 3 3
  • webpack 可谓是让人欣喜又让人忧,功能强大但需要一定的学习成本。在探寻 webpack 插件机制前,首先需要...
    牧云云阅读 1,062评论 0 0