《webpack 实战(入门、进阶与调优)》笔记
第 2 章 模块打包
2.3 commonJs 和 es6 module 的区别
commonjs、在代码执行阶段加载;es6 module 在代码编译阶段加载
- 可以用 if 来判断是否加载某个 commonJs
- es6 module 的导入、导出都是声明式的,必须位于模块的顶层作用域,不能放在 if 语句中
comonjs 导入的是一个对象,而 es6 module 支持直接导入变量,减少了引用层级,程序效率更高
1 | // commonjs |
- commonjs 导入的是导出值的一个值拷贝,而 es6 modlue 则是导出值的动态映射,并且这个映射是只读的
以下代码中,count 不能重新赋值
1 | import {count, add} from './calculator.js' |
如何处理循环依赖
- commonJs
A 依赖 B, B 依赖 C, C 也依赖 B
1 | // foo.js |
- 解释:
- index.js 导入 foo.js,开始执行 foo.js
- foo.js 的第一句引入 bar.js,此时 foo.js 不会往下执行了,开始执行 bar.js
- 在 bar.js 中又对 foo.js 进行了引用,这里产生了循环。但不会再跳回 foo.js 中执行,而是直接取其导出值。又因为 foo.js 还没执行到导出行 13 行,所以导出值为默认值{}。
- 从 webpack 的角度来看:
1 | function __webpack_require__(moduleId) { |
当 index.js 引用 foo.js 时,相当于执行了webpack_require,初始化了一个 module 对象 installedModules[‘foo.moduleId’],其 exports={},当 bar.js 再次引用 foo.js 时,又执行了webpack_require,此时 installedModules[‘foo.moduleId’]已存在,其 exports 为{}
- es6 module
同样,在 es6module 中,也无法获得 foo.js 正确的导出值,只不过 es6module 导出的是 undefined,而不是空对象{}。但因为 es6module import 的是引用,所以他可以更好的支持循环依赖,只不过需要开发者来保证。(解决依赖冲突的方法,可以参考原书 P32)
2.4 加载其他类型模块
2.4.1 非模块化文件
一个放在 script 标签中的文件
1 | <script src="./jquery.js"></script> |
如何在 webpack 中引入 jquery 等非模块化文件呢,直接
1 | import './jquery.js' |
这句代码会直接执行 jquery.js
2.4.4 加载 npm 模块
每个 npm 模块都有一个入口文件,被维护在模块内部 package.json 中的 main 字段。
除了直接加载模块,还可以通过<package_name>/
的形式单独加载 npm 包内部的某个 js 文件,如:
import all from 'lodash/fp/all.js'
,这样子 webpack 最终只会打包 node_module/lodash/fp/all.js,不会打包全部的 lodash 库。
2.5 模块打包原理
- webpack 初始化,定义好一些内容,如webpack_require函数、installedModules 对象
- 加载入口模块
- 执行模块代码,执行到 module.exports 就记录下导出值;遇到 require 函数,就交出执行权,进入webpack_require去加载其他模块
- 在webpack_require中判断当前模块在 installedModules 是否存在,存在则直接返回,不存在则进入 3
- 所有模块加载完毕后,又回到入口模块,继续执行到结尾。
第 3 章 资源输入输出
module、chunk、bundle 的区别
- module,码农开发写的代码模块
- chunk,webpack 打包过程中的分块,通常一个 module 一个 chunk
- bundle,打包完成后的文件,通常一个 chunk 一个 bundle,但如果用 loader\plugin 对代码进行了拆分处理等,一个 chunk 可能对应多个 bundle。比如一个 index 入口文件,会单独拆出一个 index.css 文件。
fielname、chunkfilename
filename,入口文件名
chunkfilename,不在 entry 中,但被打包出来的文件名
- publicpath
第 4 章 预处理器 loader
- webpack 中配置格式
1 | module.exports = { |
常用 loader
- bable-loader
- url-loader,处理图片
第 5 章 样式处理
- css-loader,识别 css 文件
- style-loader,将打包后的 css 内容添加到 header 的 style 标签里
- postcss-loader\less-loader\sass-loader
- stylelint
- css moduels
css-loader 的 options 开启 module:true 即可。
第 6 章 代码分片
6.3 optimization.splitChunks
1 | module.exports = { |
第 7 章 生产环境配置
7.2 开启 production 模式
webpack 配置 mode 为 production,可自动添加很多适用于生产环境的配置项,减少手动的工作。如:
- 把 treeshaking 标记为死代码的代码,去除掉
- 默认开启代码分片
- 自动设置 process.env.NODE_ENV
- 自动开启代码压缩
7.3 环境变量
可通过 DefinePlugin 设置自定义环境变量,通常用于 publicpath 打包地址的区分。
7.4 source-map
7.4.3 安全
既要保证上线后,使用可读性代码(source-map)来追踪 bug,又要保证安全性,不让别人看透你的代码。source-map 提供三种解决方案:
- hidden-source-map
webpack 同样生成完整的 map 文件,但不会再 bundle 文件中添加对 map 文件的引用。可以将 map 文件上传到 sentry 来查看问题。
- nosources-source-map
生成环境的代码仍然有 map,但是不会暴露代码内容。对于错误来说,仍然可以通过控制台来查看源码的错误栈。它对于追自动错误来说基本足够。
- 对.map 文件开放白名单
只允许公司内网查看.map 文件,外网无权限。
7.5 资源压缩
7.5.1 压缩 js
webpack4 推荐 terser-webpack-plugin,不用 uglifyjs 了。
1 | module.exports = { |
7.5.2 压缩 css
使用 mini-css-extract-plugin 提取样式到 css 文件,然后使用 optimize-css-assets-webpack-plugin 进行压缩。
7.6 缓存
7.6.1 资源 hash
参考webpack 中 contenthash 和 chunkhash 的区别
7.6.2 输出动态 html
使用 html-webpack-plugin,可以自动在打包后把 js、css 等静态资源的 hash 名字映射到 html 文件的对应位置。
7.7 bundle 体积监控和分析
vscode 插件 Import Cost
webpack-bundle-analyzer 插件
1 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; |
1 | { |
- unused-webpack-plugin 插件(没研究出用法)
1 | const UnusedWebpackPlugin = require('unused-webpack-plugin'); |
webpack 打包速度分析(书中没有,参考)
- 安装插件 speed-measure-webpack-plugin
npm install --save-dev speed-measure-webpack-plugin
- 引入插件、创建插件对象
1 | const SpeedMeasurePlugin = require('speed-measure-webpack-plugin') //引入插件 |
- 使用插件的 wrap()方法将配置包起来
1 | module.exports = smp.wrap({ |
第 8 章 打包优化
打包优化有两个方向:
- 打包速度
- 打包体积
打包速度:
8.1 happypack(不推荐了,官方推荐用 thread-loader)
8.2 缩小打包作用域
打包体积:
8.3 动态链接库和 DllPlugin
DllPlugin 和 splitChunks 有些类似,都是用来提取公共模块。但本质上有所区别:
- splitChunks,在打包过程中按照一定的规则提取模块
- dllplugin,将 vendor 完全拆出来,有一个单独的 webpack.vendor.config.js 来进行配置,在实际构建的时候完全不对这部分代码处理。
总结来说,打包结果上,二者都会提取公共模块(如 node_module 中代码),但构建速度上,dllplugin 比 splitchunks 要快。
二者选其一使用就好,不要并存。(推荐用 splitchunks)
8.4 tree-shaking
tree-shaking 依赖 es6 module 编译阶段分析能力,可以标记死代码。
tree-shaking 只对 es6 module 生效。有时候对依赖的 npm 包,tree-shaking 并没有生效,原因可能是该库是 commonjs 的方式导出的。虽然我们引用用了 import,但在该 npm 包内部,打包后的代码里,是用的 require。
tree-shaking,只是对死代码做标记,真正去除死代码是通过压缩工具,如 terser-webpack-plugin 或 uglifyplugin。在 webpack4 中,将 mode 设置为 production 也可以达到相同的效果。
runtimeChunk
runtime 包含两部分:
webpack 的运行环境(具体作用就是模块解析, 加载) ,比如打包后的代码里的webpack_require,这部分代码基本是不会变的。
包含 chunks 映射关系的 list。
单独从 app.js 里提取出来,避免映射关系的变化,影响 main.js 文件内容的变化。
因为每一个 chunk 的 id 基本都是基于内容 hash 出来的,所以每次改动都会影响它,如果不将它提取出来的话,等于 app.js 每次都会改变。缓存就失效了。设置 runtimeChunk 之后,webpack 就会生成一个个 runtime~xxx.js 的文件。
然后每次更改所谓的运行时代码文件时,打包构建时 app.js 的 hash 值是不会改变的。如果每次项目更新都会更改 app.js 的 hash 值,那么用户端浏览器每次都需要重新加载变化的 app.js,如果项目大切优化分包没做好的话会导致第一次加载很耗时,导致用户体验变差。现在设置了 runtimeChunk,就解决了这样的问题。所以这样做的目的是避免文件的频繁变更导致浏览器缓存失效,所以其是更好的利用缓存。提升用户体验。
1 | module.exports = { |
第 9 章 开发环境调优
9.2 模块热更新
9.2.2 原理
本地起一个 webpack-dev-server(wds)服务,打包的代码直接存在内存里。浏览器和 node 服务通过 websocket 通信。
当代码更新后,wds 会向浏览器推送更新事件,并带上这次构建的 hash,让浏览器与上一次资源进行对比。浏览器对比完发现 hash 变了,知道源码有更新,会请求 wds 获取更新文件的列表,这个请求的名字为[hash].hot-update.json。