一、说明
Webpack 本质上是一个函数,他接受一个配置信息为参数,执行后返回一个compiler对象,调用compiler对象中的run方法会启动编译。run方法接受一个回调,可以用来查看编译过程中的错误信息或编译信息
二、核心思想
- 第一步:根据配置信息
webpack.config.js
找到入口文件src/index.js
- 第二步:找到入口文件所依赖的模块,并收集关键信息:比如路径、源代码、它所依赖的模块
let modules = [
{
id: './src/name.js',
dependencies: [],
source: 'module.exports = "不要秃头啊";'
},
{
id: './src/age.js',
dependencies: [],
source: 'module.exports = "99";'
},
{
id: './src/index.js',
dependencies: ['./src/name.js', './src/age.js'],
source:
'const name = require("./src/name.js");\n' +
'const age = require("./src/age.js");\n' +
'console.log("entry文件打印作者信息", name, age);',
}
]
- 第三步:根据上一步得到的信息,生成最终输出到硬盘的文件
dist
: 包括modules对象、require模板代码、入口执行文件等
在过程中,由于浏览器不认识html、js、css以外的代码,所以我们需要对源文件进行转换--Loader系统
Loader系统:本质上就是接收资源文件,并对其进行转换,并将转换后文件进行输出
除此之外,打包过程中有一些特殊的时机需要处理,比如:
- 在打包前校验用户参数,判断格式
- 打包过程中,感知哪些文件可以忽略编译,直接引用
- 编译完成后,需要将输出的内容插入到html中
- 输出到硬盘前,需要先清空dist文件夹
- ......
这时候就需要一个可扩展的设计,给社区提供可扩展的接口 -- Plugin系统
Plugin系统本质上是一种事件流的机制,到了固定的时间节点就广播特定的事件,用户可以在事件内执行特定的逻辑,类似于生命周期。
三、架构设计
上面可以感知到,我们可以通过建立一套事件流的机制来管控整个打包过程,大致可以分为三个阶段
- 打包开始前的准备工作
- 打包过程中(也就是编译阶段)
- 打包结束后(包含打包成功和失败的处理)
在Webpack中,compiler就是总控,它代表上述三个阶段,在它上面也挂载着各种生命周期,而compilation专门负责编译相关的工作,也就是打包过程中的阶段。
实现事件流:Tapable,类似于node的EventEmitter库,但更专注于自定义事件的触发和处理,主要实现时间的监听和触发。
四、具体实现
- 第一步:搭建结构,读取配置参数,这里读取的是webpack.config.js中的参数
- 第二步:用配置参数对象初始化Compiler对象
- 第三步:挂载配置文件中的插件
- 第四步:执行Compiler对象的run方法开始执行编译
- 在编译前触发run钩子执行
- 执行compile函数启动编译
- 在compile内部初始化Compilation
- 执行Compilation中的build方法开始进行编译
- 编译完成后触发done钩子,表示编译完成
- 第五步:根据配置文件中的entry配置项找到所有的入口
- 单入口文件要兼容转化成
{main: 'xx'}
这种格式 - 多入口文件直接取配置项即可
- 单入口文件要兼容转化成
- 第六步:从入口文件出发,调用配置的loader规则,对各模块进行编译
- 6.1 把入口文件的绝对路径添加到依赖数组(
this.fileDependencies
)中,记录此次编译依赖的模块 - 6.2 得到入口模块的
module
对象(里面放着该模块的路径、依赖模块、源代码等)- 6.2.1 读取模块内容,获取源代码
- 6.2.2 创建模块对象
- 6.2.3 找到配置文件中对应的
Loader
对源代码进行翻译和替换
- 6.3 将生成的入口文件module对象push进this.modules中
- 6.1 把入口文件的绝对路径添加到依赖数组(
- 第七步:找出此模块所依赖的模块,再对依赖模块进行编译
- 7.1 先把源代码编译成AST
- 7.2 在
AST
中查找require语句,找出依赖的模块名称和绝对路径 - 7.3 将依赖模块的绝对路径push到
this.fileDependencies
中 - 7.4 生成依赖模块的
模块id
- 7.5 修改语法结构,将依赖的模块改为依赖
模块id
- 7.6 将依赖模块的信息push到该模块的
dependencies
属性中 - 7.7 生成新代码,并把转译后的源代码放到
module._source
属性上 - 7.8 对依赖模块进行编译(对
module对象
中的dependencies
进行递归执行buildModule
) - 7.9 对依赖模块编译完成后得到依赖模块的
module对象
push到this.modules
中 - 7.10 等依赖模块全部编译完成后,返回入口模块的
module
对象
- 第八步:等所有模块都编译完成后,根据模块之间的依赖关系,组装代码块
chunk
- 8.1 每个入口文件会对应生成一个代码块
chunk
,每个代码块里面会放着本入口模块和它依赖的模块 - 8.2 将生成的
chunk
对象push进this.chunks
中去
- 8.1 每个入口文件会对应生成一个代码块
- 第九步:把各个代码块chunk转换成一个一个文件加入到输出列表
- 9.1 对
this.chunks
进行遍历 - 9.2 对每个chunk结合配置文件中的output.filename生成filename和运行时代码
- 9.3 放到
this.assets
中
- 9.1 对
- 第十步:确定好输出内容之后,根据配置的输出路径和文件名,将文件内容写入到文件系统
- 第十一步: 如果要实现热更新的话
- 11.1 遍历
fileDependencies
,对其中每一个文件进行监听 - 11.2 监听触发后,重新执行
compile(onCompiled)
方法
- 11.1 遍历