一、打包后的文件分析
- 打包后的文件就是一个函数自调用,当前函数调用时传入一个对象
- 这个对象我们为了方便将之称为是模块定义,它就是一个键值对
- 这个键名就是当前被加载模块的文件名与某个目录的拼接()
- 这个键值就是一个函数,和 node.js 里的模块加载有一些类似,会将被加载模块中的内容包裹于一个函数中
- 这个函数在将来某个时间点上会被调用,同时会接收到一定的参数,利用这些参数就可以实现模块的加载操作
- 针对于上述的代码就相当于是将 {}(模块定义) 传递给了 modules
installedModules对象,存放被加载进来的模块(function (modules) { ......
})
/************************************************************************/
({
"./src/index.js":
/*! no static exports found */
(function (module, exports) {
console.log('index.js内容')
module.exports = '入口文件导出内容'
})
});
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
// 01 采用 cms
module.exports = '拉勾教育'
// 02 采用 esm 导出内容
export default 'zcegg'
export const age = 18
采用cms规范,打包后只是把导入时候的require用webpack_require进行了替换
采用esm规范:
"./src/login.js":
(function (module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, "age", function () { return age; });
__webpack_exports__["default"] = ('zcegg');
const age = 18
})
- webpack_exports空对象
- r方法:往webpack_exports对象上加了toString方法,值为Module; __esModule属性,值为true
d方法:判断是否有age属性,没有,增加这个属性和属性的访问器
2、esModule
导入的时候不变,使用import
// 01 采用 cms 导出模块内容
module.exports = 'zce'
// 02 采用 esModule 导出模块
export default '拉勾教育'
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')
})