Skip to content

《深入浅出Webpack》精读笔记

写在前面

  • 书籍介绍:《深入浅出Webpack》对Webpack进行了全面讲解,涵盖了Webpack入门、配置、实战、优化、原理等方面的内容。其中,第1章讲解Webpack入门所涉及的知识;第2章详细讲解Webpack提供的常用配置项;第3章结合实际项目中的常见场景进行实践;第4章给出优化Webpack的优秀方案;第5章剖析了Webpack的原理,并讲解如何开发Plugin和Loader;附录汇总了常见的Loader、Plugin和Webpack的其他学习资源。除了深入讲解Webpack,本书还介绍了ES6、TypeScript、PostCSS、Prepack、离线缓存、单页应用、CDN等Web开发相关的技能
  • 我的简评:市面上出版的讲解Webpack的书籍不多,本人觉得是前端打包工具迭代的太快了。Webpack目前还是前端打包工具的首选,值得去学。《深入浅出Webpack》这本书对Webpack讲解的比较详细,总结的比较全面,通俗易懂,还有一个亮点就是每一章节都带有例子。总得来说,还是很不错的一本书。
  • !!福利:文末有pdf书籍、笔记思维导图、随书代码打包下载地址哦

第一章 入门

1.1.前端的发展

  • 模块化是指将一个复杂的系统分解为多个模块以方便编码
    1. CommonJS:CommonJS 是一种被广泛使用的 JavaScript 模块化规范,其核心思想是通过 require 方法来同步加载依赖的其他模块,通过 module.exports 导出需要暴露的接口; CommonJS 规范的流行得益于 Node.js 采用了这种方式;CommonJS 的优点在于:1、 代码可复用于 Node.js 环境下井运行,例如做同构应用;2、 通过 Npm 发布的很多第三方模块都采用了 CommonJS 规范;CommonJS 的缺点在于:这样的代码无法直接运行在浏览器环境下,必须通过工具转换成标准的 ES5
    1. AMD:AMD也是一种 JavaScript模块化规范,与 CommonJS 最大的不同在于,它采用了异步的方式去加载依赖的模块; AMD规范主要用于解决针对浏览器环境的模块化问题,最具代表性的实现是 requirejs ;AMD 的优点在于:1、可在不转换代码的情况下直接在浏览器中运行;2、可异步加载依赖;3、可并行加载多个依赖;4、代码可运行在浏览器环境和 Node.js 环境下;AMD 的缺点在于 JavaScript 运行环境没有原生支持AMD, 需要先导入实现了 AMD 的库后才能正常使用;
    1. ES6 模块化:ES6 模块化是国际标准化组织 ECMA 提出的 JavaScript 模块化规范,它在语言层面上实现了模块化。浏览器厂商和 Node.js都宣布要原生支持该规范 。 它将逐渐取代 CommonJS 和AMD 规范,成为浏览器和服务器通用的模块解决方案 ;ES6 模块的缺点在于目前无法直接运行在大部分 JavaScript运行环境下,必须通过工具转换成标准的 ES5 后才能正常运行;
  • 新语言:ECMAScript 6.0 ( 简称 ES6 )是 JavaScript 语言的下一代标准。它在语言层面为 JavaScript引入了很多新语法和 API,使得 JavaScript 语言可以用来编写复杂的大型应用程序;TypeScript是 JavaScript 的一个超集,由 Microsoft 开发并开源,除了支持 ES6 的所有功能,还提供了静态类型检查;Flow 也是 JavaScript 的一个超集,它的主要特点是为 JavaScript 提供静态类型检查,和 Typescript 相似但更灵活,可以让我们只在需要的地方加上类型检查;SCSS 是一种 css 预处理器,其基本思想是用和 css 相似的编程语言写完后再编译成正常的 css 文件。采用 scss 写 css 的好处在于可以方便地管理代码,抽离公共的部分,通过逻辑写出更灵活的代码;

1.2.常见的构建工具及对比

  • 构建就是源代码转换成可执行的JavaScript、 css、HTML代码。包括如下内容:代码转换、文件优化、代码分割、模块合并、自动刷新、代码校验、自动发布
  • 构建其实是工程化、自动化思想在前端开发中的体现,将一系列流程用代码去实现,让代码自动化地执行这一系列复杂的流程
  • 1、Npm Script:Npm 内置的 一个功能,允许在 package.json 文件里面使用 scripts 字段定义任务;Npm Script 的优点是内置,无须安装其他依赖;其缺点是功能太简单,虽然提供了 pre 和 post 两个钩子,但不能方便地管理多个任务之间的依赖;
  • 2、Grunt:有大量现成的插件封装了常见的任务,也能管理任务之间的依赖关系,自动化地执行依赖的任务 ,每个任务的具体执行代码和依赖关系写在配置文件 Gruntfile.js 里;Grunt 的优点是:1、灵活,它只负责执行我们定义的任务;2、大量的可复用插件封装好了常见的构建任务;Grunt 的缺点是集成度不高,要写很多配置后才可以用,无法做到开箱即用;
  • 3、Gulp:Gulp是一个基于流的自动化构建工具。除了可以管理和执行任务,还支持监听文件、读写文件;Gulp 的最大特点是引入了流的概念,同时提供了一系列常用的插件去处理流,流可以在插件之间传递;Gulp 的优点是好用又不失灵活,既可以单独完成构建,也可以和其他工具搭配使用;缺点和 Grunt 类似,集成度不高,要写很多配置后才可以用,无法做到开箱即用;
  • 4、Fis3:是一个来自百度的优秀国产构建工具;Fis3 的优点是集成了各种 Web 开发所需的构建功能,配置简单、开箱即用;其缺点是目前官方己经不再更新和维护,不支持最新版本的Node.js;Fis3 是一种专注于 Web 开发的完整解决方案;
  • 5、Webpack:Webpack是一个打包模块化 JavaScript 的工具,在 Webpack 里一切文件皆模块,通过 Loader 转换文件,通过 Plugin 注入钩子,最后输出由多个模块组合成的文件;Webpack 专注于构建模块化项目;Webpack 的优点:1、专注于处理模块化的项目,能做到开箱即用、一步到位;2、可通过 Plugin 扩展,完整好用又不失灵活;3、使用场景不局限于 Web 开发;4、社区庞大活跃 , 经常引 入紧跟时代发展的新特性,能为大多数场景找到已有的开源扩展;5、良好的开发体验;Webpack 的缺点是只能用于采用模块化开发的项目;
  • 6、Rollup:Rollup 是一个和 Webpack 很类似但专注于 ES6 的模块打包工具;亮点在于,能针对 ES6 源码进行 Tree Shaking,以去除那些已被定义但没被使用的代码并进行 Scope Hoisting,以减小输出文件的大小和提升运行性能;Rollup 在用于打包 JavaScript 库时比 Webpack 更有优势,因为其打包出来的代码更小、更快;它的功能不够完善,在很多场景下都找不到现成的解决方案;
  • 经过多年的发展, Webpack 已经成为构建工具中的首选

1.4.使用Loader

  • 要支持非 JavaScript 类型的文件,则需要使用 Webpack 的 Loader 机制

1.5.使用Plugin

  • Plugin 是用来扩展 Webpack 功能的,通过在构建流程里注入钩子实现,它为 Webpack 带来了很大的灵活性

1.6.使用DevServer

  • DevServer 会启动一个 HTTP 服务器用于服务网页请求,同时会帮助启动Webpack,并接收 Webpack 发出的文件更变信号,通过 WebSocket 协议自动刷新网页做到实时预览
  • DevServer 会将 Webpack 构建出的文件保存在 内存中,在要访问输出的文件时,必须通过 HTTP 服务访问
  • 如果尝试修改 index.html 文件并保存,则我们会发现这并不会触发以上机制。index.html文件是脱离了 JavaScript 模块化系统的,所以 Webpack 不知道它的存在
  • 除了通过重新刷新整个网页来实现实时预览, DevServer 还有一种被称作模块热替换的刷新技术。模块热替换能做到在不重新加载整个网页的情况下,通过将己更新的模块替换老模块,再重新执行一次来实现实时预览

1.7.核心概念

  • Entry:入口, Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入
  • Module:模块,在 Webpack 里一切皆模块,一个模块对应一个文件
  • Chunk:代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割
  • Loader:模块转换器,用于将模块的原内容按照需求转换成新内容
  • Plugin:扩展插件,在 Webpack 构建流程中的特定时机注入扩展逻辑,来改变构建结果或做我们想要的事情
  • Output:输出结果,在 Webpack 经过一系列处理并得出最终想要的代码后输出结果

第二章 配置

2.1.Entry

  • 配置模块的入口
  • 可抽象成输入, Webpack 执行构建的第一步将从入口开始, 搜寻及递归解析出所有入口依赖的模块
  • entry配置是必填的,若不填则将导致 Webpack 报错、退出

2.2.Output

  • 配置如何输出最终想要的代码
  • output 是一个 object ,里面包含一系列配置项
  • 注意, ExtractTextWebpackPlugin插件使用 contenthash 而不是 chunkhash 来代表哈希值,原因在于 ExtractTextWebpackPlugin 提取出来的内容是代码内容本身,而不是由一组模块组成的 Chunk

2.3.Module

  • 配置处理模块的规则

2.4.Resolve

  • 配置寻找模块的规则
  • Webpack 在启动后会从配置的入口模块出发找出所有依赖的模块, Resolve 配置 Webpack如何寻找模块所对应的文件

2.5.Plugin

  • 配置扩展插件
  • plugins 配置项接收一个数组,数组里的每一项都是一个要使用的 Plugin 的实例, Plugin 需要的参数通过构造函数传入

2.10.可用如下经验去判断如何配置 Webpack:

  • 若想让源文件加入构建流程中被 Webpack 控制,则配置 entry;
  • 若想自定义输出文件的位置和名称 ,则配置 output;
  • 若想自定义寻找依赖模块时 的 策略 ,则配置 resolve;
  • 若想自定义解析和转换文件的策略, 则配置 module,通常是配置 module.rules 里的 Loader;
  • 若其他大部分需求可能通过 Plugin 去实现,则配置 plugin ;

第三章 实战

  • 3.1.使用ES6语言
  • 3.2.使用Typescript
  • 3.3.使用Flow检查器
  • 3.4.使用SCSS语言
  • 3.5.使用PostCSS
  • 3.6.使用React框架
  • 3.7.使用Vue框架
  • 3.8.使用Angular2框架
  • 3.9.为单页应用生成HTML
  • 3.10.管理多个单页应用
  • 3.11.构建同构应用
  • 3.12.构建Electron应用
  • 3.13.构建Npm模块
  • 3.14.构建离线应用
  • 3.15.搭配Npm Script
  • 3.16.检查代码
  • 3.17.通过Node.js API启动Webpack
  • 3.18.使用Webpack Dev Moddleware
  • 3.19.加载图片
  • 3.20.加载SVG
  • 3.21.加载Source Map

第四章 优化

4.1.缩小文件的搜索范围

  • Webpack 在启动后会从配置的 Entry 出发,解析出文件中的导入语句,再递归解析
  • 在遇到导入语句时, Webpack 会做以下两件事:根据导入语句去寻找对应的要导入的文件;根据找到的要导入的文件的后缀,使用配置中的 Loader 去处理文件;
  • 可以通过 test 、 include 、 exclude 三个配置项来命中 Loader 要应用规则的文件。为了尽可能少地让文件被 Loader 处理,可以通过include 去命中只有哪些文件需要被处理
  • resolve.mainFields 用于配置采用哪个字段作为入口文件的描述
  • 优化 resolve.extensions 配置:后缀尝试列表要尽可能小,不要将项目中不可能存在的情况写到后缀尝试列表中;频率出现最高的文件后缀要优先放在最前面,以做到尽快退出寻找过程; 在源码中写导入语句时,要尽可能带上后缀 , 从而可以避免寻找过程
  • module.noParse 配置项可以让 Webpack 忽略对部分没采用模块化的文件的递归解析处理,这样做的好处是能提高构建性能

4.2.使用DllPlugin

  • 为什么为 Web 项目构建接入动态链接库的思想后,会大大提升构建速度呢?原因在于,包含大量复用模块的动态链接库只需被编译一次,在之后的构建过程中被动态链接库包含的模块将不会重新编译,而是直接使用动态链接库中的代码
  • Webpack 己经内置了对动态链接库的支持,需要通过以下两个内置的插件接入:DllPlugin 插件: 用于打包出一个个单独的动态链接库文件;DllReferencePlugin 插件:用于在主要的配置文件中引入 DllP!ugin 插件打包好的动态链接库文件;

4.3.使用HappyPack

  • 它将任务分解给多个子进程去并发执行,子进程处理完后再将结果发送给主进程
  • 在整个 Webpack 构建流程中,最耗时的流程可能就是 Loader 对文件的转换操作了,因为要转换的文件数据量巨大,而且这些转换操作都只能一个一个地处理
  • HappyPack 的核心原理就是将这部分任务分解到多个进程中去并行处理,从而减少总的构建时间

4.4.使用ParallelUglifyPlugin

  • 由于压缩 JavaScript 代码时,需要先将代码解析成用 Object 抽象表示的 AST 语法树,再去应用各种规则分析和处理AST,所以导致这个过程的计算量巨大, 耗时非常多
  • UglifyES 一般用于为比较新的 JavaScript 运行环境压缩代码,例如用于 ReactNative 的代码运行在兼容性较好的 JavaScriptCore 引擎中,为了得到更好的性能和尺寸,可采用 UglifyES 压缩

4.5.使用自动刷新

  • Webpack 官方提供了两大模块,一个是核心的 webpack ,一个是在1.6节中提到的 webpack-dev-server。而文件监听功能是 Webpack 提供的
  • 当发现某个文件发生了变化时,并不会立刻告诉监昕者,而是先缓存起来,收集一段时间的变化后,再一次性告诉监听者。配置项中的 watchOptions.aggregateTimeout 用于配置这个等待时间
  • 在开启监听模式时,默认情况下会监听配置的 Entry 文件和所有 Entry 递归依赖的文件。在这些文件中会有很多存在于 node_modules 下,而一个很大的优化点就是忽略 node modules 下的文件,不监昕它们
  • 监听到文件更新后的下一步是刷新浏览器, webpack 模块负责监听文件, webpack-devserver 模块则负责刷新浏览器。 webpack 模块会在文件发生变化时通知 webpack-dev-server模块
  • 控制浏览器刷新有如下三种方法:借助浏览器扩展去通过浏览器提供的接口刷新, WebStorm IDE 的 LiveEdit 功能就是这样实现的;向要开发的网页中注入代理客户端代码,通过代理客户端去刷新整个页面;将要开发的网页装进一个 iframe 中,通过刷新 iframe 去看到最新效果;

4.6.开启模块热替换

  • DevServer 还支持一种叫做模块热替换( Hot Module Replacement)的技术可在不刷新整个网页的情况下做到超灵敏实时预览
  • 原理是在一个源码发生变化时,只需重新编译发生变化的模块,再用新输出的模块替换掉浏览器中对应的老模块
  • 模块热替换的原理和自动刷新的原理类似,都需要向要开发的网页中注入一个代理客户端来连接 DevServer 和网页,不同在于模块热替换的独特的模块替换机制
  • 请不要将模块热替换技术用于线上环境,它是专门为提升开发效率而生的

4.7.区分环境

  • 为了尽可能复用代码, 在构建的过程中需要根据目标代码要运行的环境输出不同的代码,我们需要一套机制在源码中区分环境

4.8.压缩代码

  • 浏览器通过服务器访问网页时获取的 JavaScript、 css 资源都是文本形式的,文件越大,网页加载的时间越长
  • 目前最成熟的 JavaScript 代码压缩工具是 UglifyJS,它会分析 JavaScript 代码语法树,理解代码的含义,从而做到去掉无效代码、去掉日 志输出代码、缩短变量名等优化
  • 为了压缩 ES6 代码,需要使用专门针对 ES6 代码的 UglifyES
  • 目前比较成熟、可靠的 css 压缩工具是 cssnano ,基于 PostCSS

4.9.CDN加速

  • 虽然在前面通过压缩代码的手段减小了网络传输的大小,但实际上最影响用户体验的还是网页首次打开时的加载等待,其根本原因是网络传输过程耗时较大
  • CDN 其实是通过优化物理链路层传输过程 中的光速有限、丢包等问题来提升网速的
  • 最核心的部分是通过 publicPath 参数设置存放静态资源的 CDN 目录 URL

4.10.使用Tree Shaking

  • Tree Shaking 可以用来剔除 JavaScript 中用不上的死代码。它依赖静态的 ES6 模块化语法,例如通过 import 和 export 导入、导出
  • 在项目中使用大量的第三方库时,我们会发现 Tree Shaking 似乎不生效了,原因是大部分 Npm 中的代码都采用了 CommonJS 语法,这导致 Tree Shaking 无法正常工作而降级处理

4.11.提取公共代码

  • 在用户第一次访问后,这些页面的公共代码的文件己经被浏览器缓存起来, 在用户切换到其他页面时,就不会再重新加载存放公共代码的文件,而是直接从缓存中获取
  • Webpack 内置了专门用于提取多个 Chunk 中的公共部分的插件 CommonsChunkPlugin

4.12.分割代码以按需加载

  • 被分割出去的代码的加载需要一定的时机去触发 ,即当用户操作到了或者即将操作到对应的功能时再去加载对应的代码
  • Webpack 内置了强大的分割代码的功能去实现按需加载,实现起来非常简单

4.13.使用Prepack

  • Prepack 由 Facebook 开源,采用了较为激进的方法:在保持运行结果 一致的情况下,改变源代码的运行逻辑,输出性能更好的 JavaScript 代码。实际上,Prepack 就是一个部分求值器,编译代码时提前将计算结果放到编译后的代码中, 而不是在代码运行时才去求值
  • Prepack 的工作原理和流程大致如下:通过 Babel 将 JavaScript 源码解析成抽象语法树(AST),以更细粒度地分析源码:Prepack 实现了 一个 JavaScript 解释器,用于执行源码。借助这个解释器, Prepack才能理解源码具体是如何执行的,并将执行过程中的结果返回到输出中
  • Prepack 目前还处于初期开发阶段,局限性也很大

4.14.开启Scope Hoisting

  • Scope Hoisting 可以让 Webpack 打包出来的代码文件更小、运行更快,它又被译作“作用域提升”,是在 Webpack 3 中新推出的功能
  • Scope Hoisting 的实现原理其实很简单:分析模块之间的依赖关系,尽可能将被打散的模块合并到一个函数中,但前提是不能造成代码元余。 因此只有那些被引用了 一次的模块才能被合并
  • 由于 Scope Hoisting 需要分析模块之间的依赖关系,因此源码必须采用 ES6 模块化语句,不然它将无法生效

4.15.输出分析

  • 最直接的分析方法是阅读 Webpack 输出的代码,但 Webpack 输出的代码可读性非常差而且文件非常大,让我们非常头疼
  • 在启动 Webpack 时带上以上两个参数,启动命令如下: webpack --profile --json > stats.json 我们会发现项目中多出了一个 stats.json 文件

第五章 原理

5.1.工作原理概括

  • Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:初始化参数、开始编译、确定入口、编译模块、完成模块编译、输出资源、输出完成

5.2.输出文件分析

  • 使用 CommonsChunkPJugin 提取公共代码时输出的文件和使用异步加载时输出的文件是一样的,都会有 webpack require 和 webpackJsonp。 原因在于提取公共代码和异步加载在本质上都是代码分割

5.3.编写Loader

  • Loader 就像一个翻译员,能将源文件经过转化后输出新的结果,并且一个文件还可以链式地经过多个翻译员翻译
  • 一个 Loader 的职责是单一的,只需要完成一种转换。如果一个源文件需要经历多步转换才能正常使用,就通过多个 Loader 去转换 。 在调用多个 Loader去转换一个文件时,每个 Loader 都会链式地顺序执行

5.4.编写Plugin

  • 在 Webpack 运行的生命周期中会广播许多事件, Plugin 可以监昕这些事件,在合适的时机通过 Webpack 提供的API改变输出结果

5.5.调试Webpack

  • 由于 Webpack 运行在 T、fode. 上,所以调试 Webpack 就相当于调试 Node.js 程序

写在后面