Webpack的工作流程

v2-2e1d66f4a0900fdf4ae06010f45262fb_720w.jpg

  1. 初始化阶段
    1. 初始化参数:从配置文件、配置对象、Shell参数中读取,与默认参数结合得到最终参数
    2. 创建编译器对象:用上一步得到的参数创建Compiler对象
    3. 初始化编译环境:包括注入内置插件、注册各种模块工厂、初始化RuleSet集合、加载配置的插件
    4. 开始编译:执行compiler对象的run方法
    5. 确定入口:根据配置中的entry找出所有的入口文件,调用compilation.addEntry将入口文件转换为 dependence 对象
  2. 构建阶段
    1. 编译模块(make):根据 entry 对应的 dependence 创建 module 对象,调用 loader 将模块转译为 JS 内容,调用 JS 解释器将内容转换为 AST 对象,从中找出该模块依赖的模块,再递归
    2. 完成模块编译:从上一步得到每个模块被翻译的内容以及它们之间的依赖关系图
  3. 生成阶段
    1. 输出资源(seal):根据入口和模块之间的依赖关系,组装成一个个包含多个模块的chunk,再把每个chunk转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后时机
    2. 写入文件系统(emitAssets):在确定好输出内容后,根据配置确定输出的路径和文件名,执行onCompiled 把文件内容写入到文件系统

webpack 核心原理

webpack-cli

let compiler = new webpack()

  • compiler.run
  • compiler.watch 文件变化重新 compiler.run
  • compiler.cache 开启缓存体系,创建延时写入队列

    WebpackOptionsApply

    挂载 Webpack 所有内置插件(入口)
  1. EntryOptionPlugin——入口插件增强
  2. 注入数据工厂和实例

    1. hooks.compilation 注入实例工厂 —> 准备 NormalModule
    2. hooks.make 写入入口 _addEntryItem —> build

      Webpack 函数

  3. 归一化 options

  4. 创建 compiler 实例
  5. 初始化配置的插件,注入插件钩子函数
  6. 初始化 options 值
  7. 初始化 webpack 内置插件
  8. 调度 compiler.run 开始执行构建

    Compiler

  9. new Compilation => 创建 Compilation 实例 —> 注入实例工厂 & 写入入口_addEntryItem

  10. 执行 make 钩子函数,从入口开始解释模块,再深入到依赖中,最后生成模块树
  11. 监听 make 进入 compilation.seal —> (进入 Compilation 的第一步,循环遍历 entrys 生成 chunk)
    3-1. 根据入口文件和解析模块是否有异步模块,创建 chunks
    3-2. 封装代码
  12. 执行 onCompiled => 写入本地文件
  13. 执行 done 钩子函数
    5-1. compiler.close()
    5-2. 如果配置了永久缓存会在这个时候利用空闲队列写入本地,默认的缓存就在 node_modules/.cache 目录中

    Compliration

  14. 循环遍历 entrys 生成 chunk

  15. builChunkGraph —> Template 模块

    import()、require.ensure()等方法生成的动态模块添加到 chunks 中

  16. processModuleDependencies => 根据 chunk 获得依赖

  17. hooks.optimizeChunkModules => 生成代码和封装,替换 __webpack_require__

    Module 模块

  18. 准备 NormalModule => build =>

    1. runloaders 启动 loaders
    2. loader 执行完的源码解析成 ast <-> Parser 模块

      Dependency 模块

  19. EntryDependency 入口依赖

  20. 其他依赖 —> Template 模块

    Template 模块

  • 异步资源
  • 其他 loader 资源

module chunk bundle 的区别

  1. module:我们手写下一个一个的文件,无论是 ESM 还是 commonJS ,他们都是 module
  2. 当我们写的 module 源文件传到 webpack 进行打包时,webpack 会根据文件引用关系生成 chunk 文件,webpack 会对这个 chunk 文件进行一些操作
  3. webpack 处理好 chunk 文件后,最后会输出 bundle 文件,这个 bundle 文件包含了经过加载和编译的最终源文件,所以可以直接在浏览器中运行

我们直接写出来的是 module,webpack 处理的是 chunk,最后生成浏览器可以直接运行的是 bundle

什么是模块?

模块是一组与特定功能相关的代码,封装了实现细节,公开了公共 api,并与其他模块结合以构建更大的应用程序。
模块化就是更高级的抽象,把一类或多种实现封装到一个模块中。

  • CommonJS: Node 的模块系统,同步方式导入
  • AMD(异步模块定义):RequireJS 是 AMD 最受欢迎的实现
  • UMD(通用模块定义)
  • ESModule:定义了 异步 导入和导出模块的语义

    如何打包?

  1. 解析入口文件,遍历所有依赖项
    1. @babel/parser 解析入口文件,获取 AST
    2. 通过@babel/core 的 transformFromAst 来解析入口文件的 ast
    3. 获取所有的依赖模块
      1. 定义一个依赖数组,用来存放 ast 中解析的所有依赖
      2. 使用@babel/traverse 用来遍历以及更新每一个子节点
  2. 递归解析所有依赖项,生成一个依赖关系图
  3. 利用依赖图,返回一个可以在浏览器运行的 JavaScript 文件
    1. 创建一个立即执行函数,用于在浏览器上直接运行
    2. 将依赖关系图作为参数传递给立即执行函数
    3. 立即执行入口文件
    4. 重写 require 方法,当代码运行 require(‘./message.js’)转换成 require(‘src/message.js’)
  4. 输出到 dist/bundle.js

Webpack 打包结果解析

总的来说,webpack 打包输出的结果就是一个 IIFE,通过这个 IIFE 以及 webpack_require 来支持各种模块的打包方案。

commonjs 下的打包结果

  • webpack 的 打包结果就是一个 IIFE 一般被称为 webpackbootstrap, 接收一个对象 modules 作为参数,modules 的 key 是 依赖路径,value 是 经过简单处理后的脚本。
  • 打包结果中定义了一个重要的模块加载函数 __webpack_require__
  • 首先使用 __webpack_require__ 去加载入口模块 ./src/index.js
  • 加载函数__webpack_require__ 使用了一个闭包变量 installedModules,作用是将已经加载过的结果保存在内存中。

ES 规范下的打包结果

  • 打包主体与之前的基本相同
  • 多了 __webpack_require__.r(__webpack_exports__) 这个语句。__webpack_require__.r这个方法是用来给模块的 exports 对象加上 ES 模块化规范的标记
  • 具体标记方式为:如果当前环境支持 Symbol 对象,则可以通过 Object.defineProperty 为 exports 对象的 Symbol.toStringTag 属性赋值 Module,这样做的结果是 exports 对象在调用 toString 方法时会返回 Module,同时将 exports._esModule = true

按需加载下的打包结果

产出结果变化较大:

  • 多了一个 __webpack_require__.e
    • 它初始化了一个 Promise 数组,使用 Promis.all()异步插入 script 脚本
  • 多了一个 webpackJsonp
    • webpackJsonp 会挂载到全局对象 window 上,进行模块安装

编写一个插件

插件的架构
插件是⌈具有apply方法的prototype对象⌋所实例化出来的 ,这个apply方法在安装插件时会被webpack compiler调用一次。apply 方法可以接收一个 webpack compiler 对象的引用,从而可以在回调函数中访问到 compiler 对象。

插件的组成

  • 一个JavaScript命名函数
  • 插件函数的prototype上定义一个apply方法
  • 指定一个绑定到webpack自身的事件钩子
  • 处理webpack内部实例的特定数据
  • 功能完成后调用webpack提供的回调 ```javascript function Plugin() {}

Plugin.prototype.apply = function(complier) { // 指定一个挂载到webpack自身的事件钩子 complier.plugin(‘WebpackEventHook’, function(compliation/处理webpack内部实例的特定数据/, callback) { // do something

  1. // 功能完成后调用webpack的回调函数
  2. // 异步时使用
  3. callback()

}) }

  1. <a name="d02ff2f4"></a>
  2. ### compiler 和 compilation
  3. > webpack 的构建过程是 通过 compiler 控制流程,通过 compilation 进行代码解析
  4. - compiler 对象:**它的实例包含了完整的 webpack 配置**,在启动webpack时被一次性建立,并配置好所有可操作的设置,包括options,loader,plugin。且全局只有一个 compiler 实例,因此他就像 webpack 的骨架或神经中枢。**当插件被实例化的时候,就会收到一个 compiler 对象,通过这个对象就能访问 webpack 的内部环境**
  5. - compilation 对象:代表了一次资源版本构建。 当运行webpack开发环境中间件时,每当检测到文件变化,都会创建一个新的`compliation`,从而生成一组新的编译资源。这个对象包含了当前的模块资源、编译生成资源、变化的文件以及被跟踪依赖的状态信息等。**所有构建过程中产生的构建数据都会被存储到这个对象上**。它也掌控着构建过程中的每一个环节,该对象还提供了很多事件回调供插件做扩展。
  6. <a name="loader"></a>
  7. ### loader
  8. > loader 的本质就是函数,是一个基于 CommonJS 规范的函数模块
  9. 可以通过 `loader-utils` 获取配置的 loader 的 `options`<br />通过 loader 中的 `this.callback`来返回内容,this.callback 可以传入 4 各参数:
  10. - error:loader 出错时向外抛出一个 error
  11. - content:通过 loader 编译后需要导出的内容
  12. - sourceMap:为方便调式编译后的 souceMap
  13. - ast:本次编译生成的抽象语法树。**之后执行的 loader 可以直接使用这个 AST,省去重复生成 AST 的过程**
  14. **!注意:使用 this.callback 返回内容时,该 loader 必须返回 undefined,这样 webpack 就知道该 loader 返回的结果在 this.callback 中,而不是 return 中**
  15. this 指向的是一个叫 `loaderContext`的`loader-runner`特有对象。<br /> 异步 loader 的解决方案:async-await、webpack 提供的 this.async
  16. <a name="plugin"></a>
  17. ### plugin
  18. > loader 和 plugin 的区别:
  19. > - loader 其实就是一个**转换器**,执行单纯的文件转换操作
  20. > - pluhin 是一个**扩展器**,丰富了 webpack 本身,在 loader 中的操作执行结束后,webpack 进行打包时,webpack plugin 并不直接操作文件,而是基于事件机制工作,监听 webpack 打包过程中的某些事件,见缝插针,修改打包结果。
  21. compiler 对象暴露了 webpack 整个生命周期相关的钩子。访问方法:`compiler.hooks.someHook.tap(...)`
  22. **一个自定义 webpack plugin 的骨架就是一个带有 apply 方法的类,apply 的参数接收 compiler,compiler 插入指定的事件钩子,钩子函数中可以获取 compilation 对象**
  23. 编写 webpack plugin 的套路:
  24. 1. 定义一个 JavaScript class 函数,或者在函数原型中定义一个以 compiler 对象为参数的 apply 方法
  25. 2. 在 apply 方法中通过 compiler 插入指定的事件钩子,并在钩子回调中获取 compilation 对象
  26. 3. 使用 compilation 修改 webpack 打包的内容
  27. **可以使用 tapSync 和 tabPromise 方法处理异步操作**
  28. <a name="SplitChunks"></a>
  29. ## SplitChunks
  30. > 使用这个做高性能 JS 的核心不是缓存,而是代码利用率
  31. webpack 实现 code splitting 有两种方式:
  32. 1. 借助 webpack 的配置和同步代码分析完成
  33. 2. 异步加载模块的拆分
  34. <a name="f7f2b86e"></a>
  35. ### 异步加载模块的拆分
  36. 写法:`import('xxx').then()`<br />这样的写法中 lodash 只会在调用该函数时通过`jsonp`的形式来调用加载<br />除此之外,还需要一个 babel 插件`babel-plugin-dynamic-import-webpack`来做一下转换或者用`plugin-syntax-dynamic-import`
  37. <a name="SplitChunksPlugin"></a>
  38. ### SplitChunksPlugin
  39. webpack 的默认优化:
  40. ```javascript
  41. module.exports = {
  42. // ...
  43. optimization: {
  44. splitChunks: {
  45. // 在cacheGroup外层的属性适用于所有缓存组,不过每个缓存组内部可以重设这些属性
  46. chunks: 'async', // 将什么类型的代码用于分割,'initial': 入口代码块 | 'all': 全部 | 'async':按需加载的代码块
  47. minSize: 30000, // 大小超过30kb的模块才会被提取
  48. maxSize: 0, // 只是提示,可以被违反,会尽量将chunk分的比maxSize小,当设为0代表能分则分,分不了不会强制
  49. minChunks: 1, // 莫格模块至少被多少代码块引用,才会被提取成新的chunk
  50. maxAsyncRequests: 6, // 分割后,按需加载的代码块最多允许的并行请求数
  51. maxInitialRequests: 4, // 分割后,入口代码块最多允许的并行请求数
  52. automaticNameDelimiter: '~', // 代码块命名分割符
  53. name: true, // 每个缓存组打包得到的代码块名称
  54. cacheGroups: {
  55. vendors: {
  56. test: /[\\/]node_modules[\\/]/, // 匹配node_modules 中的模块
  57. priority: -10, // 优先级,当模块同时命中多个缓存组的规则时,分配到优先级高的缓存组
  58. },
  59. default: {
  60. minChunks: 2, // 覆盖外层的全局属性
  61. priority: -20,
  62. reuseExsitingChunk: true, // 是否复用已经从原代码块中分割出来的模块
  63. },
  64. },
  65. },
  66. },
  67. }

这些规则一旦被制定,只有全部满足的模块才会被提取,所以需要根据项目情况合理配置才能达到满意的优化结果

Name 属性

name(默认为 true),用来决定缓存组打包得到的 chunk 名称,容易被轻视但作用很大。
有两类取值,boolean 和 string:

  • 值为 true的时候,webpack 会基于代码块和缓存组的 key 自动选择一个名称,这样一个缓存组会打包出多个 chunk
  • 值为false时,适合生产模式使用,webpack 会避免对 chunk 进行不必要的命名,已减少打包体积,除入口 chunk 外,其他 chunk 的名称都由 id 决定,所以最终看到的打包结果是一排数字命名的 js,(这也就是为啥我们看线上网页请求的资源,总会参杂一些 0.js、1.js 之类的文件)
  • 值为 string 时,缓存组最终会打包成一个 chunk,名称就是该 string。当两个缓存组 name 一样的时候,最终会打包在一个 chunk 中。

cacheGroups

每个缓存组根据规则将匹配到的模块分配到代码块(chunk)中,每个缓存组的打包结果可以是单一 chunk,也可以是多个 chunk

  1. cacheGroups: {
  2. vendors: {
  3. test: /[\\/]node_modules[\\/]/,
  4. priority: -10,
  5. filename: 'vendors.js'
  6. },
  7. default: {
  8. priority: -20,
  9. reuseExsitingChunk: true,
  10. filename: common.js
  11. }
  12. }

决定代码分割到哪个 JS 文件里面去,通过 test 进行索引,然后 filename 进行命名

这个缓存组的概念其实就是当你引入不同库,例如 jquery 和 loadsh 时候,他们会根据规则选择打包到哪一个 JS 文件里面,形成一组

  • priority: 代码优先级,如果某一个模块执行 test 后同时符合多个组,那么谁的优先级高,就放到哪个组里面
  • reuseExsitingChunk: true:会判断一个模块是否已经加载到别的组了,如果是那么就会直接复用,不会再次引入

chunk 是什么?

我们把 src 源代码打包分成了两个 JS 文件,这个 JS 文件就叫 Chunk(数据块)

懒加载

其实就是延时加载,即当用到的时候再去

CodeSplitting 异步模块加载就是懒加载的一种体现

优点

  1. 提高页面初始化的加载速度,不需要加载额外 JS 代码
  2. 前端框架的路由概念也是需要居于代码分割和路由切换代码懒加载来实现

Prefetching 和 Preloading

prefetch: 一旦发现核心 JS 文件都加载完以后,会偷偷加载 click.js 文件

区别

  • prefetch 是在核心代码加载完之后带宽空闲的时候再去加载
  • preload 是和核心代码文件一起去加载的

因此,使用 prefetch 的方式加载异步文件更合适一些

MiniCSSExtractPlugin

作用:该插件将CSS提取到单独的文件中。它为每个包含CSS的JS文件(打包之后的JS文件)创建一个CSS文件。它支持CSS和SourceMap的按需加载。

MinCSSExtractPlugin 不能和 style-loader 一起使用

有时候需要将CSS单独打包,作为CDN,或者是页面引入了很多的CSS JS 文件,而JS在最后加载,那么会导致页面在开始的时候没有样式,直到解析到JS,所以将CSS单独打包,单独在页面引入。
使用了这个插件,如果两个JS中引入的CSS文件不同但是代码相同,打包之后只会生成一个CSS文件,并且通过link标签引入CSS文件
如果没有用这个插件,index.html中会使用<style>嵌入式引入样式,即使CSS代码一样也会引入多次

参数

filename

控制打包后的入口JS文件中提取CSS样式生成的CSS文件的名称

chunkFilename

控制打包后的非入口JS文件中提取CSS样式生成的CSS文件的名称

moduleFilename

该参数的值是个函数,主要应用多入口场景,控制从打包后的入口JS文件中提取CSS样式生成的CSS文件名称,如果和filename参数共用,filename将失效

  1. new MiniCssExtractPlugin({
  2. filename: 'css/[name].css',
  3. moduleFilename: ({ name }) => `${name}.css`
  4. })

publicPath

默认是output中设置的path,作用是指定目标文件的定制公共路径