webpack学习笔记

32 阅读5分钟

一、说明

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中
  • 第七步:找出此模块所依赖的模块,再对依赖模块进行编译
    • 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中去
  • 第九步:把各个代码块chunk转换成一个一个文件加入到输出列表
    • 9.1 对this.chunks进行遍历
    • 9.2 对每个chunk结合配置文件中的output.filename生成filename和运行时代码
    • 9.3 放到this.assets
  • 第十步:确定好输出内容之后,根据配置的输出路径和文件名,将文件内容写入到文件系统
  • 第十一步: 如果要实现热更新的话
    • 11.1 遍历fileDependencies,对其中每一个文件进行监听
    • 11.2 监听触发后,重新执行compile(onCompiled)方法

参考文档