文章目录
第1章 Webpack简介
1.1 何为Webpack
Webpack是一个开源的JavaScript模块打包工具,其最核心的功能是解决模块之间的依赖,把各个模块按照特定的规则和顺序组织在一起,最终合并为一个JS文件。
1.2 为什么需要Webpack
1.2.1 何为模块
按照特定的功能将其拆分为多个代码段,每个代码段实现一个特定的目的。
1.2.2 JavaScript中的模块
通过导入和导出语句我们可以清晰的看到模块间的依赖关系。
模块可以借助工具来进行打包,在页面中只需要加载合并后的资源文件,减少了网络开销。
多个模块之间的作用域是隔离的,彼此不会有命名冲突。
1.2.3 模块打包工具
模块打包工具的任务就是解决模块间的依赖,使其打包后的结果能运行在浏览器上。他的工作方式主要分为两种:
- 将存在依赖关系的模块按照特定规则合并为单个JS文件,一次全部加载进页面中。
- 在页面初始时加载一个入口模块,其他模块异步的进行加载。
1.2.4 为什么选择Webpack
Webpack具备以下几点优势:
- Webpack默认支持多种模块标准。包括AMD、CommonJS,以及最新的ES6模块。
- Webpack有完备的代码分割(code splitting)解决方案。它可以分割打包后的资源,首屏只加载必要的部分,不太重要的功能放到后面动态的加载。
- Webpack可以处理各种类型的资源。Webpack还可以处理样式、模板,甚至图片等。
- Webpack拥有庞大的社区支持。
1.3 安装
npm i webpack webpack-cli -S
webpack是核心模块,webpack-cli是命令行工具。
1.4 打包第一个应用
打包模式mode分为:development、production、none三种模式。
使用script添加脚本命令。
webpack-dev-server的两大职能:
- 令Webpack进行模块打包,并处理打包结果的资源请求。
- 作为普通的Web Server,处理静态资源文件请求。
webpack-dev-server只是将打包结果放在内存中,并不会写入实际的bundle.js。它还有一项很便捷的特性就是live-reloading(自动刷新)。
第2章 模块打包
2.1 CommonJS
CommonJS是由JavaScript社区于2009年提出的包含模块、文件、IO、控制台在内的一系列标准。
2.1.1 模块
CommonJS规定每个文件是一个模块。
每个模块拥有各自的作用域。
2.1.2 导出
module.export
和export
导出模块。
建议将module.export
和export
放在模块的末尾
2.1.3 导入
require()
当我们require一个模块时会有两种情况:
- require的模块是第一次被加载。这时会首先执行该模块,然后导出内容。
- require的模块曾被加载过。这时该模块的代码不会再次执行,而是直接导出上次执行后得到的结果。
2.2 ES6 Module
2.2.1 模块
ES6 Module将每个文件作为一个模块。ES6 Module会自动采用严格模式。
2.2.2 导出
- 命名导出。
export const xx
- 默认导出。
export default
2.2.3 导入
import
和import xx from
2.2.4 复合写法
`export { name, add } from ‘./xx.js’
把某一模块导入之后立即导出。
2.3 CommonJS和ES6 Module的区别
2.3.1 动态与静态
CommonJS和ES6 Module的本质区别在于CommonJS对模块依赖关系的建立发生在代码运行阶段,而ES6 Module的模块依赖关系的建立发生在代码编译阶段。
ES6 Module相比CommonJS具备的几点优势:
- 死代码检测和排除。检测哪些模块没有被调用过。
- 模块变量类型检查。
- 编译器优化。
2.3.2 值拷贝与动态映射
在导入一个模块时,对于CommonJS来说获取的是一份导出值的拷贝;而在ES6 Module中则是值的动态映射,并且这个映射是只读的。
// Commonjs
// calculator.js
let count = 0
module.exports = {
count,
add(a, b) {
count += 1
return a + b
}
}
// index.js
let count = require('./calculator.js').count
const add = require('./calculator.js').add
console.log(count) // 0
add(2, 3)
console.log(count) // 0
count++
console.log(count) // 1
// ES6 Module
// calculator.js
let count = 0
const add = (a, b) => {
count += 1
return a + b
}
export = {
count,
add
}
// index.js
import { count, add } from './calculator.js'
console.log(count) // 0
add(2, 3)
console.log(count) // 1
count++ // 不可更改,会抛出SyntaxError: 'count' is read-only
2.3.3 循环依赖
尽量避免循环依赖的产生。ES6 Module的特性使其更好地支持循环依赖。
2.4 加载其他类型模块
2.4.1 非模块化文件
通过import直接引入即可
2.4.2 AMD
AMD是Asynchronous Module Definition(异步模块定义)。它加载模块的方式是异步的。
2.4.3 UMD
UMD的全称是Universal Module Definition,通用模块标准。它的目标是使一个模块能运行在各种环境下。
2.4.4 加载npm模块
npm和yarn两者的仓库是共通的。
npm init -y
初始化
2.5 模块打包原理
打包结果(bundle)可以分为以下几个部分:
- 最外层立即执行匿名函数。用来包裹整个bundle,并构成自身的作用域。
- installedModules对象。每个模块只在第一次被加载的时候执行,之后其导出值就被存储到这个对象里面。
__webpack_require__
函数。对模块加载的实现。- modules对象。工程中所有产生了依赖关系的模块都会以key-value的形式放在这里。key可以理解为一个模块的id,由数字或者一个很短的hash字符串构成;value则是由一个匿名函数包裹的模块实体,匿名函数的参数则赋予了每个模块导出和导入的能力。
bundle如何在浏览器中执行的
- 在最外层的匿名函数中会初始化浏览器执行环境,包括定义installedModules对象、
__webpack_require__
函数等,为模块的夹杂和执行做一些准备工作。 - 加载入口模块。每个bundle都有且只有一个入口模块,在上面的示例中,index.js是入口模块,在浏览器中会从它开始执行。
- 执行模块代码。如果执行到了module.exports则记录下模块的导出值;如果中间遇到require函数(准确的说是
__webpack_require__
),则会暂时交出执行权,进入__webpack_require__
函数体内进行加载其他模块的逻辑。 - 在
__webpack_require__
中会判断即将加载的模块是否存在于installedModules中。如果存在则直接取值,否则回到第3步,执行该模块的代码来获取导出值。 - 所有依赖的模块都已执行完毕,最后执行权又回到入口模块。当入口模块的代码执行到结尾,也就意味着整个bundle运行结束。
第3章 资源输入输出
3.1 资源处理流程
chunk的字面的意思是代码块,在Webpack中可以理解成被抽象和包装过后的一些模块。
3.2 配置资源入口
在配置入口时,实际上做了两件事:
- 确定入口模块位置,告诉Webpack从哪里开始进行打包。
- 定义chunk name。如果工程只有一个入口,name默认其chunk name为“main”;如果工程有多个入口,我们需要为每个入口定义chunk name,来作为该chunk的唯一标识。
3.2.1 context
context可以理解为资源入口的路径前缀,在配置时要求必须使用绝对路径的形式。
3.2.2 entry
entry的配置有多种形式:字符串、数组、对象、函数。
3.2.3 实例
1.单页应用
对于单页应用(SPA)来说,一般定义单一入口即可。
2.提取vendor
vendor的意思是“供应商”,在Webpack中vendor一般指的是工程所使用的库、框架等第三方模块集合打包而产生的bundle。
3.多页应用
提供多个入口,形成多个bundle。
3.3 配置资源出口
3.3.1 filename
filename的作用是控制输出资源的文件名,字符串。
Webpack支持使用一种类似模板语言的形式动态地生成文件名。output: { filename: '[name].js' }
filename配置项模板变量
变量名称 | 功能描述 |
---|---|
[chunkhash] | 指代当前chunk内容的hash |
[hash] | 指代Webpack此次打包所有资源生成的hash |
[id] | 指代当前chunk的id |
[query] | 指代filenmae配置项中的query |
变量的两个作用:
-
对不同的chunk进行区分。
-
控制客户端缓存。
更新缓存一般只用在生产环境的配置下。
3.3.2 path
path可以指定资源输出的位置,要求值必须为绝对路径。
3.3.3 publicPath
publicPath用来指定资源的请求位置。
- 输出位置:打包完成后资源产生的目录,一般将其指定为工程中的dist目录。
- 请求位置:由JS或CSS所请求的间接资源路径。页面中的资源分为两种,一种是由HTML页面直接请求,比如通过script标签加载的JS;另一种是由JS或CSS请求的,如异步加载的JS、从CSS请求的图片字体等。publicPath的作用就是制定这部分间接资源的请求位置。
publicPath有3中形式:
-
HTML相关。
-
Host相关。
若值为“/”开始,则代表是以当前页面的host name为基础路径。
-
CDN相关
当publicPath以协议头或相对协议的形成开始时,代表当前路径是CDN相关。
devServer的publicPath指的是静态资源服务路径。
3.3.4 实例
1.单入口
对于单入口的场景,通常不必设置动态地output.filename,直接指定输出的文件名即可。
2.多入口
在多入口的场景下,必然会需要模板变量来配置filename。
第4章 预处理器
预处理器(loader),它赋予了Webpack可处理不同资源类型的能力。
4.1 一切皆模块
对webpack来说,所有静态资源都模块。Webpack维护模块间的关系可以使工程结构更加直观,代码的可维护性更强。
4.2 loader概述
每个loader本质上都是一个函数。在Webpack4之后,loader同时支持字符串和抽象语法书(AST)的传递。
用公式表达loader的本质:output = loader(input)
。
这里的input可能是工程源文件的字符串,也可能是上一个loader转化后的结果,包括转化后的结果(也是字符串类型)、source map,以及AST对象;
output同样包含转化后的文件字符串、source map,以及AST。
如果这是最后一个loader,结果将直接被送到Webpack进行后续处理,否则将作为下一个loader的输入向后传递。
loader本身就是一个函数,在该函数中对接收到的内容进行转换,然后返回转换后的结果。
4.3 loader的配置
loader的字面意思是装载器,在Webpack中它的实际功能则更像是预处理器。Webpack本身只认识JavaScript,对于其他类型的资源必须预先定义一个或多个loader对其进行转译,输出为Webpack能够接收的形式再继续进行。
4.3.1 loader的引入
loader都是一些第三方npm模块,Webpack本身并不包含任何loader。
与loader相关的配置都在module对象中,其中module.rules
代表了模块的处理规则。每条规则内部可以包含很多配置项,这里介绍最重要的两项——test和use。
- test可接收一个正则表达式或者一个元素为正则表达式的数组,只有正则匹配上的模块才会使用这条规则。
- use可接收一个数组,数组包含该规则所使用的loader。
4.3.2 链式loader
use: ['style-loader', 'css-loader']
。
Webpack打包时是按照数组从后往前的顺序将资源交给loader处理的。
4.3.3 loader options
在引入loader的时候可以通过options将它们传入。
4.3.4 更多配置
1.exclude与include
exclude和include是用来排除或包含指定目录下的模块,可接收正则表达式或者字符串,以及由它们组成的数组。
exclude: /node_modules/
。include: /src/
。该配置通常是比较的,否则可能拖慢整体的打包速度。
exclude和include同时存在时,exclude的优先级更高。
2.resource与issuer
resource与issuer可用于更加精确地确定模块规则的作用范围。
在Webpack中,被加载者是resource,加载者是issuer。test、exclude、include本质上属于对resourece也就是被加载者的配置。
rules: [
{
use: ['style-loader', 'css-loader'],
resource: {
test: /\.css$/,
exclude: /node-modules/
}
issuer: {
test: /\.js$/,
include: /src/
}
}
]
3.enforce
enforce用来指定一个loader的种类,只接受“pre”或“post”两个字符串类型的值。
Webpack中的loader按照执行顺序可分为pre、inline、normal、post四种类型。inline官方已推荐不使用。
{
test: /\.js$/,
enforce: 'pre',
use: 'eslint-loader'
}
“pre”表示将在所有正常loader之前执行。需要在所有loader之后执行的可以使用“post”。
我们也可以不适用enforce而只要保证loader顺序是正确的即可。
4.4 常用loader介绍
4.4.1 babel-loader
babel-loader用来处理ES6+并将其编译为ES5。
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true,
presets: [[
'env', {
modules: false
}
]]
}
}
}
4.4.2 ts-loader
ts-loader用于连接Webpack与Typescript的模块。
{
test: /\.ts$/,
use: 'ts-loader'
}
4.4.3 html-loader
html-loader用于将HTML文件转化为字符串并进行格式化,这使得我们可以把一个HTML片段通过JS加载进来。
{
test: /\.html$/,
use: 'html-loader'
}
4.4.4 handlebars-loader
handlebars-loader用于处理handlebars模板。
{
test: /\.handlebars$/,
use: 'handlebars-loader'
}
4.4.5 file-loader
file-loader用于打包文件类型的资源,并返回其publicPath。
{
test: /\.(png|jpg|gif)$/,
use: "file-loader",
}
4.4.6 url-loader
url-loader与file-loader作用类似,唯一不同在于用户可以设置一个文件大小的阈值,当大于阈值返回publicPath,小于阈值返回base64形式编码。
{
test: /\.(png|jpg|gif)$/,
use: {
loader: 'url-loader',
options: {
limit: 10240,
name: '[name].[ext]',
publicPath: './assets-path'
}
}
}
4.4.7 vue-loader
vue-loader用于处理vue组件。
4.5 自定义loader
1.loader初始化
2.启用缓存
Webpack中可以使用this.cacheable进行控制。
3.获取options
loader-utils
依赖库可以用于提供一些帮助函数。
4.source-map
source-map可以便于实际开发者在浏览器控制台查看源码。
第5章 样式处理
5.1 分离样式文件
mini-css-extract-plugin(适用于Webpack4及以上),专门用于提取样式到CSS文件。
5.1.1 mini-css-extract-plugin
plugins用于接收一个插件数组,我们可以使用Webpack内部提供的一些插件,也可以加载外部插件。
5.1.2 多样式文件的处理
[name].css
处理多样式文件。
5.2 样式预处理
5.2.1 Sass与SCSS
现在使用更多的是SCSS(对CSS3的扩充版本)。
sass-loader就是将SCSS语法编译为CSS。
node-sass用来编译SCSS。
5.2.2 Less
less-loader
。
5.3 PostCSS
PostCSS是一个编译插件的容器,它的工作模式是接受样式源代码并交由编译插件处理,最后输出CSS。
5.3.1 PostCSS与Webpack
rules: [
{
test: /\.css/,
use: [
'style-loader',
'css-loader',
'postcss-loader'
]
}
]
5.3.2 自动前缀
结合Autoprefixer自动添加厂商前缀。
5.3.3 stylelint
stylelint是一个CSS的质量检测工具,就像eslint一样,可以为其添加各种规则,来统一项目的代码规范。
在postcss.config.js中添加相应的配置。
{
plugins: [
stylelint({})
]
}
5.3.4 CSSNext
CSSNext可以让我们在应用中使用最新的css语法特性。
{
plugins: [
postcssNext({})
]
}
5.4 CSS Modules
CSS Modules的理念就是CSS模块化,具体如下:
- 每个CSS文件中的样式都拥有单独的作用域,不会和外界发生命名冲突。
- 对CSS进行依赖管理,可以通过相对路径引入CSS文件。
- 可以通过composes轻松复用其他CSS模块。
{
test: /\.css$/,
use: ["style-loader", {
loader: "css-loader",
options: {
modules: true,
localIdentName: '[name]__[local]__[hash:base64:5]'
}
}, "postcss-loader"],
}
第6章 代码分片
代码分片(code splitting)可以把代码按照特定的形式进行拆分,使用户不必一次全部加载,而是按需加载。
6.1 通过入口划分代码
可以把一些库和工具放在一个单独的入口,该入口产生的资源不会经常更新,可以有效利用客户端缓存。
6.2 optimization.SplitChunks
公共模块提取的好处:
- 开发过程中减少了重复模块打包,可以提升开发速度。
- 减小整体资源体积。
- 合理分片后的代码可以有效地利用客户端缓存。
optimization.SplitChunks是Webpack4为了改进CommonsChunkPlugin而重新设计和实现的代码分片特性。
optimization: {
splitChunks: {
chunks: 'all'
}
}
// 这个配置项的含义是,SplitChunks将会对所有的chunks生效。
6.2.1 从命令式到声明式
splitChunks默认情形下的提取条件:
- 提取后的chunk可被共享或者来自node_modules目录。
- 提取后的JavaScript chunk体积大于30kB(压缩和gzip之前),CSS chunk体积大于50kB。
- 在按需加载过程中,并行请求的资源最大值小于等于5。
- 在首次加载时,并行请求的资源数最大值小于等于3。
6.2.2 默认的异步提取
默认情况下满足上面4个条件就会被提取。
6.2.3 配置
splitChunks默认配置。
splitChunks: {
chunks: "async",
minSize: {
javascript: 30000,
style: 50000
},
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
-
匹配模式
chunks字段配置工作模式有3个可选值,分别是async(默认)、initial和all。
-
匹配条件
maxSize、minChunks、maxAsyncRequests、maxInitialRequests都属于匹配条件。
-
命名
配置项name默认为true,意味着splitChunks可以根据cacheGroups和作用范围自动为新生成的chunk命名,并以autoMaticNameDelimiter分隔。
-
cacheGroups
分离chunks时的规则。默认情况下有两种规则——vendors和dafault。vendors用于提取所有node_modules中符合条件的模块,default则作用于被多次引用的模块。
6.3 资源异步加载
资源异步加载(按需加载)主要解决当模块数量过多、资源体积过大时,可以把一些暂时使用不到的模块延迟加载。
6.3.1 import()
通过import函数加载的模块及其依赖会被异步地进行加载,并返回一个Promise对象。
// foo.js
import('./bar.js').then(( { add } ) => {
console.log(add(2, 3));
})
// bar.js
export function add(a, b) {
return a + b;
}
首屏加载的JS资源地址是通过页面中的script标签来指定的,而间接资源(通过首屏JS再进一步加载的JS)的位置则要通过output.publicPath来指定。
Webpack的import函数可以在任何我们希望的时候调用,而ES6 Module中的import必须出现在代码的顶层作用域。
6.3.2 异步chunk的配置
output: {
chunkFilename: '[name].js'
}
// foo.js
import(/* webpackChunkName: "aaa" */ './bar.js')
通过特有的注释来让Webpack获取到异步chunk的名字,并配置output.chunkFilename为[name].js。
第7章 生产环境配置
7.1 环境配置的封装
- 使用相同的配置文件。
- 为不同环境创建各自的配置文件。
7.2 开启production模式
mode: 'production'
。Webpack会自动添加许多适用于生产环境的配置项,减少了人为手动的工作。
7.3 环境变量
在Webpack中可以使用DefinePlugin进行设置,为生产环境和本地环境添加不同的环境变量。
plugins: [
new webpack.DefinePlugin({
ENV: JSON.stringify('production'),
IS_PRODUCTION: true,
ENV_ID: 111111,
CONSTANTS: JSON.stringify({
TYPES: ['foo', 'bar']
})
})
]
// app.js
document.write(ENV) // production
许多框架和库都采用proces.env.NODE_ENV
作为一个区别开发环境和生成环境的变量。如果启用了mode:production,Webpack会设置好proces.env.NODE_ENV
,不需要人为再添加。
7.4 source map
source map 是指将编译、打包、压缩后的代码映射回源代码的过程。
7.4.1 原理
Webpack对于源代码的每一步处理都有可能会改变代码的位置、结构,甚至是所处文件,因此每一步都需要生成对应的source map。若我们启用了devtool配置项,source map就会跟随源代码一步步被传递,直到生成最后的map文件。
7.4.2 source map配置
devtool: 'source-map'
。开启source map之后,打开Chrome的开发者工具,在“Sources”选项卡下面的“webpack://”目录中可以找到解析后的工程源码。
source map还有cheap-source-map、eval-source-map等。
7.4.3 安全
Webpack提供了hidden-source-map及nonsoureces-source-map两种策略来提升source map的安全性。
hidden-source-map会产出完整的map文件,但是不会在bundle文件中添加对map文件的引用。这样一来我们就无法追溯源码,我们可以使用**Sentry(错误跟踪平台,开发者接入后可以进行错误的收集和聚类)**来解决这个问题。
nosources-source-map,它对于安全性的保护则没那么强,但是使用方式相对简单。文件的具体内容会被隐藏起来,我们仍然可以在控制台查看源代码的错误栈。
还有另外一种选择是正常打包source map,然后通过服务器的nginx设置将.map文件只对固定的白名单(比如公司内网)开放。
7.5 资源压缩
在将资源发布到线上环境前,我们通常都会进行代码压缩,或者叫uglify,意思是移除多余的空格、换行及执行不到的代码,缩短变量名,在执行结果不变的前提下将代码替换为更短的形式。
7.5.1 压缩JavaScript
terser工具,支持ES6+代码的压缩,官方在Webpack4中默认使用了terser的插件terser-webpack-plugin。Webpack4如果开启了mode:production,则不需要人为 设置。
// 覆盖默认的minimizer
optimization: {
minimizer: [new TerserPlugin({})],
}
7.5.2 压缩CSS
压缩CSS文件的前提是使用extract-text-webpack-plugin或mini-css-extract-plugin将样式提取出来,接着使用optimize-css-assets-webpack-plugin来进行压缩,这个插件本质上使用的是压缩器cssnano。
7.6 缓存
缓存是指重复利用浏览器已经获取过的资源。
7.6.1 资源hash
一个常用的方法是每次打包的过程中对资源的内容计算一次hash,并作为版本号存放在文件名中。
output: {
filename: 'bundle@[chunkhash].js'
}
7.6.2 输出动态HTML
理想的情况是在打包结束后自动把最新的资源名同步过去。使用html-webpack-plugin可以帮我们做到这一点。
new HtmlWebpackPlguin({ template: xx.html })
。html-webpack-plugin会自动将我们打包出来的资源名放入生成的index.html中。
7.6.3 使chunk id更稳定
理想状态下,对于缓存的应用是尽量让用户在启动时只更新代码变化的部分,而对没有变化的部分使用缓存。
从Webpack4以后已经修改了模块id的生成机制,也就不再有chunk id都改变的问题。
7.7 bundle体积监控和分析
通过vs code的import cost插件进行模块大小的实时监测。也可以通过webpack-bundle-analyzer进行分析。
第8章 打包优化
让打包的速度更快,输出的资源更小。
8.1 HappyPack
HappyPack是一个通过多线程来提升Webpack打包速度的工具。
8.1.1 工作原理
代码转译的工作流程:
1.从配置中获取打包入口;
2.匹配loader规则,并对入口模块进行转译。
3.对转译后的模块进行依赖查找(如a.js中加载了b.js和c.js)。
4.对新找到的模块重复进行步骤2和步骤3,直到没有新的依赖模块。
HappyPack使用于那些转译任务比较重的工程,比如babel-loader和ts-loader。
8.1.2 单个loader的优化
用HappyPack提供的loader来替换原有loader,并将原有的那个通过HappyPack插件传进去。
const HappyPack = require('happypack')
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'happypack/loader'
}
]
},
plugins: [
new HappyPack({
loaders: [
{
loader: 'babel-loader',
options: {
presets: ['react']
}
}
]
})
]
}
8.1.3 多个loader的优化
优化多个loader时,需要为每一个loader配置一个id。
const HappyPack = require('happypack')
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'happypack/loader?id=js'
},
{
test: /\.ts$/,
exclude: /node_modules/,
loader: 'happypack/loader?id=ts'
}
]
},
plugins: [
new HappyPack({
id: 'js',
loaders: [
{
loader: 'babel-loader',
options: {}
}
]
}),
new HappyPack({
id: 'ts',
loaders: [
{
loader: 'ts-loader',
options: {}
}
]
}),
]
}
8.2 缩小打包作用域
缩小范围针对任务本身,比如去掉冗余的流程,尽量不做重复性的工作等。
8.2.1 exclude和include
一般使用exclude和include排除掉node_modules目录。
8.2.2 noParse
noParse可以对模块进行忽略,这些模块仍然会被打包进资源文件,只不过Webpack不会对其进行任何解析。
module.exports = {
module: {
noParse: /lodash/
}
}
8.2.3 IgnorePlugin
IgnorePlugin可以完全排除一些模块,被排除的模块即便被引用了也不会被打包进资源文件中。
这对于排除一些库相关文件非常有用。一些由库产生的额外资源我们用不到但又无法去掉,因为引用的语句处于库文件的内部。
plugins: [
new webpack.IgnorePlugin({
resourceRegExp: /^\.\/locals$/, // 匹配资源文件
contextRegExp: /moment$/, // 匹配检索目录
})
]
8.2.4 Cache
有些loader会有一个cache配置项,用来在编译代码后同时保存一份缓存,在执行下一次编译前会先检查源文件是否有变化,如果没有就直接采用缓存,也就是上次编译的结果。
在Webpack5中添加了一个新的配置项cache: { type: "filesystem" }
,它会在全局启用一个文件缓存。
8.3 动态链接库与DllPlugin
动态链接库是早期Windows解决计算机内存空间较小的一种内存优化方法。当一段相同的子程序被多个程序调用时,为了减少内存消耗,可以将这段子程序存储为一个可执行文件,当被多个程序调用时只在内存中生成和使用同一个实例。
DllPlugin借鉴了动态链接库的这种思路,对于第三方模块或者一些不常用变化的模块,可以将它们预先编译和打包,然后在项目实际构建过程中直接取用即可。
DllPlugin和Code Splitting有点类似,都可以用来提取公共模块,但本质上有一些区别。Code Splitting的思路是设置一些特定的规则并在打包的过程中根据这些规则提取模块;DllPlugin则是将vendor完全拆出来,有自己的一整套Webpack配置并独立打包,在实际工程构建时就不用再对它进行任何处理,直接取用即可。
8.3.1 vendor配置
// webpack.vendor.config.js
const path = require('path')
const webpack = require('webpack')
const dllAssetPath = path.join(__dirname, 'dll')
const dllLibraryName = 'dllExample'
module.exports = {
entry: ['react'],
output: {
path: dllAssetPath,
filename: 'vendor.js',
library: dllLibraryName
},
plugins: [
new webpack.DllPlugin({
name: dllLibraryName,
path: path.join(dllAssetPath, 'manifest.json')
})
]
}
8.3.2 vendor打包
打包之后会生成一个dll目录,vendor.js包含了库的代码,manifest.json是资源清单。
8.3.3 链接到业务代码
将vendor链接到项目中很简单,通过DllReferencePlugin,它起到一个索引和链接的作用。
new webpack.DllReferencePlugin({
manifest: require(path.join(__dirname, 'dll/manifest.json'))
})
// index.html
<script src="../dll/vendor.js"></script>
当页面执行到vendor.js时,会声明dllExample全局变量。而manifest相当于我们注入app.js的资源地图。
8.4 tree shaking
tree shaking可以在打包过程中帮助我们检测工程中没有被引用过得模块,这部分代码将永远无法被执行到,因此也被称为“死代码”。Webpack会对这部分代码进行标记,并在资源压缩时将它们从最终的bundle中去掉。
8.4.1 ES6 Module
tree shaking只能对ES6 Module生效。如果bundle的体积并没有因为tree shaking而减小,这可能是由于该库是使用CommonJS的形式导出的。
我们应该尽可能使用ES6 Module形式的模块,这样tree shaking的效率更高。
8.4.2 使用Webpack进行依赖关系构建
如果我们在工程中使用了babel-loader,那么一定要通过配置来禁用它的模块依赖解析。因为如果由babel-loader来做依赖解析,Webpack接受到的就都是转化过得CommonJS形式的模块,无法进行tree-shaking。
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [{
loader: 'babel-loader',
options: {
presets: [
[@babel/preset-env, { modules: false }]
]
}
}]
}
]
8.4.3 使用压缩工具去除死代码
tree shaking本身只是为死代码添加上标记,真正去除死代码是通过压缩工具来进行的。在Webpack4之后的版本,将mode设置为production可以达到压缩的效果。
第9章 开发环境调优
9.1 Webpack开发效率插件
9.1.1 webpack-dashboard
webpack-dashboard用于更好地展示控制台输出的一些打包相关的信息。
new DashboardPlugin()
9.1.2 webpack-merge
webpack-merge对于需要配置多种打包环境的项目来说非常实用。
const merge = require('webpack-merge')
module.exports = merge.smart(commonConfig, {
mode: 'production',
module: {
rules: [
{
test: /\.css$/,
...
}
]
}
})
合并代码时会自动添加,不需要编写冗余的代码。
9.1.3 speed-measure-webpack-plugin
speed-measure-webpack-plugin简称SMP,SMP可以分析出Webpack整个打包过程中的各个loader和plugin上耗费的时间。
// webpack.xx.config.js
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin")
const smp = new SpeedMeasurePlugin()
module.exports = smp.wrap({
entry: './app.js',
...
})
9.1.4 size-plugin
size-plugin可以帮助我们监控资源体积的变化,尽早地发现问题。
new SizePlugin()
9.2 模块热替换
只要检测到代码改动就会自动重新构建,然后触发网页刷新,这种一般被称为live reload。Webpack在live reload的基础上又进一步,可以让代码在网页不刷新的前提下得到最新的改动,甚至不需要重新发起请求就能看到更新后的效果,这就是模块热替换(Hot Module Replacement,HMR)。
9.2.1 开启HMR
HMR是需要手动开启的,并且有一些必要条件。
确保项目是基于webpack-dev-server或webpack-dev-middle进行开发,Webpack本身的命令行并不支持HMR。
devServer: {
hot: true
}
上面配置产生的结果是Webpack会为每个模块绑定一个module.hot对象,这个对象包含了HMR的API。
调用HMR API有两种方式,一种是手动地添加这部分代码,另一种是借助一些现成的工具,比如react-hot-loader、vue-loader等。
9.2.2 HMR原理
在本地开发环境下,浏览器是客户端,webpack-dev-server(WDS)相当于是我们的服务端。HMR的核心就是客户端从服务端拉取更新后的资源(HMR拉取的不是整个资源文件,而是chunk diff,即chunk需要更新的部分)。
WDS与浏览器之间维护了一个websocket,当本地资源发生变化时WDS会向浏览器推送更新事件,并带上这次构建的hash,让客户端与上一次资源进行比对。
如果客户端知道新的构建结果和当前的有了差别,就会向WDS发起一个请求来获取更改文件的列表,通常这个请求的名字为[hash].hot-update.json。
9.2.3 HMR API示例
module.hot.accept()
只要发生改变就在当前环境下全部重新执行一遍。
第10章 更多JavaScript打包工具
10.1 Rollup
Webpack的优势在于它更全面,基于“一切皆模块”的思想而衍生出丰富的loader和plugin可以满足各种使用场景。
Rollup更专注于JavaScript的打包。不包含无关代码,资源体积更小。
Rollup有一项Webpack不具备的特性,即通过配置output.format开发者可以选择输出资源的模块形式(CommonJS,amd,esm,iife,umd及system)。
10.2 Parcel
Parcel在JavaScript打包工具中属于相对后来者。(使用的人不多)。