1.内容概述
1.webpack打包流程拆分不同的环节
2.做了什么工作
3.如何完成的
4.使用了什么技术
5.模块的加载和loader的使用
6.webpack的配置演示
2.打包后源码分析
path: path.resolve(‘dist’) 输出目录的路径
对输出文件进行分析
1.我们称之为模块定义
“./src/index.js”: 路径模块id 视为键值对
(function (module, exports) {
console.log(‘index.js内容’)
module.exports = ‘入口文件导出内容’ //模块定义
})
值是一个函数的调用
传递模块的参数
/**
* 01 打包后的文件就是一个函数自调用,当前函数调用时传入一个对象
* 02 这个对象我们为了方便将之称为是模块定义,它就是一个键值对
* 03 这个键名就是当前被加载模块的文件名与某个目录的拼接()
* 04 这个键值就是一个函数,和 node.js 里的模块加载有一些类似,会将被加载模块中的内容包裹于一个函数中
* 05 这个函数在将来某个时间点上会被调用,同时会接收到一定的参数,利用这些参数就可以实现模块的加载操作
*
* 06 针对于上述的代码就相当于是将 {}(模块定义) 传递给了 modules
*/
2.installedModules 缓存 存放将来被加载的模块对象
3. function __webpack_require__(moduleId) 核心作用是返回模块的导出
return了 module.exports
1.先判断installedModules缓存里面有没有
4.__webpack_require__.t 里面调用了 3的方法
return __webpack_require__(__webpack_require__.s = "./src/index.js") 进入方法的调用
5.__webpack_require__.s 缓存路径的 这个语句不影响
6.初始化环境
完成最简单的打包操作
生成输出文件
匿名函数自动调用 实参
路径 模块的id 路径+文件名
值为函数 被加载模块的内容 外部包裹函数 require函数里面调用
调用就可以拿到模块的加载内容
3.单文件打包后源码调试
添加属性和方法
连续赋值 从右往左看
modules[moduleId] 为函数
调用函数
modules[moduleId].call(module.exports, module, module.exports, webpack_require);
module.l= true 表示加载过了
得到自调用函数 生成模块定义
模块定义传给modules
调用reqire方法 传入模块id
调用模块方法 导出module.exports
4.功能函数说明
引入其他模块分析执行流程
在主js文件里 会有导入附属js 的语句
let name = webpack_require(/*! ./login.js */ “./src/login.js”)
模块定义里有多个键值对了
1.installedModules 缓存 存放将来被加载的模块对象
2. __webpack_require__(moduleId) 核心作用是返回模块的导出
3.__webpack_require__.m = modules 将模块定义保存一份,通过 m 属性挂载到自定义的方法身上
4. __webpack_require__.c = installedModules 保存缓存
5.__webpack_require__.o 判断被传入的对象 obj 身上是否具有指定的属性 **** ,如果有则返回 true
6. __webpack_require__.d 通过 o 方法判断是否没有 name 属性 并且添加一个name属性 和访问器
7.__webpack_require__.r
1.判断是否是esmodules
是的话添加一个 值为 module 的Symbol
2.添加一个 __esModule 属性
8.__webpack_require__.t 未调用
调用后会拿到模块中的内容value
对于 value 来说我们可能会直接返回,也可能会处理之后再返回
9.__webpack_require__.n 分会module的default属性 未调用
10.__webpack_require__.p 配置里面的public属性
5.commonJS模块打包
主js采用 require函数
只是对__webpack_require__ 只是 对require进行了替换 就是上面学习的例子
js里面
esModules模块
调用了__webpack_require__.r 方法 判断是否esmodules 并且添加 Module的的Symbol
添加 __esModule 属性 value 为true
调用了__webpack_require__.d 处理age 属性 作为d方法里面name值 添加age属性和访问器
6.esModule 打包
主js 采用 import from
调用了__webpack_require__.r 方法 判断是否esmodules 并且添加 Module的的Symbol
添加 _esModule 属性 value 为true
webpack自定义方法 替换
var login_js__WEBPACK_IMPORTED_MODULE_0 = webpack_require(/*! ./login.js */ “./src/login.js”);
module.exports = 'zce'
使用 __webpack_require__.n 方法添加default 并赋予值 default 和别的值 区别时 多调一次 __webpack_require__.n 方法
__webpack_require__.d 添加属性值
__webpack_require__.o 判断是否esmodule
打包的最后一个环节
两种规范都可以使用 作打包处理 CJS处理的结果更少 但还是根据情况使用
7.功能函数手写
上
手写 mybuilt.js hzj.js
从点头到尾写一遍 不包含__webpack_require__.t 方法
下
esmodule模式下的验证
__webpack_require__下的r和d方法
8.懒加载流程梳理
懒加载 打包的时候并没有加载
import函数 then()
多出两个方法
webpack_require.e(“login”)
返回一个 Promise.all
webpack_require.t.bind (null, “./src/login.js”, 7)
null 修改 this 不关注
7 二进制 001
返回 一个 ns
1.import()可以实现指定模块的懒加载操作
2.核心原理时 jsonp
3.t 方法针对内容进行不同处理 处理方法取决于传入的数值( mode,8 6 7 3 2 1)
9.T方法分析及实现
webpack_require.t(value,mode) 导出模块定义或者处理再导出
01 接收二个参数,一个是 value 一般用于表示被加载的模块id ,第二个值 mode 是一个二进制的数值
02 t 方法内部做的第一件事情就是调用自定义的 require 方法加载value 对应的模块导出,重新赋值给 value
03 当获取到了这个 value 值之后余下的 8 4 ns 2 都是对当前的内容进行加工处理,然后返回使用
04 当mode & 8 成立是直接将 value 返回 ( commonJS )
05 当 mode & 4 成立时直接将 value 返回(esModule)
06 如果上述条件都不成立,还是要继续处理 value ,定义一个 ns {}
6-1 如果拿到的 value 是一个可以直接使用的内容,例如是一个字符串,将它挂载到 ns 的 default 属性上
6-2 如果不是可直接使用的,就先遍历对象的值,给ns上加各种属性,方便直接调用
按位方式
a 1000 8
b 0010 2
a&b 0000 0
10.懒加载源码分析
上
.bind() 补充之前函数的第一个入参修改
第一个参数会作为原函数运行时的 this 指向
webpackJsonpCallback
1.模块依赖关系的合并
2.让当前的promise变成 成功的状态
jsonpArray.push = webpackJsonpCallback; 也重写了window["webpackJsonp"]
下 实现懒加载的流程
__webpack_require__.e 方法 返回promise jsonp
installedChunkData 为0表示已加载
undefined 未加载
promise 正在加载
undefined的时候
installedChunkData 添加 resolve reject 和promise
调用 jsonpScriptSrc 方法
document.head.appendChild(script); 添加一个head标签
返回Promise.all(promises); 但是promise没有进入成功态
这个时候加载指定的文件内容
进入webpackJsonpCallback
获取需要加载的id chunkId = chunkIds[i];
获取模块的resolve方法
标记加载过 installedChunks[chunkId] = 0;
判断模块数据有没有当前要加载的模块数据 并复制
执行resolve方法
进入__webpack_require__.t 方法
加载模块 获得导出模块内容
对模块内容进行处理 创建一个ns对象 并将模块内容加进去
返回ns对象(导出内容)
jsonpScriptSrc 路径和命名
11.手写懒加载源码
webpackJsonpCallback 回调方法重写
Object.prototype.hasOwnProperty.call(installedChunks, chunkId) 判断是否加载了
将 模块的resolve 放到resolves
将关系放到modules上
执行放到resolves里的resolve方法
__webpack_require__.e 实现json加载内容 利用promise实现异步加载操作
有primise就执行 没有就创建
var promise = new Promise(function (resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
promises.push(installedChunkData[2] = promise);
12.webpack 与tapable 底层核心工具库
配置初始化
内容编译
编译后输出
事件驱动型事件流工作机制
bundles的compilation
tapable 独立的库
实例化hook注册事件监听
通过hook触发事件监听
执行懒编译生成的可执行代码
hook本质是tapable实例对象 同步和异步
Hook 普通钩子 监听器之间互相独立不干扰
BailHook 熔断钩子 某个监听返回非undefined时 后续不执行
WaterfallHook 瀑布钩子 上一个监听的返回值可传递至下一个
LoopHook 循环钩子 如果当前未返回false 则一直执行
同步钩子
SynckHook
SyncBailHook
SyncWaterfallHook
SyncLoopHook
异步串行钩子
AsyncSeriesHook
AsyncSeriesBailHook
AsyncSeriesWaterfallHook
异步并行钩子
AsyncParalleHook
AsyncParalleBailHook
13.同步钩子的使用及调试
依赖 tapable
SyncHook
导入SyncHook require(‘tapable’)
定义Hook实例 new SyncHook([‘name’, ‘age’])
tap 设置监听
call 触发监听
SyncBailHook 同理 熔断:某一个地方断掉了 就不执行了
return 非 undefined 后续监听不执行了
SyncWaterfallHook
return 的东西可以给下一个钩子使用
SyncLoopHook
在钩子循环结束前 不会执行之后的钩子 只会执行前面的钩子
taps里面放了多个数组
如果当前的钩子返回undefined 则会从头循环钩子 从头开始循环
14.异步钩子的使用及调试
安装tapable库
库里有很多类
1.AsyncParallelHook 并行钩子
异步钩子 添加事件监听有三种方式
tap tapAsync tapPromise
tap
hook.tap(‘fn1’, function (name) {
console.log(‘fn1—>’, name)
})
allAsync 后面的参数要回调函数
hook.callAsync(‘zoe’, function () {
console.log(‘最后执行了回调操作’)
})
tapAsync
hook.tapAsync('fn1', function (name, callback) {
setTimeout(() => {
console.log('fn1--->', name)
callback()
}, 1000)
})
hook.callAsync('lg', function () {
console.log('最后一个回调执行了')
console.timeEnd('time')
})
tapPromise 回调方式通过promise实现
hook.tapPromise('fn1', function (name) {
return new Promise(function (resolve, reject) {
setTimeout(() => {
console.log('fn1--->', name)
resolve()
}, 1000)
})
})
调用 直接使用promise
hook.promise('foo').then(() => {
console.log('end执行了')
console.timeEnd('time')
})
2.AsyncParallelBailHook 并行异步熔断钩子
hook.tapAsync('fn2', function (name, callback) {
setTimeout(() => {
console.log('fn2--->', name)
callback('err')
}, 2000)
})
执行到错误的钩子然后回调 callAsync函数
回调callAsync函数之后执行 报错误的钩子后的钩子
3.AsyncSeriesHook 异步串行钩子
同一 只是是串行的
15.SyncHook源码分析
Hook.js是父类
HookCodeFactory 钩子代码工厂 继承
上
先实例化Hook 再SyncHook
构造Hook 简写
this.args = args
this.taps = [] // 将来用于存放组装好的 {} 默认空数组
this._x = undefined // 将来在代码工厂函数中会给 _x = [f1, f2, f3…]
赋值一系列方法
重写 SyncHook 里的方法修饰 主要还是调用Hook的方法
tap方法 将字符串转化为对象
调用_insert 方法 将东西添加到taps里面
_insert
this.taps[this.taps.length] = options 将内容转换成对象存到taps里面
下
call方法
里面调用_createCall方法
_createCall
返回对象this.compile
taps: this.taps,
args: this.args
到了 SyncHook的compile方法 调了factory的setup方法 返回create方法
factory
setup 把taps的函数放在了.x属性里面
create 心就是创建一段可执行的代码体然后返回 然后拼接头和身体
16.SyncHook手写实现
上 监听
* 01 实例化 hook , 定义 _x = [f1, f2, …] taps = [{}, {}]
* 02 实例调用 tap taps = [{}, {}]
* 03 调用 call 方法, HookCodeFactory setup create
* 04 Hook SyncHook HookCodeFactory
下 调用
call()
核心为factory的 create方法
组装函数 fn = new Function("name, age",
"var _x = this._x, var _fn0 = _x[0]; _fn0(name, age);")
head content
17.AsyncParalleHook源码分析
核心为factory的 create方法
组装函数 核心为content方法 判断是否执行完最后的事情 再回调
相比同步函数 异步函数需要有一个最后的回调函数
18.定位webpack打包入口
执行入口
complier
run方法
看源码不需要从上到下看懂全部的含义
从执行顺序看
01 cmd 文件核心的作用就组装了 node *****/webpack/bin/webpack.js
02 webpack.js 中核心的操作就是 require 了 node_modules/webpack-cli/bin/cli.js
03 cli.js
01 当前文件一般有二个操作,处理参数,将参数交给不同的逻辑(分发业务)
02 options
03 complier
04 complier.run( 至于run 里面做了什么,后续再看,当前只关注代码入口点 )
19.编译主流程分析
options就是webpack.config.js导出的内容
complier继承了tapable的类
主流程编译流程:
合并默认配置和自定义配置 —》 实例化compiler 设置Node文件读写的能力 通过循环 挂载plugins 处理webpack内部默认的插件(入口文件)
触发配置
Compiler.beforeRun
Compiler.run
Compiler.beforeCompile
Compiler.compile 编译
Compiler.make
20.手写webpack.js
01 实例化 compiler 对象
02 初始化 NodeEnvironmentPlugin(让compiler具体文件读写能力)
03 挂载所有 plugins 插件至 compiler 对象身上
04 挂载所有 webpack 内置的插件(入口)
05 返回 compiler 对象即可
1.run.js
使用 2.webpack.js 调用webpack.config.js的内容作为options
生成compiler
引用NodeEnvironmentPlugin类 3.NodeEnvironmentPlugin.js
调用 compiler的 run方法 4.compiler .js
21.EntryOptionPlugin webpack.config.js里的入口配置插件管理
WebpackOptionsApply实例对象
process(options, compiler) 方法
EntryOptionPlugin 类 apply 方法
里面的 entryOption 是一个钩子实例,
entryOption 在 EntryOptionPlugin 内部的 apply 方法中调用了 tap (注册了事件监听)
上述的事件监听在 new 完了 EntryOptionPlugin 之后就调用了
itemToPlugin, 它是一个函数,接收三个参数( context item 'main)
SingleEntryPlugin
在调用 itemToPlugin, 的时候又返回了一个 实例对象
有一个构造函数,负责接收上文中的 context entry name
compilation 钩子监听
make 钩子监听
22.手写EntryOptionPlugin 流程
先新建 文件 WebpackOptionsApply.js 声明类 WebpackOptionsApply WebpackOptionsApply.js
调用 process(options, compiler)方法 WebpackOptionsApply.js
里面调用了 EntryOptionPlugin 的 apply方法 EntryOptionPlugin.js
方法里定义了一个钩子 entryOption EntryOptionPlugin.js
钩子调用了 itemToPlugin 函数 EntryOptionPlugin.js
函数返回了 SingleEntryPlugin 的实例的apply方法 SingleEntryPlugin.js
apply方法里监听了make 异步钩子 SingleEntryPlugin.js
compilation 触发
23.run方法实现
run方法流程
finalCallback方法 调用callback
onCompiled 对callbak处理
this.hooks.beforeRun 触发钩子
24.compile方法实现
newCompilationParams 方法 获取参数
normalModuleFactory 模块工厂 还有上下文等等
beforeRun 触发钩子的监听
compile 触发钩子的监听
创建一个 compilation newCompilation类 传入最开始的参数
make 触发钩子的监听
-------------------------------------------------------------------------------
02 compile 方法执行
1 准备参数(其中 normalModuleFactory 是我们后续用于创建模块的)
2 触发beforeCompile
3 将第一步的参数传给一个函数,开始创建一个 compilation (newCompilation)
4 在调用 newCompilation 的内部
- 调用了 createCompilation
- 触发了 this.compilation 钩子 和 compilation 钩子的监听
------------------------------------------------------------------------------------
25.make前流程回顾
一、步骤 run.js
01 实例化 compiler 对象( 它会贯穿整个webpack工作的过程 )
02 由 compiler 调用 run 方法
二、compiler 实例化操作 webpack.js
01 compiler 继承 tapable,因此它具备钩子的操作能力(监听事件,触发事件,webpack是一个事件流)
02 在实例化了 compiler 对象之后就往它的身上挂载很多属性,其中 NodeEnvironmentPlugin 这个操作就让它具备了
文件读写的能力(我们的模拟时采用的是 node 自带的 fs )
03 具备了 fs 操作能力之后又将 plugins 中的插件都挂载到了 compiler 对象身上
04 将内部默认的插件与 compiler 建立关系,其中 EntryOptionPlugin 处理了入口模块的 id
05 在实例化 compiler 的时候只是监听了 make 钩子(SingleEntryPlugin)
5-1 在 SingleEntryPlugin 模块的 apply 方法中有二个钩子监听 compilation和make
5-2 其中 compilation 钩子就是让 compilation 具备了利用 normalModuleFactory 工厂创建一个普通模块的能力
5-3 因为它就是利用一个自己创建的模块来加载需要被打包的模块
5-4 其中 make 钩子 在 compiler.run 的时候会被触发,走到这里就意味着某个模块执行打包之前的所有准备工作就完成了
5-5 addEntry 方法调用()
三、run 方法执行( 当前想看的是什么时候触发了 make 钩子 ) compiler.js
01 run 方法里就是一堆钩子按着顺序触发(beforeRun run compile)
02 compile 方法执行
1 准备参数(其中 normalModuleFactory 是我们后续用于创建模块的)
2 触发beforeCompile
3 将第一步的参数传给一个函数,开始创建一个 compilation (newCompilation)
4 在调用 newCompilation 的内部
- 调用了 createCompilation
- 触发了 this.compilation 钩子 和 compilation 钩子的监听
03 当创建了 compilation 对象之后就触发了 make 钩子
04 当我们触发 make 钩子监听的时候,将 compilation 对象传递了过去
四、总结
1 实例化 compiler
2 调用 compile 方法
3 newCompilation
4 实例化了一个 compilation 对象(它和 compiler 是有关系)
5 触发 make 监听
6 addEntry 方法(这个时候就带着 context name entry 一堆的东西) 就奔着编译去了.....
1.内容概述
1.webpack打包流程拆分不同的环节
2.做了什么工作
3.如何完成的
4.使用了什么技术
5.模块的加载和loader的使用
6.webpack的配置演示
2.打包后源码分析
path: path.resolve(‘dist’) 输出目录的路径
对输出文件进行分析
1.我们称之为模块定义
“./src/index.js”: 路径模块id 视为键值对
(function (module, exports) {
console.log(‘index.js内容’)
module.exports = ‘入口文件导出内容’ //模块定义
})
值是一个函数的调用
传递模块的参数
/**
* 01 打包后的文件就是一个函数自调用,当前函数调用时传入一个对象
* 02 这个对象我们为了方便将之称为是模块定义,它就是一个键值对
* 03 这个键名就是当前被加载模块的文件名与某个目录的拼接()
* 04 这个键值就是一个函数,和 node.js 里的模块加载有一些类似,会将被加载模块中的内容包裹于一个函数中
* 05 这个函数在将来某个时间点上会被调用,同时会接收到一定的参数,利用这些参数就可以实现模块的加载操作
*
* 06 针对于上述的代码就相当于是将 {}(模块定义) 传递给了 modules
*/
2.installedModules 缓存 存放将来被加载的模块对象
3. function __webpack_require__(moduleId) 核心作用是返回模块的导出
return了 module.exports
1.先判断installedModules缓存里面有没有
4.__webpack_require__.t 里面调用了 3的方法
return __webpack_require__(__webpack_require__.s = "./src/index.js") 进入方法的调用
5.__webpack_require__.s 缓存路径的 这个语句不影响
6.初始化环境
完成最简单的打包操作
生成输出文件
匿名函数自动调用 实参
路径 模块的id 路径+文件名
值为函数 被加载模块的内容 外部包裹函数 require函数里面调用
调用就可以拿到模块的加载内容
3.单文件打包后源码调试
添加属性和方法
连续赋值 从右往左看
modules[moduleId] 为函数
调用函数
modules[moduleId].call(module.exports, module, module.exports, webpack_require);
module.l= true 表示加载过了
得到自调用函数 生成模块定义
模块定义传给modules
调用reqire方法 传入模块id
调用模块方法 导出module.exports
4.功能函数说明
引入其他模块分析执行流程
在主js文件里 会有导入附属js 的语句
let name = webpack_require(/*! ./login.js */ “./src/login.js”)
模块定义里有多个键值对了
1.installedModules 缓存 存放将来被加载的模块对象
2. __webpack_require__(moduleId) 核心作用是返回模块的导出
3.__webpack_require__.m = modules 将模块定义保存一份,通过 m 属性挂载到自定义的方法身上
4. __webpack_require__.c = installedModules 保存缓存
5.__webpack_require__.o 判断被传入的对象 obj 身上是否具有指定的属性 **** ,如果有则返回 true
6. __webpack_require__.d 通过 o 方法判断是否没有 name 属性 并且添加一个name属性 和访问器
7.__webpack_require__.r
1.判断是否是esmodules
是的话添加一个 值为 module 的Symbol
2.添加一个 __esModule 属性
8.__webpack_require__.t 未调用
调用后会拿到模块中的内容value
对于 value 来说我们可能会直接返回,也可能会处理之后再返回
9.__webpack_require__.n 分会module的default属性 未调用
10.__webpack_require__.p 配置里面的public属性
5.commonJS模块打包
主js采用 require函数
只是对__webpack_require__ 只是 对require进行了替换 就是上面学习的例子
js里面
esModules模块
调用了__webpack_require__.r 方法 判断是否esmodules 并且添加 Module的的Symbol
添加 __esModule 属性 value 为true
调用了__webpack_require__.d 处理age 属性 作为d方法里面name值 添加age属性和访问器
6.esModule 打包
主js 采用 import from
调用了__webpack_require__.r 方法 判断是否esmodules 并且添加 Module的的Symbol
添加 _esModule 属性 value 为true
webpack自定义方法 替换
var login_js__WEBPACK_IMPORTED_MODULE_0 = webpack_require(/*! ./login.js */ “./src/login.js”);
module.exports = 'zce'
使用 __webpack_require__.n 方法添加default 并赋予值 default 和别的值 区别时 多调一次 __webpack_require__.n 方法
__webpack_require__.d 添加属性值
__webpack_require__.o 判断是否esmodule
打包的最后一个环节
两种规范都可以使用 作打包处理 CJS处理的结果更少 但还是根据情况使用
7.功能函数手写
上
手写 mybuilt.js hzj.js
从点头到尾写一遍 不包含__webpack_require__.t 方法
下
esmodule模式下的验证
__webpack_require__下的r和d方法
8.懒加载流程梳理
懒加载 打包的时候并没有加载
import函数 then()
多出两个方法
webpack_require.e(“login”)
返回一个 Promise.all
webpack_require.t.bind (null, “./src/login.js”, 7)
null 修改 this 不关注
7 二进制 001
返回 一个 ns
1.import()可以实现指定模块的懒加载操作
2.核心原理时 jsonp
3.t 方法针对内容进行不同处理 处理方法取决于传入的数值( mode,8 6 7 3 2 1)
9.T方法分析及实现
webpack_require.t(value,mode) 导出模块定义或者处理再导出
01 接收二个参数,一个是 value 一般用于表示被加载的模块id ,第二个值 mode 是一个二进制的数值
02 t 方法内部做的第一件事情就是调用自定义的 require 方法加载value 对应的模块导出,重新赋值给 value
03 当获取到了这个 value 值之后余下的 8 4 ns 2 都是对当前的内容进行加工处理,然后返回使用
04 当mode & 8 成立是直接将 value 返回 ( commonJS )
05 当 mode & 4 成立时直接将 value 返回(esModule)
06 如果上述条件都不成立,还是要继续处理 value ,定义一个 ns {}
6-1 如果拿到的 value 是一个可以直接使用的内容,例如是一个字符串,将它挂载到 ns 的 default 属性上
6-2 如果不是可直接使用的,就先遍历对象的值,给ns上加各种属性,方便直接调用
按位方式
a 1000 8
b 0010 2
a&b 0000 0
10.懒加载源码分析
上
.bind() 补充之前函数的第一个入参修改
第一个参数会作为原函数运行时的 this 指向
webpackJsonpCallback
1.模块依赖关系的合并
2.让当前的promise变成 成功的状态
jsonpArray.push = webpackJsonpCallback; 也重写了window["webpackJsonp"]
下 实现懒加载的流程
__webpack_require__.e 方法 返回promise jsonp
installedChunkData 为0表示已加载
undefined 未加载
promise 正在加载
undefined的时候
installedChunkData 添加 resolve reject 和promise
调用 jsonpScriptSrc 方法
document.head.appendChild(script); 添加一个head标签
返回Promise.all(promises); 但是promise没有进入成功态
这个时候加载指定的文件内容
进入webpackJsonpCallback
获取需要加载的id chunkId = chunkIds[i];
获取模块的resolve方法
标记加载过 installedChunks[chunkId] = 0;
判断模块数据有没有当前要加载的模块数据 并复制
执行resolve方法
进入__webpack_require__.t 方法
加载模块 获得导出模块内容
对模块内容进行处理 创建一个ns对象 并将模块内容加进去
返回ns对象(导出内容)
jsonpScriptSrc 路径和命名
11.手写懒加载源码
webpackJsonpCallback 回调方法重写
Object.prototype.hasOwnProperty.call(installedChunks, chunkId) 判断是否加载了
将 模块的resolve 放到resolves
将关系放到modules上
执行放到resolves里的resolve方法
__webpack_require__.e 实现json加载内容 利用promise实现异步加载操作
有primise就执行 没有就创建
var promise = new Promise(function (resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
promises.push(installedChunkData[2] = promise);
12.webpack 与tapable 底层核心工具库
配置初始化
内容编译
编译后输出
事件驱动型事件流工作机制
bundles的compilation
tapable 独立的库
实例化hook注册事件监听
通过hook触发事件监听
执行懒编译生成的可执行代码
hook本质是tapable实例对象 同步和异步
Hook 普通钩子 监听器之间互相独立不干扰
BailHook 熔断钩子 某个监听返回非undefined时 后续不执行
WaterfallHook 瀑布钩子 上一个监听的返回值可传递至下一个
LoopHook 循环钩子 如果当前未返回false 则一直执行
同步钩子
SynckHook
SyncBailHook
SyncWaterfallHook
SyncLoopHook
异步串行钩子
AsyncSeriesHook
AsyncSeriesBailHook
AsyncSeriesWaterfallHook
异步并行钩子
AsyncParalleHook
AsyncParalleBailHook
13.同步钩子的使用及调试
依赖 tapable
SyncHook
导入SyncHook require(‘tapable’)
定义Hook实例 new SyncHook([‘name’, ‘age’])
tap 设置监听
call 触发监听
SyncBailHook 同理 熔断:某一个地方断掉了 就不执行了
return 非 undefined 后续监听不执行了
SyncWaterfallHook
return 的东西可以给下一个钩子使用
SyncLoopHook
在钩子循环结束前 不会执行之后的钩子 只会执行前面的钩子
taps里面放了多个数组
如果当前的钩子返回undefined 则会从头循环钩子 从头开始循环
14.异步钩子的使用及调试
安装tapable库
库里有很多类
1.AsyncParallelHook 并行钩子
异步钩子 添加事件监听有三种方式
tap tapAsync tapPromise
tap
hook.tap(‘fn1’, function (name) {
console.log(‘fn1—>’, name)
})
allAsync 后面的参数要回调函数
hook.callAsync(‘zoe’, function () {
console.log(‘最后执行了回调操作’)
})
tapAsync
hook.tapAsync('fn1', function (name, callback) {
setTimeout(() => {
console.log('fn1--->', name)
callback()
}, 1000)
})
hook.callAsync('lg', function () {
console.log('最后一个回调执行了')
console.timeEnd('time')
})
tapPromise 回调方式通过promise实现
hook.tapPromise('fn1', function (name) {
return new Promise(function (resolve, reject) {
setTimeout(() => {
console.log('fn1--->', name)
resolve()
}, 1000)
})
})
调用 直接使用promise
hook.promise('foo').then(() => {
console.log('end执行了')
console.timeEnd('time')
})
2.AsyncParallelBailHook 并行异步熔断钩子
hook.tapAsync('fn2', function (name, callback) {
setTimeout(() => {
console.log('fn2--->', name)
callback('err')
}, 2000)
})
执行到错误的钩子然后回调 callAsync函数
回调callAsync函数之后执行 报错误的钩子后的钩子
3.AsyncSeriesHook 异步串行钩子
同一 只是是串行的
15.SyncHook源码分析
Hook.js是父类
HookCodeFactory 钩子代码工厂 继承
上
先实例化Hook 再SyncHook
构造Hook 简写
this.args = args
this.taps = [] // 将来用于存放组装好的 {} 默认空数组
this._x = undefined // 将来在代码工厂函数中会给 _x = [f1, f2, f3…]
赋值一系列方法
重写 SyncHook 里的方法修饰 主要还是调用Hook的方法
tap方法 将字符串转化为对象
调用_insert 方法 将东西添加到taps里面
_insert
this.taps[this.taps.length] = options 将内容转换成对象存到taps里面
下
call方法
里面调用_createCall方法
_createCall
返回对象this.compile
taps: this.taps,
args: this.args
到了 SyncHook的compile方法 调了factory的setup方法 返回create方法
factory
setup 把taps的函数放在了.x属性里面
create 心就是创建一段可执行的代码体然后返回 然后拼接头和身体
16.SyncHook手写实现
上 监听
* 01 实例化 hook , 定义 _x = [f1, f2, …] taps = [{}, {}]
* 02 实例调用 tap taps = [{}, {}]
* 03 调用 call 方法, HookCodeFactory setup create
* 04 Hook SyncHook HookCodeFactory
下 调用
call()
核心为factory的 create方法
组装函数 fn = new Function("name, age",
"var _x = this._x, var _fn0 = _x[0]; _fn0(name, age);")
head content
17.AsyncParalleHook源码分析
核心为factory的 create方法
组装函数 核心为content方法 判断是否执行完最后的事情 再回调
相比同步函数 异步函数需要有一个最后的回调函数
18.定位webpack打包入口
执行入口
complier
run方法
看源码不需要从上到下看懂全部的含义
从执行顺序看
01 cmd 文件核心的作用就组装了 node *****/webpack/bin/webpack.js
02 webpack.js 中核心的操作就是 require 了 node_modules/webpack-cli/bin/cli.js
03 cli.js
01 当前文件一般有二个操作,处理参数,将参数交给不同的逻辑(分发业务)
02 options
03 complier
04 complier.run( 至于run 里面做了什么,后续再看,当前只关注代码入口点 )
19.编译主流程分析
options就是webpack.config.js导出的内容
complier继承了tapable的类
主流程编译流程:
合并默认配置和自定义配置 —》 实例化compiler 设置Node文件读写的能力 通过循环 挂载plugins 处理webpack内部默认的插件(入口文件)
触发配置
Compiler.beforeRun
Compiler.run
Compiler.beforeCompile
Compiler.compile 编译
Compiler.make
20.手写webpack.js
01 实例化 compiler 对象
02 初始化 NodeEnvironmentPlugin(让compiler具体文件读写能力)
03 挂载所有 plugins 插件至 compiler 对象身上
04 挂载所有 webpack 内置的插件(入口)
05 返回 compiler 对象即可
1.run.js
使用 2.webpack.js 调用webpack.config.js的内容作为options
生成compiler
引用NodeEnvironmentPlugin类 3.NodeEnvironmentPlugin.js
调用 compiler的 run方法 4.compiler .js
21.EntryOptionPlugin webpack.config.js里的入口配置插件管理
WebpackOptionsApply实例对象
process(options, compiler) 方法
EntryOptionPlugin 类 apply 方法
里面的 entryOption 是一个钩子实例,
entryOption 在 EntryOptionPlugin 内部的 apply 方法中调用了 tap (注册了事件监听)
上述的事件监听在 new 完了 EntryOptionPlugin 之后就调用了
itemToPlugin, 它是一个函数,接收三个参数( context item 'main)
SingleEntryPlugin
在调用 itemToPlugin, 的时候又返回了一个 实例对象
有一个构造函数,负责接收上文中的 context entry name
compilation 钩子监听
make 钩子监听
22.手写EntryOptionPlugin 流程
先新建 文件 WebpackOptionsApply.js 声明类 WebpackOptionsApply WebpackOptionsApply.js
调用 process(options, compiler)方法 WebpackOptionsApply.js
里面调用了 EntryOptionPlugin 的 apply方法 EntryOptionPlugin.js
方法里定义了一个钩子 entryOption EntryOptionPlugin.js
钩子调用了 itemToPlugin 函数 EntryOptionPlugin.js
函数返回了 SingleEntryPlugin 的实例的apply方法 SingleEntryPlugin.js
apply方法里监听了make 异步钩子 SingleEntryPlugin.js
compilation 触发
23.run方法实现
run方法流程
finalCallback方法 调用callback
onCompiled 对callbak处理
this.hooks.beforeRun 触发钩子
24.compile方法实现
newCompilationParams 方法 获取参数
normalModuleFactory 模块工厂 还有上下文等等
beforeRun 触发钩子的监听
compile 触发钩子的监听
创建一个 compilation newCompilation类 传入最开始的参数
make 触发钩子的监听
-------------------------------------------------------------------------------
02 compile 方法执行
1 准备参数(其中 normalModuleFactory 是我们后续用于创建模块的)
2 触发beforeCompile
3 将第一步的参数传给一个函数,开始创建一个 compilation (newCompilation)
4 在调用 newCompilation 的内部
- 调用了 createCompilation
- 触发了 this.compilation 钩子 和 compilation 钩子的监听
------------------------------------------------------------------------------------
25.make前流程回顾
一、步骤 run.js
01 实例化 compiler 对象( 它会贯穿整个webpack工作的过程 )
02 由 compiler 调用 run 方法
二、compiler 实例化操作 webpack.js
01 compiler 继承 tapable,因此它具备钩子的操作能力(监听事件,触发事件,webpack是一个事件流)
02 在实例化了 compiler 对象之后就往它的身上挂载很多属性,其中 NodeEnvironmentPlugin 这个操作就让它具备了
文件读写的能力(我们的模拟时采用的是 node 自带的 fs )
03 具备了 fs 操作能力之后又将 plugins 中的插件都挂载到了 compiler 对象身上
04 将内部默认的插件与 compiler 建立关系,其中 EntryOptionPlugin 处理了入口模块的 id
05 在实例化 compiler 的时候只是监听了 make 钩子(SingleEntryPlugin)
5-1 在 SingleEntryPlugin 模块的 apply 方法中有二个钩子监听 compilation和make
5-2 其中 compilation 钩子就是让 compilation 具备了利用 normalModuleFactory 工厂创建一个普通模块的能力
5-3 因为它就是利用一个自己创建的模块来加载需要被打包的模块
5-4 其中 make 钩子 在 compiler.run 的时候会被触发,走到这里就意味着某个模块执行打包之前的所有准备工作就完成了
5-5 addEntry 方法调用()
三、run 方法执行( 当前想看的是什么时候触发了 make 钩子 ) compiler.js
01 run 方法里就是一堆钩子按着顺序触发(beforeRun run compile)
02 compile 方法执行
1 准备参数(其中 normalModuleFactory 是我们后续用于创建模块的)
2 触发beforeCompile
3 将第一步的参数传给一个函数,开始创建一个 compilation (newCompilation)
4 在调用 newCompilation 的内部
- 调用了 createCompilation
- 触发了 this.compilation 钩子 和 compilation 钩子的监听
03 当创建了 compilation 对象之后就触发了 make 钩子
04 当我们触发 make 钩子监听的时候,将 compilation 对象传递了过去
四、总结
1 实例化 compiler
2 调用 compile 方法
3 newCompilation
4 实例化了一个 compilation 对象(它和 compiler 是有关系)
5 触发 make 监听
6 addEntry 方法(这个时候就带着 context name entry 一堆的东西) 就奔着编译去了.....
26.addEntry流程分析
上
01 make 钩子在被触发的时候,接收到了 compilation 对象实现,它的身上挂载了很多内容
02 从 compilation 当中解构了三个值
entry : 当前需要被打包的模块的相对路径(./src/index.js)
name: main
context: 当前项目的根路径
03 dep 是对当前的入口模块中的依赖关系进行处理
04 调用了 addEntry 方法
05 在 compilation实例的身上有一个 addEntry 方法,然后内部调用了 _addModuleChain 方法,去处理依赖
_addModuleChain方法内
06 在 compilation 当中我们可以通过 NormalModuleFactory 工厂来创建一个普通的模块对象
07 在 webpack 内部默认启了一个 100 并发量的打包操作,当前我们看到的是 normalModule.create()
08 在 beforeResolve 里面会触发一个 factory 钩子监听【 这个部分的操作其实是处理 loader, 当前我们重点去看 】
09 上述操作完成之后获取到了一个函数被存在 factory 里,然后对它进行了调用
10 在这个函数调用里又触发了一个叫 resolver 的钩子( 处理 loader的,拿到了 resolver方法就意味着所有的Loader 处理完毕 )
11 调用 resolver() 方法之后,就会进入到 afterResolve 这个钩子里,然后就会触发 new NormalModule
下
12 在完成上述操作之后就将module 进行了保存和一些其它属性的添加
_addModuleChain方法内
13 调用 buildModule 方法开始编译---》 调用 build ---》doBuild
build
* 01 从文件中读取到将来需要被加载的 module 内容,这个
* 02 如果当前不是 js 模块则需要 Loader 进行处理,最终返回 js 模块
* 03 上述的操作完成之后就可以将 js 代码转为 ast 语法树
* 04 当前 js 模块内部可能又引用了很多其它的模块,因此我们需要递归完成
* 05 前面的完成之后,我们只需要重复执行即可
27.addEnter 初始化
手写 Compilation.js
构造方法
addEntry方法
_addModuleChain方法
28._addModuleChain实现
创建 NormalModule 命名 entryModule
调用 buildModule 方法 创建模块
29.buildModule 实现
调用 NormalModule 的build方法
在 NormalModule.js调用 doBuild方法
doBuild方法
_source 通过 getSource 方法 读取文件内容并存取
_ast 通过 parse 实现语法树?
30.build及parse实现
babylon模块
return babylon.parse(source, {
sourceType: ‘module’,
plugins: [‘dynamicImport’] // 当前插件可以支持 import() 动态导入的语法
})
status 动态传参
31.处理模块依赖
打包 :创建一个新的模块 ,让这个模块加载需要被打包的模块的内容 ,对内容进行一些处理
01 需要将 Index.js 里的 require 方法替换成 __webpack_require__
02 还有将 ./title 替换成 ./src/title.js
03 实现递归的操作 ,所以要将依赖的模块信息保存好,方例交给下一次 create
操作 _ast 语法树 内容处理
在 dobuild 方法 转换语法树的时候操作
generator _ast 改完之后不能运行 ,需要再转换成代码 @babel/generator
traverse 进到这个语法里面取修改 @babel/traverse
// 这里的 _ast 就是当前 module 的语法树,我们可以对它进行修改,最后再将 ast 转回成 code 代码
替换内容
node.arguments = [types.stringLiteral(depModuleId)] 可以直接换数组内容 @babel/types
afterBuild 处理依赖
通过 processDependencies 方法实现模块依赖加载
// 01 当前的函数核心功能就是实现一个被依赖模块的递归加载
// 02 加载模块的思想都是创建一个模块,然后想办法将被加载模块的内容拿进来?
// 03 当前我们不知道 module 需要依赖几个模块, 此时我们需要想办法让所有的被依赖的模块都加载完成之后再执行 callback?【 neo-async 】
32.抽离 createModule 方法
解耦创建 NormalModule 达到复用的目的
改写_addModuleChain 方法 单词创建 NormalModule 大部分逻辑转入 createModule 方法
33.编译依赖模块
完成 processDependencies
可以直接递归导入依赖 调用 createModule 加载依赖
parser, //没有这个语法树无法加载代码
34.chunk流程分析及实现
所谓封装 chunk 指的就是依据某个入口,然后找到它的所有依赖,将它们的源代码放在一起,之后再做合并
不同的模块 生成不同的代码块 chunk
make 钩子触发的时候吹chunk
compilation.seal 封装chunk
保存 chunk 信息和modules里对应的module的模块
// 01 当前所有的入口模块都被存放在了 compilation 对象的 entries 数组里
// 02 所谓封装 chunk 指的就是依据某个入口,然后找到它的所有依赖,将它们的源代码放在一起,之后再做合并
35.生成chunk代码 具体内容实现
存取文件的名字
chunk.files.push(fileName)
模板文件和源代码
// 01 获取模板文件的路径
let tempPath = path.posix.join(__dirname, ‘temp/main.ejs’)
// 02 读取模块文件中的内容
let tempCode = this.inputFileSystem.readFileSync(tempPath, ‘utf8’)
// 03 获取渲染函数
let tempRender = ejs.compile(tempCode)
// 04 按ejs的语法渲染数据
let source = tempRender({
entryModuleId: chunk.entryModule.moduleId,
modules: chunk.modules
})
资源组装到source
把source赋值到this.assets对应的文件中
36.生成打包文件
emitAssets Compiler.js
// 当前需要做的核心: 01 创建dist 02 在目录创建完成之后执行文件的写操作
在 emit 钩子调用 emitFlies 方法写入文件
emitFlies
this.outputFileSystem.writeFileSync(targetPath, source, ‘utf8’)//输出文件 路径 内容 编码级