han's bolg - 年糕記

《webpack 实战(入门、进阶与调优)》笔记

《webpack 实战(入门、进阶与调优)》笔记

第 2 章 模块打包

2.3 commonJs 和 es6 module 的区别

  • commonjs、在代码执行阶段加载;es6 module 在代码编译阶段加载

    • 可以用 if 来判断是否加载某个 commonJs
    • es6 module 的导入、导出都是声明式的,必须位于模块的顶层作用域,不能放在 if 语句中
  • comonjs 导入的是一个对象,而 es6 module 支持直接导入变量,减少了引用层级,程序效率更高

1
2
3
4
5
6
// commonjs
module.exports = {
name: 'a'
}
// es6 module
export name = 'a'
  • commonjs 导入的是导出值的一个值拷贝,而 es6 modlue 则是导出值的动态映射,并且这个映射是只读的

以下代码中,count 不能重新赋值

1
2
3
4
5
6
7
8
9
10
11
12
import {count, add} from './calculator.js'

console.log(count, 1)

add(1, 2)

console.log(count, 2)

// 此处会报错,Cannot use import statement outside a module
// count = 3;

console.log(count, 3)

如何处理循环依赖

  1. commonJs

A 依赖 B, B 依赖 C, C 也依赖 B

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// foo.js
const bar = require('./bar.js');
console.log('value of bar:', bar);
module.exports = 'foo.js'

// bar.js
const foo = require('./foo.js');
console.log('value of foo:', foo);
module.exports = 'bar.js'

// index.js
require('./foo.js')

// 输出结果:
value of foo: {}
value of bar: bar.js
  • 解释:
  1. index.js 导入 foo.js,开始执行 foo.js
  2. foo.js 的第一句引入 bar.js,此时 foo.js 不会往下执行了,开始执行 bar.js
  3. 在 bar.js 中又对 foo.js 进行了引用,这里产生了循环。但不会再跳回 foo.js 中执行,而是直接取其导出值。又因为 foo.js 还没执行到导出行 13 行,所以导出值为默认值{}。
  • 从 webpack 的角度来看:
1
2
3
4
5
6
7
8
9
10
11
12
function __webpack_require__(moduleId) {
if(installedModules[moduleId]) {
return installedModules[moduleId].exports
}else {
var module = installedModules[moduleId] = {
id: moduleId,
loaded: false,
exports: {}
}
...
}
}

当 index.js 引用 foo.js 时,相当于执行了webpack_require,初始化了一个 module 对象 installedModules[‘foo.moduleId’],其 exports={},当 bar.js 再次引用 foo.js 时,又执行了webpack_require,此时 installedModules[‘foo.moduleId’]已存在,其 exports 为{}

  1. 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 模块打包原理

  1. webpack 初始化,定义好一些内容,如webpack_require函数、installedModules 对象
  2. 加载入口模块
  3. 执行模块代码,执行到 module.exports 就记录下导出值;遇到 require 函数,就交出执行权,进入webpack_require去加载其他模块
  4. webpack_require中判断当前模块在 installedModules 是否存在,存在则直接返回,不存在则进入 3
  5. 所有模块加载完毕后,又回到入口模块,继续执行到结尾。

第 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
2
3
4
5
6
7
8
9
10
11
module.exports = {
...,
module: {
rules: [
{
test: /.css/,
use: ['style-loader', 'css-loader'] // loader使用顺序从后向前
}
]
}
}
  • 常用 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
module.exports = {
...,
optimization: {
...,
splitChunks: {
// chunks默认为'async',只拆分异步模块。如使用import('lodash')这种形式加载的模块
// all对所有同步、异步模块都拆分。
chunks: 'all',
name: false,
// 默认情况下有两种规则,vendors和default
// vendors提取所有node_modules中符合条件的模块
// default提取被多次引用的模块
cacheGroups: {
react: {
test: /react/,
name: 'react',
priority: 20,
},
},
},
}
}

第 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
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports = {
...,
optimization: {
minimize: true,
minimizer: [
// This is only used in production mode
new TerserPlugin({
terserOptions: {
parallel: true // 默认为false,建议开发
}
})
]
}
}

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
2
3
4
5
6
7
8
9
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
...,
plugin: [
...,
new BundleAnalyzerPlugin(),
]
}
1
2
3
4
5
6
7
8
{
...,
"scripts": {
...,
"build": "node scripts/build.js",
"analyz": "NODE_ENV=production npm_config_report=true yarn build",
}
}
  • unused-webpack-plugin 插件(没研究出用法)
1
2
3
4
5
6
7
8
9
10
11
12
const UnusedWebpackPlugin = require('unused-webpack-plugin');

module.exports = {
...,
plugin: [
...,
new UnusedWebpackPlugin({
directories: [path.join(__dirname, 'src')],
root: __dirname
})
]
}

webpack 打包速度分析(书中没有,参考

  • 安装插件 speed-measure-webpack-plugin

npm install --save-dev speed-measure-webpack-plugin

  • 引入插件、创建插件对象
1
2
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin') //引入插件
const smp = new SpeedMeasurePlugin() //创建插件对象
  • 使用插件的 wrap()方法将配置包起来
1
2
3
4
5
6
7
8
9
10
11
12
module.exports = smp.wrap({
entry: {
index: './src/index.js',
search: './src/search.js',
},
output: {
path: path.join(__dirname, 'dist'), //__dirname(当前模块的目录名) + dist
filename: '[name]_[chunkhash:8].js', //打包后输出的文件名,添加文件指纹 chunkhash
},
plugpins: [],
.....
});

第 8 章 打包优化

打包优化有两个方向:

  1. 打包速度
  2. 打包体积

打包速度:

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 包含两部分:

  1. webpack 的运行环境(具体作用就是模块解析, 加载) ,比如打包后的代码里的webpack_require,这部分代码基本是不会变的。

  2. 包含 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
2
3
4
5
6
7
8
module.exports = {
//...
optimization: {
runtimeChunk: {
name: (entrypoint) => `runtime~${entrypoint.name}`,
},
},
}

第 9 章 开发环境调优

9.2 模块热更新

9.2.2 原理

本地起一个 webpack-dev-server(wds)服务,打包的代码直接存在内存里。浏览器和 node 服务通过 websocket 通信。

当代码更新后,wds 会向浏览器推送更新事件,并带上这次构建的 hash,让浏览器与上一次资源进行对比。浏览器对比完发现 hash 变了,知道源码有更新,会请求 wds 获取更新文件的列表,这个请求的名字为[hash].hot-update.json。