从零开始的Webpack原理剖析(六)——tapable


theme: orange
highlight: gruvbox-dark

前言

好几天没有更新了,不是因为放弃了,而是之前一直高强度的学习,身体直接嘎了😭,从9月底开始包括十一假期期间,只要是有时间,每天都学习到晚上10点多,那阵子到处都是裁员的风声,导致我心里的压力也变得极大,断更的这段时间,因为急性化脓性扁桃体发炎,在家躺了一个多星期才好,期间每天烧到38.5°+,又伴随着口腔溃眼牙龈红肿出血,喝水都感觉刀割一般的疼,一度感觉好不了了,万幸,每天晚上跑去医院,连着输了一星期的液,一切都回归正常,回到公司后不敢这么肝了,又躺平了1周后,决定继续之前的学习之路。一场大病,真的给了我好多警示,只有生病的时候,才能体会到健康的来之不易,所以,还是要多多保重身体,健康没了,赚再多的钱💰,也是无济于事…继续回归正题吧~

tapable简介

从零开始的Webpack原理剖析(四)——webpack工作流程文章中,我们曾经提到过tapable,也实现写了一个最简单的tapable中的一个钩子,那么这篇文章,我们来详细的分析一下tapable。这样之后学习webpack插件的时候,就能将之前学到的知识都串联起来,非常容易理解,是必要的前置知识。

看完本文你能学习到:

  • 知道什么是Tapable
  • 知道Tapable中提供的钩子有什么作用以及用法;
  • 为学习webpack插件打好基础;

那么tapable其实就是一个提供事件发布订阅库,通过其提供的一系列钩子,我们可以注册事件,然后在不同的阶段去触发这些注册的事件。那么官方一共提供了如下的几个钩子:

const {
  SyncHook,
  SyncBailHook,
  SyncWaterfallHook,
  SyncLoopHook,
  AsyncParallelHook,
  AsyncParallelBailHook,
  AsyncSeriesHook,
  AsyncSeriesBailHook,
  AsyncSeriesWaterfallHook
} = require("tapable")

是不是看起来太多了,根本记不下来?别急,我们按照相应的分类进行划分。

tapable中钩子分类

我们首先还是要做一下准备工作:

1、创建文件夹webpack-tapable
2、初始化package.json文件,安装tapable库 npm install tapable -D
3、创建个文件index.js,我们示例代码可以在此文件中来写

按同步/异步划分

image.png

异步的钩子又可以分为异步并行钩子(异步操作一起触发)异步串行钩子(异步操作按照顺序依次触发)

是不是感觉特别不好记,没关系我们不需要硬背下来,可以通过前缀,清楚的明白他们的作用,就像VueReact中的声明周期一样,都是语义化的,而且我们只需要进行一定的联想,就能理解每个钩子函数是干嘛的。比如带Async前缀的就是异步的钩子,而Async后边又可以根据SeriesParalle进行区分是异步并行还是串行的钩子;那剩下的一堆前缀,比如Bail,Waterfall,Loop又是啥呢?别急,我们继续往下看

按执行机制或返回值划分,有4种类型

Basic Hook:基本类型的钩子,不关心返回值,只要订阅了事件,就会被触发,钩子有SyncHook、AsyncParallelHook、AsyncSeriesHook,我们先用SyncHook这个钩子进行示范:

// index.js
const { SyncHook } = require('tapable')
// 通过new操作获得SyncHook的实例,传入的参数可以理解为注册事件函数的参数
const hook = new SyncHook(['name', 'age'])
// 我们用tap注册3个事件
hook.tap('person1', (name, age) => {
  console.log('person1:', name, age)
})
hook.tap('person2', (name, age) => {
  console.log('person2:', name, age)
})
hook.tap('person3', (name, age) => {
  console.log('person3:', name, age)
})
// 使用call来依次执行之前订阅的事件
hook.call('zhangsan', 30)

我们执行node index.js,来查看执行结果,发现我们订阅的方法,被依次执行了,没错,这就是最普通的钩子,按顺序全部执行,一把梭。

image.png

Bail Hook:保险类型的钩子,遇到第一个返回值为非undefined的订阅事件,就直接返回,后续订阅的事件,不再执行,钩子有SyncBailHook、AsyncSeriesBailHook, AsyncParallelBailHook,我们用SyncBailHook进行示范:

// index.js
const { SyncBailHook } = require('tapable')
const hook = new SyncBailHook(['name', 'age'])
hook.tap('person1', (name, age) => {
  console.log('person1:', name, age)
})
hook.tap('person2', (name, age) => {
  console.log('person2:', name, age)
  // 注意,这里加了个返回值
  return 222
})
hook.tap('person3', (name, age) => {
  console.log('person3:', name, age)
})
hook.call('zhangsan', 30)

同样,还是执行node index.js,可以发现结果为:

image.png

这就是Bail Hook的特点,因为在第二个注册的事件中,return了非undefined的值,所以第三个被注册的事件,就不会被触发。就比如你约了3个相亲对象,第1个没看中,第2个看中了,牵手成功,就不再去和第3个去相亲了。

Waterfall Hook:瀑布类型的钩子,这个就非常有意思了,和普通的Basic Hook非常像,注册的事件都会被执行,唯一的区别就是,会把return的非undefined值传给下一个注册事件的第一个参数,钩子有SyncWaterfallHook,AsyncSeriesWaterfallHook,我们以SyncWaterfallHook来举例:

// index.js
const { SyncWaterfallHook } = require('tapable')
const hook = new SyncWaterfallHook(['name', 'age'])
hook.tap('person1', (name, age) => {
  console.log('person1:', name, age)
  return 'person1 return 的值'
})
hook.tap('person2', (name, age) => {
  console.log('person2:', name, age)
  return null
})
hook.tap('person3', (name, age) => {
  console.log('person3:', name, age)
})
hook.call('zhangsan', 30)

执行node index.js可以看到结果为:

image.png

这就是瀑布类型的钩子,就像瀑布一样,把返回值层层传递给下一个注册事件的第一个参数。

Loop Hook:循环型钩子,这个其实比较绕,只要某一个事件返回的值不是undefined,那么就从头,注意是从头执行所有注册的事件,直到所有事件的返回值是undefined为止,钩子有SyncLoopHook 和 AsyncSeriesLoopHook(文档中没提及,但是源码中有暴露这个钩子),我们以SyncLoopHook举例:

const { SyncLoopHook } = require('tapable')

let hook = new SyncLoopHook(['name', 'age'])
let flag1 = 0
let flag2 = 0
let flag3 = 0
hook.tap('person1', (name, age) => {
  console.log('person1', 'flag1:', flag1, name, age)
  if (++flag1 == 1) {
    flag1 = 0
    return
  }
  return true
})
hook.tap('person2', (name, age) => {
  console.log('person2', 'flag2:', flag2, name, age)
  if (++flag2 == 2) {
    flag2 = 0
    return
  }
  return true
})
hook.tap('person3', (name, age) => {
  console.log('person3', 'flag3:', flag3, name, age)
  if (++flag3 == 3) {
    flag3 = 0
    return
  }
  return true
})
hook.call('zhangsan', 30)

执行node index.js,猜一猜会有多少个console.log?这里有点绕,仔细分析不难发现打印出了15次,如下:

image.png

我们稍作分析在执行call后,person1事件先被调用了,根据if里边的逻辑,返回了undefined,所以继续调用person2事件,因为++flag2第一次为1,所以返回true了,根据Loop Hook的特点,这时候,要从头去执行注册的事件,也就是要再次从person1事件调用开始执行,所以又从头来了。那么以此类推,就可以得到15次console.log的结果。

通过几个同步钩子的举例,算是对学习Tapable的一个热身,我们也大致明白了Tapable提供的这几个钩子大致有什么作用和区别,同步的钩子比较好理解,所以当做了范例,接下来我们再举几个异步钩子的例子,进一步加深理解。

  • 我们可以看到,同步钩子注册都是通过tap方法,调用的时候是通过call方法来触发;
  • 而异步的钩子,可以通过tap、tapAsync、tapPromise方法进行注册,触发的话通过callAsync、promise方法进行触发(是的只有这两种触发方式了),接下来我们会一一讲解其区别;

其他钩子的应用

AsyncParallelHook:异步并行执行钩子

  • 使用tap同步注册
const { AsyncParallelHook } = require('tapable')

let hook = new AsyncParallelHook(['name', 'age'])
hook.tap('person1', (name, age) => {
  console.log('person1', name, age)
  return 'result1'
})
hook.tap('person2', (name, age) => {
  console.log('person2', name, age)
  return 'result2'
})
hook.tap('person3', (name, age) => {
  console.log('person3', name, age)
  return 'result3'
})
// 注意,这里用callAsync进行触发
hook.callAsync('zhangsan', 18, err => {
  console.log('err', err)
  console.log('callSync的回调函数')
})
---------------------------------------
// 执行node index.js,可以看到结果,是按顺序进行触发的:
person1 zhangsan 18
person2 zhangsan 18
person3 zhangsan 18
err undefined
callSync的回调函数
  • 使用tapSync异步注册:全部任务完成后执行最终的回调;
const { AsyncParallelHook } = require('tapable')

let hook = new AsyncParallelHook(['name', 'age'])
console.time('cost')
hook.tapAsync('person1', (name, age, callback) => {
  setTimeout(() => {
    console.log('person1', name, age)
    /* 这里需要新增一个callback回调的参数,用来表示内部异步操作已经执行完,
    因为在外部无法知道异步操作什么时候完成 */
    callback()
  }, 1000)
})
hook.tapAsync('person2', (name, age, callback) => {
  setTimeout(() => {
    console.log('person2', name, age)
    callback()
  }, 2000)
})
hook.tapAsync('person3', (name, age, callback) => {
  setTimeout(() => {
    console.log('person3', name, age)
    callback()
  }, 3000)
})
hook.callAsync('zhangsan', 18, err => {
  console.log('err', err)
  console.log('callSync的回调函数')
  console.timeEnd('cost')
})
----------------------------------
// 执行node index.js 可以看到,每隔1秒钟,就会打印相应的结果,类似于Promise.all()的效果:
// 1s后
person1 zhangsan 18
// 又过了1s后
person2 zhangsan 18
// 又过了1s后
person3 zhangsan 18
err undefined
callSync的回调函数
cost: 3.005s
  • 使用tapPromise进行promise注册
const { AsyncParallelHook } = require('tapable')

let hook = new AsyncParallelHook(['name', 'age'])
console.time('cost')
hook.tapPromise('person1', (name, age) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('person1', name, age)
      resolve()
    }, 1000)
  })
})
hook.tapPromise('person2', (name, age, callback) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('person2', name, age)
      resolve()
    }, 2000)
  })
})
hook.tapPromise('person3', (name, age, callback) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('person3', name, age)
      resolve()
    }, 3000)
  })
})
hook.promise('zhangsan', 18).then(() => {
  console.log('完成啦')
  console.timeEnd('cost')
})
----------------------------------
// 执行node index.js 可以看到,每隔1秒钟,就会打印相应的结果,也是类似于Promise.all()的效果:
person1 zhangsan 18
person2 zhangsan 18
person3 zhangsan 18
完成啦
cost: 3.002s

那么使用tapAsynctapPromise都是注册异步事件,那么有什么区别呢?我们可以看到,其实就是写法上的不同,tapAsync需要一个回调参数,来告知异步操作已经完成,而tapPromise通过返回一个promise来告知异步操作已完成。

AsyncParallelBailHook:异步并行保险钩子

我们再稍作回忆,保险钩子是啥意思呢?只要有一个注册的事件返回的不是undefined,后续注册的事件,就不会被执行了。

  • 使用tap注册:注册可以自行尝试
  • 使用tapAsync注册
const { AsyncParallelBailHook } = require('tapable')

let hook = new AsyncParallelBailHook(['name', 'age'])
console.time('cost')
hook.tapAsync('person1', (name, age, callback) => {
  setTimeout(() => {
    console.log('person1', name, age)
    callback()
  }, 1000)
})
hook.tapAsync('person2', (name, age, callback) => {
  setTimeout(() => {
    console.log('person2', name, age)
    callback('person2 error')
  }, 2000)
})
hook.tapAsync('person3', (name, age, callback) => {
  setTimeout(() => {
    console.log('person3', name, age)
    callback()
  }, 3000)
})

hook.callAsync('zhangsan', 18, err => {
  console.log('err:', err)
  console.timeEnd('cost')
})
-----------------------------------------
// 执行node index.js 可以发现结果为
// 过1s
person1 zhangsan 18
// 过1s
person2 zhangsan 18
err: person2 error
cost: 2.005s
// 注册的事件还是会被触发,但是已经是触发回调后的事情了,所以总消耗时间为2s
person3 zhangsan 18
  • 使用tapPromise注册
const { AsyncParallelBailHook } = require('tapable')

let hook = new AsyncParallelBailHook(['name', 'age'])
console.time('cost')
hook.tapPromise('person1', (name, age) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('person1', name, age)
      resolve()
    }, 1000)
  })
})
hook.tapPromise('person2', (name, age, callback) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('person2', name, age)
      // 注意,这里resolve了值相当于返回值为'123'并不是undefined
      resolve('123')
    }, 2000)
  })
})
hook.tapPromise('person3', (name, age, callback) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('person3', name, age)
      resolve()
    }, 3000)
  })
})
hook.promise('zhangsan', 18).then(() => {
  console.log('完成啦')
  console.timeEnd('cost')
})
-----------------------------------------
// 执行node index.js 可以发现结果为
// 过1s
person1 zhangsan 18
// 过1s
person2 zhangsan 18
完成啦
cost: 2.004s
// 注册的事件还是会被触发,但是已经是触发.then之后的事情了,所以总消耗时间为2s
person3 zhangsan 18

看完几个异步并行钩子的例子,我们来看几个异步串行钩子的例子,其他的几个钩子,可以自行尝试一下

AsyncSeriesHook:异步串行钩子

任务一个一个执行,执行完一个,再执行下一个

  • 使用tap注册:同样,tap注册可以自行尝试
  • 使用tapAsync注册
const { AsyncSeriesHook } = require('tapable')

let hook = new AsyncSeriesHook(['name', 'age'])
console.time('cost')
hook.tapAsync('person1', (name, age, callback) => {
  setTimeout(() => {
    console.log('person1', name, age)
    callback()
  }, 1000)
})
hook.tapAsync('person2', (name, age, callback) => {
  setTimeout(() => {
    console.log('person2', name, age)
    callback()
  }, 2000)
})
hook.tapAsync('person3', (name, age, callback) => {
  setTimeout(() => {
    console.log('person3', name, age)
    callback()
  }, 3000)
})

hook.callAsync('zhangsan', 18, err => {
  console.log('err:', err)
  console.timeEnd('cost')
})
---------------------------------------
/* 执行node index.js,可以发现结果很有意思,因为是串行的,所以注册完的事件,要等上一个事件完成后,再去调用执行,所以总共耗时为6秒 */
// 先过了1s
person1 zhangsan 18
// 又过了2s
person2 zhangsan 18
// 又过了3s
person3 zhangsan 18
err: undefined
cost: 6.015s
  • 使用tapPromise注册:有了上边的经验,可以自行尝试,和tapAsync只是写法不太相同

AsyncSeriesBailHook:异步串行保险钩子

只要有一个注册的事件返回了非undefined值,就结束,后边注册的事件就不会被执行了,这里和异步的有些小小的区别,我们在下边的例子里可以看到。

  • 使用tap注册:自行尝试
  • 使用tapAsync注册:自行尝试
  • 使用tapPromise注册
const { AsyncSeriesBailHook } = require('tapable')

let hook = new AsyncSeriesBailHook(['name', 'age'])
console.time('cost')
hook.tapPromise('person1', (name, age, error) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('person1', name, age)
      resolve()
    }, 1000)
  })
})
hook.tapPromise('person2', (name, age, error) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('person2', name, age)
      resolve('finished')
    }, 2000)
  })
})
hook.tapPromise('person3', (name, age, error) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('person3', name, age)
      resolve()
    }, 3000)
  })
})
hook.promise('zhangsan', 18).then(() => {
  console.log('完成啦')
  console.timeEnd('cost')
}).catch(err => {
  console.log(err)
})
---------------------------------------
// 执行node index.js,因为在第二个注册事件的时候,resolve了值,所以根据Bail钩子的特性,后边的事件就不会被触发了
// 过了1s
person1 zhangsan 18
// 又过了2s
person2 zhangsan 18
完成啦
cost: 3.011s
// 之后person3不会再被执行了,这个之前的异步保险钩子有小小的区别

AsyncSeriesWaterfullHook:异步串行瀑布钩子

  • tap注册:自己试验
  • tapPromise注册:自己试验
  • tapAsync注册:
const { AsyncSeriesWaterfallHook } = require('tapable')

let hook = new AsyncSeriesWaterfallHook(['name', 'age'])
console.time('cost')
hook.tapAsync('person1', (name, age, callback) => {
  setTimeout(() => {
    console.log('person1', name, age)
    // 注意,callback的第二个参数才为传参数,第一个参数如果为非空值,则相当于直接报错中断了,可以尝试一下
    callback(null, 'person1的name')
  }, 1000)
})
hook.tapAsync('person2', (name, age, callback) => {
  setTimeout(() => {
    console.log('person2', name, age)
    callback(null, 'person2的name')
  }, 2000)
})
hook.tapAsync('person3', (name, age, callback) => {
  setTimeout(() => {
    console.log('person3', name, age)
    callback()
  }, 3000)
})

hook.callAsync('zhangsan', 18, err => {
  console.log('err:', err)
  console.timeEnd('cost')
})
---------------------------------------
// 执行node index.js,发现结果如下,也符合我们之前说过的waterfull钩子的特征
// 先过1s
person1 zhangsan 18
// 又过了2s
person2 person1的name 18
// 又过了3s
person3 person2的name 18
err: null
cost: 6.021s

Tapable其他知识

interceptor拦截器

所有的钩子,都在不同阶段提供了拦截器,有register、tap、call,具体我们看示例:

// 我们还是用上边AsyncSeriesWaterfallHook这个例子
const { AsyncSeriesWaterfallHook } = require('tapable')

let hook = new AsyncSeriesWaterfallHook(['name', 'age'])
console.time('cost')
hook.tapAsync('person1', (name, age, callback) => {
  setTimeout(() => {
    console.log('person1', name, age)
    callback(null, 'person1的name')
  }, 1000)
})
hook.tapAsync('person2', (name, age, callback) => {
  setTimeout(() => {
    console.log('person2', name, age)
    callback(null, 'person2的name')
  }, 2000)
})
hook.tapAsync('person3', (name, age, callback) => {
  setTimeout(() => {
    console.log('person3', name, age)
    callback()
  }, 3000)
})
// 添加2个拦截器
hook.intercept({
  register (tapInfo) {
    console.log('拦截器1register开始执行了')
    return tapInfo
  },
  tap (tapInfo) {
    console.log('拦截器1开始tap了')
  },
  call (name, age) {
    console.log(name, age)
    console.log('拦截器1开始call了')
  }
})
hook.intercept({
  register (tapInfo) {
    console.log('拦截器2register开始执行了')
    return tapInfo
  },
  tap (tapInfo) {
    console.log('拦截器2开始tap了')
  },
  call (name, age) {
    console.log(name, age)
    console.log('拦截器2开始call了')
  }
})
hook.callAsync('zhangsan', 18, err => {
  console.log('err:', err)
  console.timeEnd('cost')
})
---------------------------------------
/* 执行node index.js我们查看拦截器的执行顺序,可以发现,有多少个注册事件,register函数就执行几次,
下一个拦截器接收到的参数,取决于上一个拦截器register函数中return返回的内容,所以在register函数中,
最好返回tap钩子;在调用call之前,先触发了call这个钩子,多个钩子只执行一次;每个钩子执行前,
都会触发tap这个函数,多个钩子触发多次*/
拦截器1register开始执行了
拦截器1register开始执行了
拦截器1register开始执行了

拦截器2register开始执行了
拦截器2register开始执行了
拦截器2register开始执行了
zhangsan 18
拦截器1开始call了
zhangsan 18
拦截器2开始call了
拦截器1开始tap了
拦截器2开始tap了
person1 zhangsan 18

拦截器1开始tap了
拦截器2开始tap了
person2 person1的name 18

拦截器1开始tap了
拦截器2开始tap了
person3 person2的name 18
err: null
cost: 6.016s

利用stage控制触发顺序

可以添加stage参数来控制触发顺序,数字越大越晚触发:

let { SyncHook } = require('tapable')
let hook = new SyncHook(['name', 'age'])
hook.tap({ name: 'tap1', stage: 1 }, (name, age) => {
  console.log('person1', name, age)
});
hook.tap({ name: 'tap3', stage: 3 }, (name, age) => {
  console.log('person3', name, age)
});
hook.tap({ name: 'tap4', stage: 4 }, (name, age) => {
  console.log('person4', name, age)
});
hook.tap({ name: 'tap2', stage: 2 }, (name, age) => {
  console.log('person2', name, age)
});

hook.call('zhangsan', 18)
-----------------------------------------------------
执行node index.js可以看到结果按照stage,从小到大的顺序执行了:
person1 zhangsan 18
person2 zhangsan 18
person3 zhangsan 18
person4 zhangsan 18

利用before控制触发顺序

如果没有stage这个参数的话,还可以用before来控制触发顺序:

// 我们想让tap2,在tap1执行之后,tap3,tap4执行之前,那么就可以用before,传入的参数是个数组,存放其他tap的名字
let { SyncHook } = require('tapable')
let hook = new SyncHook(['name', 'age'])
hook.tap({ name: 'tap1' }, (name, age) => {
  console.log('person1', name, age)
});
hook.tap({ name: 'tap3' }, (name, age) => {
  console.log('person3', name, age)
});
hook.tap({ name: 'tap4' }, (name, age) => {
  console.log('person4', name, age)
});
hook.tap({ name: 'tap2', before: ['tap3'] }, (name, age) => {
  console.log('person2', name, age)
});

hook.call('zhangsan', 18)

如果beforestage同时存在的话,则会优先处理before,之后才会继续判断stage,不过最好不要混用这两个属性,会给阅读代码造成困扰。

HookMap辅助类,来管理Hook

let { SyncHook, HookMap } = require('tapable')
let map = new HookMap(() => new SyncHook(['name', 'age']))
// 在map中创建一个名字是key1的hook,然后注册事件
map.for('key1').tap('key1-tap1',  (name, age) => {
  console.log('key1-person1', name, age)
})
map.for('key1').tap('key1-tap2',  (name, age) => {
  console.log('key1-person2', name, age)
})
// 在map中创建一个名字是key2的hook,然后注册事件
map.for('key2').tap('key2-tap1',  (name, age) => {
  console.log('key2-person1', name, age)
})
// 拿到map中不同的hook
const hook1 = map.get('key1')
const hook2 = map.get('key2')
// 不同的hook进行触发
hook1.call('zhangsan', 18)
hook2.call('lisi', 20)
---------------------------------------------------
// 执行node index.js,结果为:
key1-person1 zhangsan 18
key1-person2 zhangsan 18
key2-person1 lisi 20

在webpack源码中查看Compiler和Compilation提供的钩子

我们接下来再看下边这张图,就知道上边介绍的那些个钩子,是干嘛用的了,下图是webpack源码中的CompilerCompilation类,之前我们也简单的实现过,这时候再去看,是不是有种恍然大悟的感觉呢,比如我们之前用到过的run钩子,那么从图中可以清晰的看到,它是通过new AsyncSeriesHook()来得到的,那么按照上文的分类run就属于异步串行的钩子
image.png

同样,我们再去webpack官方文档中查看相应钩子API的时候,这里标注的类型我们也能够明白是个啥意思了。
image.png

结语

至此为止,我们把大多数钩子的不同用法,进行了举例讲解,一般来说就可以到此为止了,但是tapable是如何实现这些个钩子呢?如果想深入了解,就需要去看tapable的源码,只看肯定是不行,最好是自己用上边的一个简单例子,然后打断点进行调试,就能看到每一步都做了什么事情,本篇文章就不赘述原理了,毕竟对于大多数人来说,tapable的这些个钩子可能都没听说过,也不知道有什么作用,之后如果有空或者有需求的话,会写一写tapable的原理。

webpack的文档对新手来说真的太不友好了,好多细节的地方,如果弄不懂是啥意思,就相当于直接错过了核心概念,在我这篇webpack原理剖析专栏中,我尽可能的让每个知识点,在文档中都能够找到,并且截图说明,小白都能听懂的最新版Babel配置探索——保姆级教学文章中说过的,我们每个人写的博客,都可能随着版本的更新不能满足一些人的需求和阅读,但是能够读懂文档,才永远不会被落下,那么对于新手来说,阅读文档又有着一定的困难,所以在写每篇文章的时候,我都会结合官方文档来进行说明,哪怕是一个最简单的点,文章写多了时间长了,也能间接性的帮助小伙伴熟悉官方文档,对官方文档不至于那么抵触,不至于一碰到问题就直接百度,搜索出来的文章依旧是过时的,解决不了问题,反而在官方文档上,清清楚楚的几句话就能解决你的问题。

接下来,有了本篇文章的基础了,我们便可以继续学习webpack插件部分的内容了!

© 版权声明
THE END
喜欢就支持一下吧
点赞10 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容