Webpack 学习
1. 简介
1.1. Webpack 是什么?
官方解释:本质上,webpack 是一个用于现代 JavaScript 应用程序的静态模块打包工具。当 webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个 依赖图(dependency graph),然后将你项目中所需的每一个模块组合成一个或多个 bundles,它们均为静态资源,用于展示你的内容。
Webpack 是一个模块打包工具(module bundler),因为平常多用来对前端工程打包,所以也是一个前端构建工具。其最主要的功能就是模块打包。
模块打包,通俗地说就是:找出模块之间的依赖关系,按照一定的规则把这些模块组织合并为一个 JS 文件。
Webpack 认为一切都是模块,JS 文件、CSS 文件、jpg/png 图片等等都是模块。Webpack 会把所有的这些模块都合并为一个 JS 文件,这是它最本质的工作。当然,我们可能并不想要它把这些合并成一个 JS 文件,这个时候我们可以通过一些规则或工具来改变它。
1.2. webpack 的作用
- 模块打包。可以将不同模块的文件打包整合在一起,并且保证它们之间的引用正确,执行有序。利用打包我们就可以在开发的时候根据我们自己的业务自由划分文件模块,保证项目结构的清晰和可读性。
- 编译兼容。在前端的“上古时期”,手写一堆浏览器兼容代码一直是令前端工程师头皮发麻的事情,而在今天这个问题被大大的弱化了,通过 webpack 的 Loader 机制,不仅仅可以帮助我们对代码做 polyfill,还可以编译转换诸如 .less, .vue, .jsx 这类在浏览器无法识别的格式文件,让我们在开发的时候可以使用新特性和新语法做开发,提高开发效率。
- 能力扩展。通过 webpack 的 Plugin 机制,我们在实现模块化打包和编译兼容的基础上,可以进一步实现诸如按需加载,代码压缩等一系列功能,帮助我们进一步提高自动化程度,工程效率以及打包输出的质量。
2. 模块打包的运行原理
2.1. 打包流程
- 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。
- 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件(Plugin),执行对象的 run 方法开始编译。
- 确定入口:根据配置中的 entry 找出所有的入口文件。
- 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,然后递归直到所有入口依赖的文件都经过了这一步的处理。
- 完成模块编译:在经过 Loader 翻译完所有模块后,得到每个模块被翻译后的最终内容以及依赖关系。
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表。
- 输出完成:在确定到输出内容后,根据配置确定输出的路径和文件名,将文件内容写入文件系统。
在以上过程中,Webpack 会在特定的时间点 广播 出特定的事件,插件 在 监听 到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。
总结来讲分为三步
- 初始化: 1-2 启动构建,读取合并配置参数,加载 Plugin ,实例化 Compiler。
- 编译:3-5 从 Entry 出发,针对每个 Module 串行调用对应的 Loader 去翻译文件的内容,再找到该模块依赖的模块,递归处理。
- 输出:6-7 将编译后的模块组合成 Chunk,将 Chunk 转换成文件,输出到文件系统中。
Webpack 实际上为每个模块创造了一个可以导出和导入的环境,本质上并没有修改代码的执行逻辑,代码执行顺序与模块加载顺序也完全一致。
3. Source Map
source Map 是一项将编译、打包、压缩后的代码映射回源代码的技术,由于打包压缩后的代码并没有阅读性可言,一旦在开发中报错或者遇到问题,直接在混淆代码中 debug 问题会带来非常糟糕的体验,sourceMap 可以帮助我们快速定位到源代码的位置,提高我们的开发效率。
.map 文件结构大致如下
{
"version" : 3, // Source Map版本
"file": "out.js", // 输出文件(可选)
"sourceRoot": "", // 源文件根目录(可选)
"sources": ["foo.js", "bar.js"], // 源文件列表
"sourcesContent": [null, null], // 源内容列表(可选,和源文件列表顺序一致)
"names": ["src", "maps", "are", "fun"], // mappings使用的符号名称列表
"mappings": "A,AAAB;;ABCDE;" // 带有编码映射数据的字符串
}
4. Webpack 的热更新原理
Webpack 的热更新又称热替换(Hot Module Replacement),缩写为 HMR。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。
HMR 的核心就是客户端从服务端拉更新后的文件,准确的说是 chunk diff (chunk 需要更新的部分),实际上 WDS 与浏览器之间维护了一个 Websocket,当本地资源发生变化时,WDS 会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比。客户端对比出差异后会向 WDS 发起 Ajax 请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 WDS 发起 jsonp 请求获取该 chunk 的增量更新。
后续的部分(拿到增量更新之后如何处理?哪些状态该保留?哪些又需要更新?)由 HotModulePlugin 来完成,提供了相关 API 以供开发者针对自身场景进行处理,像 react-hot-loader 和 vue-loader 都是借助这些 API 实现 HMR。
webpack 配合 webpack-dev-server 进行应用开发的模块热更新流程图。
- 第一步,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中。
- 第二步是 webpack-dev-server 和 webpack 之间的接口交互,而在这一步,主要是 dev-server 的中间件 webpack-dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调用 webpack 暴露的 API 对代码变化进行监控,并且告诉 webpack,将代码打包到内存中。
- 第三步是 webpack-dev-server 对文件变化的一个监控,这一步不同于第一步,并不是监控代码变化重新打包。当我们在配置文件中配置了 devServer.watchContentBase 为 true 的时候,Server 会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload。注意,这儿是浏览器刷新,和 HMR 是两个概念。
- 第四步也是 webpack-dev-server 代码的工作,该步骤主要是通过 sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也包括第三步中 Server 监听静态文件变化的信息。浏览器端根据这些 socket 消息进行不同的操作。当然服务端传递的最主要信息还是新模块的 hash 值,后面的步骤根据这一 hash 值来进行模块热替换。
- webpack-dev-server/client 端并不能够请求更新的代码,也不会执行热更模块操作,而把这些工作又交回给了 webpack,webpack/hot/dev-server 的工作就是根据 webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,也就没有后面那些步骤了。
- HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接收到上一步传递给他的新模块的 hash 值,它通过 JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端返回一个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块代码。这就是上图中 7、8、9 步骤。
- 而第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,HotModulePlugin 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。
- 当 HMR 失败后,回退到 live reload 操作,也就是进行浏览器刷新来获取最新打包代码。
5. 对 bundle 体积进行监控和分析
VSCode 中有一个插件 Import Cost 可以帮助我们对引入模块的大小进行实时监测,还可以使用 webpack-bundle-analyzer 生成 bundle 的模块组成图,显示所占体积。
bundlesize 工具包可以进行自动化资源体积监控。
6. 文件指纹
文件指纹是打包后输出的文件名的后缀。
- Hash:和整个项目的构建相关,只要项目文件有修改,整个项目构建的 hash 值就会更改
- Chunkhash:和 Webpack 打包的 chunk 有关,不同的 entry 会生出不同的 chunkhash
- Contenthash:根据文件内容来定义 hash,文件内容不变,则 contenthash 不变
7. 优化 Webpack 的构建速度
- 使用高版本的 webpack 和 node.js
- 多进程/多实例构建(thread-loader)
- 压缩代码
- 图片压缩(image-webpack-loader)
- 提取页面公共资源
- DLL 使用 dll plugin 进行分包,让一些基本不会改动的代码先打包成静态资源。
- 增加缓存策略(babel-loader 开启缓存, terser-webpack-plugin 开启缓存,cache-loader)
- 优化 Loader 对于 loader 来说,影响最大的的 Babel。Babel 会将代码转为字符串生成 AST,然后对 AST 继续进行转变最后生成新代码,项目越大,代码转换越多。可以通过指定 Loader 的文件搜索范围,并且添加缓存的方式来优化。
- 可以通过 HappyPack 插件来将 Loader 的同步执行转换为并行,提高打包速度。
compiler 拓展在 webpack 源码中主要依赖于 compiler 和 compilation 两个核心对象实现。
- compiler 对象是一个全局单例,他负责把控整个 webpack 打包的构建流程。
- compilation 对象是每一次构建的上下文对象,它包含了当次构建所需要的所有信息,每次热更新和重新构建,compiler 都会重新生成一个新的 compilation 对象,负责此次更新的构建过程。
- 每个模块间的依赖关系,则依赖于 AST 语法树。每个模块文件在通过 Loader 解析完成之后,会通过 acorn 库生成模块代码的 AST 语法树,通过语法树就可以分析这个模块是否还有依赖的模块,进而继续循环执行下一个模块的编译解析。
- 最终 Webpack 打包出来的 bundle 文件是一个 IIFE 的执行函数。
loader 拓展常见的 Loader
- raw-loader: 加载文件原始内容(utf-8)
- file-loader: 把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件(处理图片和字体)
- url-loader: 和 file-loader 类似,不同点是用户可以设置一个阈值,大于阈值会交给 file-loader 处理,小于阈值时会处理返回文件 base64 形式编码(处理图片和字体)
- source-map-loader: 加载额外的 Source Map 文件,方便断点调试。
- svg-inline-loader: 将压缩后的 SVG 内容注入代码中。
- image-loader: 加载并压缩图片文件。
- json-loader: 加载 JSON 文件(默认包含)
- handlebars-loader: 将 Handlebars 模块编译成函数并返回。
- babel-loader: 把 ES6 转换成 ES5。
- ts-loader: 将 TypeScript 转换成 JavaScript。
- awesome-typescript-loader: 同上,但是新能优于 ts-loader。
- sass-loader: 将 SCSS/SASS 代码转换成 CSS。
- css-loader: 加载 CSS,支持模块化、压缩、文件导入等特性。
- style-loader: 把 CSS 代码注入到 JS 中,通过 DOM 操作加载 CSS。
- postcss-loader: 拓展 CSS 语法,使用下一代 CSS,可以配合 autoperfixer 插件自动补齐 CSS3 前缀。
- eslint-loader: 通过 ESLint 检查 JS 代码。
- tslint-loader: 通过 TSLint 检查 TS 代码。
- mocha-loader: 加载 Mocha 测试用例的代码。
- coverjs-loader: 计算测试的覆盖率。
- ver-loader: 加载 Vue.js 单文件组件。
- i18n-loader: 用于国际化。
- cache-loader: 可以在一些性能开销大的 loader 之前添加,目的是将结果缓存到磁盘里。
plugin 拓展用过的 plugin
- webpack-dashboard:可以更友好的展示相关打包信息。
- webpack-merge:提取公共配置,减少重复配置代码。
- speed-measure-webpack-plugin:简称 SMP,分析 webpack 打包过程中 loader 和 plugin 的耗时,有助于找到构建过程的性能瓶颈。
- size-plugin:监控资源体积变化,今早发现问题。
- hot-module-replacement-plugin:模块热替换