一、打包后的文件分析

  1. 打包后的文件就是一个函数自调用,当前函数调用时传入一个对象
  2. 这个对象我们为了方便将之称为是模块定义,它就是一个键值对
  3. 这个键名就是当前被加载模块的文件名与某个目录的拼接()
  4. 这个键值就是一个函数,和 node.js 里的模块加载有一些类似,会将被加载模块中的内容包裹于一个函数中
  5. 这个函数在将来某个时间点上会被调用,同时会接收到一定的参数,利用这些参数就可以实现模块的加载操作
  6. 针对于上述的代码就相当于是将 {}(模块定义) 传递给了 modules
    1. (function (modules) { ......
    2. })
    3. /************************************************************************/
    4. ({
    5. "./src/index.js":
    6. /*! no static exports found */
    7. (function (module, exports) {
    8. console.log('index.js内容')
    9. module.exports = '入口文件导出内容'
    10. })
    11. });
    installedModules对象,存放被加载进来的模块
    webpack_require
    不管是require和是import,都是使用这个方法,把模块ID对应的模块中的导出内容进行返回,它的作用就是返回模块的 exports

webpack执行自调用方法,参数modules就是当前被加载模块的对象键值对,定义一个installedModules存放被加载的模块,定义一个webpack_require方法,然后往这个方法上再加一些属性和方法,然后调用webpack_require方法,参数是被加载模块对象的键,方法中根据传入的键,判断installedModules里是否已经存在,存在就直接返回,没有就定义module对象来存储,然后通过call方法调用执行被加载模块,返回module.exports

二、功能函数说明

webpack_require.m = modules:将模块定义保存一份,通过 m 属性挂载到自定义的方法身上
webpack_require.c = installedModules :定义对象用于缓存已加载过的模块,通过c属性挂载到自定义的方法身上
webpack_require.p:webpack的公共路径

webpack_require.o方法:判断被传入的对象 obj 身上是否具有指定的属性,如果有则返回 true
webpack_require.d方法:通过o方法,判断当前 exports 身上是否具备某个属性,否定时通过Object.defineProperty新增,同时为这个属性提供一个访问器
webpack_require.r方法:判断是否是ESmodule,是的话往export上添加Symbol.toStringTag,值设置为{ value: ‘Module’ },将来通过原型链调用toString方法时返回module;不是ESmodule,直接在export上添加__esModule属性,值为true
总之就是往参数export上添加是否是ESmodule的标记

webpack_require.t方法:调用方法后,我们会拿到被加载模块中的内容 value ,对于 value 来说我们可能会直接返回,也可能会处理之后再返回
webpack_require.n方法:传入module,根据module是否是ESmodule,定义不同的gettter,执行d方法,然后返回gettter

三、不同规范模式下的打包

1、commonJS

导出使用2种不同的方法,导入的时候使用require

  1. // 01 采用 cms
  2. module.exports = '拉勾教育'
  3. // 02 采用 esm 导出内容
  4. export default 'zcegg'
  5. export const age = 18

采用cms规范,打包后只是把导入时候的require用webpack_require进行了替换
采用esm规范:

  1. "./src/login.js":
  2. (function (module, __webpack_exports__, __webpack_require__) {
  3. "use strict";
  4. __webpack_require__.r(__webpack_exports__);
  5. __webpack_require__.d(__webpack_exports__, "age", function () { return age; });
  6. __webpack_exports__["default"] = ('zcegg');
  7. const age = 18
  8. })
  • webpack_exports空对象
  • r方法:往webpack_exports对象上加了toString方法,值为Module; __esModule属性,值为true

image.png

  • d方法:判断是否有age属性,没有,增加这个属性和属性的访问器

    image.png

2、esModule

导入的时候不变,使用import

  1. // 01 采用 cms 导出模块内容
  2. module.exports = 'zce'
  3. // 02 采用 esModule 导出模块
  4. export default '拉勾教育'
  5. export const age = 100

采用cms规范,打包后,用r方法对esmodule进行标记,导入时候的import用webpack_require进行了替换,通过n方法对导入的值处理,返回一个对象,通过这个对象访问
采用esm规范:index.js中一样,login.js中,先r方法,再d方法

四、懒加载的流程

let oBtn = document.getElementById('btn')

oBtn.addEventListener('click', function () {
  import(/*webpackChunkName: "login"*/'./login.js').then((login) => {
    console.log(login)
  })
})

console.log('index.js执行了')
({
  "./src/index.js":
  (function (module, exports, __webpack_require__) {
    let oBtn = document.getElementById('btn')
    oBtn.addEventListener('click', function () {
      __webpack_require__.e(/*! import() | login */ "login").
          then(__webpack_require__.t.bind(null, /*! ./login.js */ "./src/login.js", 7)).then((login) => {
            console.log(login)
      })
    })
    console.log('index.js执行了')
  })
});

在懒加载的代码中,先调用e方法,在根据结果调用t方法
e方法利用jsonp来创建script标签再加src属性来加载,最后返回一个promise的all方法
t方法可以根据内容进行不同的处理,取决于最后一个参数

  • 接收两个参数,一个是 value 是被加载的模块id ,第二个值 mode 是一个数值
  • mode会转为二进制的数值后进行按位与比较
  • 第一步是和1做按位与,验证通过后调用自定义的 webpack_require方法加载value 对应的模块导出,重新赋值给 value
  • 当获取到了这个 value 值之后,余下的 8 4 ns 2, 都是对当前的内容进行加工处理,然后返回使用
  • 当 mode & 8 成立,直接将 value 返回 ( commonJS )
  • 当 mode & 4 成立,直接将 value 返回(esModule)
  • 如果上述条件都不成立,继续处理 value ,定义一个 ns 空对象,通过r方法标记它为esModule,值为value
    • 如果拿到的 value 是一个可以直接使用的内容,例如是一个字符串,通过Object.defineProperty给ns新增default属性,再将它挂载到 ns 的 default 属性上
    • 如果不是可以直接使用的,就对value进行遍历,调用d方法,对ns进行新增属性
    • 返回ns


五、tapable

1、认识tapable

webpack编译流程

  • 配置初始化
  • 内容编译
  • 输出编译后内容

执行过程可以看作一种事件驱动型事件流工作机制,可以将不同的插件串联,核心是负责编译的compile和负责创建bundles的compilation,他们都是tapable的实例库

tapable工作流程

  • 实例化hook注册事件监听
  • 通过hook触发事件监听
  • 执行懒编译生成的可执行代码

Hook本质是tapable实例对象,tapable中有几个类,每个类的实例就是一种Hook实例,也就是钩子,每个钩子有不同的执行特点
Hook执行机制可以分成同步和异步,异步中有并行和串行

Hook执行特点:

  • Hook:普通钩子,监听器之间互相独立不干扰
  • BailHook:熔断钩子,某个钩子的监听返回非undefined时,后续不执行
  • WaterfallHook:瀑布钩子,上一个监听的返回值可传递到下一个
  • LoopHook:循环钩子,如果当前未返回false则一直执行

tapable库同步钩子

  • synckHook
  • syncBailHook
  • syncWaterfallHook
  • syncLoopHook

tapable库异步串行钩子

  • AsyncSeriesHook
  • AsyncSeriesBailHook
  • AsyncSeriesWaterfallHook

tapable库异步并行钩子

  • AsyncParalleHook
  • AsyncParalleBailHook

2、同步钩子使用

syncHook

const { SyncHook } = require('tapable')

let hook = new SyncHook(['name', 'age'])
hook.tap('fn1', function (name, age) {
  console.log('fn1--->', name, age)
})
hook.tap('fn2', function (name, age) {
  console.log('fn2--->', name, age)
})

hook.call('zoe', 18)

syncBailHook

const { SyncBailHook } = require('tapable')

let hook = new SyncBailHook(['name', 'age'])
hook.tap('fn1', function (name, age) {
  console.log('fn1--->', name, age)
})
hook.tap('fn2', function (name, age) {
  console.log('fn2--->', name, age)
  //return 'ret2' 返回非undefined的时候,后面的钩子不会再执行
  return undefined
})
hook.tap('fn3', function (name, age) {
  console.log('fn3--->', name, age)
})

hook.call('lg', 100)

SyncWaterfallHook

const { SyncWaterfallHook } = require('tapable')

let hook = new SyncWaterfallHook(['name', 'age'])
hook.tap('fn1', function (name, age) {
  console.log('fn1--->', name, age)
  return 'ret1'
})
hook.tap('fn2', function (name, age) {
  console.log('fn2--->', name, age)        //name被换成了ret1
  return 'ret2'
})
hook.tap('fn3', function (name, age) {
  console.log('fn3--->', name, age)//name被换成了ret2
  return 'ret3'
})

hook.call('zce', 33)

syncLoopHook

const { SyncLoopHook } = require('tapable')

let hook = new SyncLoopHook(['name', 'age'])
let count1 = 0
let count2 = 0
let count3 = 0
hook.tap('fn1', function (name, age) {
  console.log('fn1--->', name, age)
  if (++count1 === 1) {
    count1 = 0
    return undefined
  }
  return true
})
hook.tap('fn2', function (name, age) {
  console.log('fn2--->', name, age)
  // if (++count2 === 2) {
  //   count2 = 0
  //   return undefined
  // }
  // return true
})
hook.tap('fn3', function (name, age) {
  console.log('fn3--->', name, age)
})

hook.call('foo', 100)


3、异步钩子使用

AsyncParallelHook
异步钩子的使用,在添加事件监听时会存在三种方式: tap tapAsync tapPromise

const { AsyncParallelHook } = require('tapable')

let hook = new AsyncParallelHook(['name'])
// hook.tap('fn1', function (name) {
//   console.log('fn1--->', name)
// })
// hook.tap('fn2', function (name) {
//   console.log('fn2--->', name)
// })

// hook.callAsync('zoe', function () {
//   console.log('最后执行了回调操作')
// })

/* console.time('time')
hook.tapAsync('fn1', function (name, callback) {
  setTimeout(() => {
    console.log('fn1--->', name)
    callback()
  }, 1000)
})
hook.tapAsync('fn2', function (name, callback) {
  setTimeout(() => {
    console.log('fn2--->', name)
    callback()
  }, 2000)
})
hook.callAsync('lg', function () {
  console.log('最后一个回调执行了')
  console.timeEnd('time')
}) */

// 03 promise 
console.time('time')
hook.tapPromise('fn1', function (name) {
  return new Promise(function (resolve, reject) {
    setTimeout(() => {
      console.log('fn1--->', name)
      resolve()
    }, 1000)
  })
})
hook.tapPromise('fn2', function (name) {
  return new Promise(function (resolve, reject) {
    setTimeout(() => {
      console.log('fn2--->', name)
      resolve()
    }, 2000)
  })
})
hook.promise('foo').then(() => {
  console.log('end执行了')
  console.timeEnd('time')
})

AsyncParallelBailHook

const { AsyncParallelBailHook } = require('tapable')

let hook = new AsyncParallelBailHook(['name'])
console.time('time')
hook.tapAsync('fn1', function (name, callback) {
  setTimeout(() => {
    console.log('fn1--->', name)
    callback()
  }, 1000)
})
hook.tapAsync('fn2', function (name, callback) {
  setTimeout(() => {
    console.log('fn2--->', name)
    callback('err')//使用传递err来阻断
  }, 2000)
})
hook.tapAsync('fn3', function (name, callback) {
  setTimeout(() => {
    console.log('fn3--->', name)
    callback()
  }, 3000)
})

hook.callAsync('zce', function () {
  console.log('最后的回调执行了')
  console.timeEnd('time')
})

AsyncSeriesHook

const { AsyncSeriesHook } = require('tapable')

let hook = new AsyncSeriesHook(['name'])
console.time('time')
hook.tapPromise('fn1', function (name) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('fn1--->', name)
      resolve()
    }, 1000)
  })
})
hook.tapPromise('fn2', function (name) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('fn2--->', name)
      resolve()
    }, 2000)
  })
})

hook.promise('foo').then(function () {
  console.log('~~~~')
  console.timeEnd('time')
})