一、Webpack 简介

1.1 webpack 是什么

webpack 是一种前端资源构建工具,一个静态模块打包器(module bundler)。

在webpack 看来, 前端的所有资源文件(js/json/css/img/less/…)都会作为模块处理。
它将根据模块的依赖关系进行静态分析,打包生成对应的静态资源(bundle)。

1.2 webpack 五个核心概念

Entry

入口(Entry):指示 webpack 以哪个文件为入口起点开始打包,分析构建内部依赖图。

Output

输出(Output):指示 webpack 打包后的资源 bundles 输出到哪里去,以及如何命名。

Loader

Loader:让 webpack 能够去处理那些非 JS 的文件,比如样式文件、图片文件(webpack 自身只理解
JS)

Plugins

插件(Plugins):可以用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,
一直到重新定义环境中的变量等。

Mode

模式(Mode):指示 webpack 使用相应模式的配置。

选项 描述 特点
development 会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 development。启用 NamedChunksPlugin 和 NamedModulesPlugin。 能让代码本地调试运行的环境
production 会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 production。启用 FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPlugin 和 TerserPlugin。 能让代码优化上线运行的环境

二、Webpack 初体验

2.1 初始化配置

  1. 初始化 package.json:npm init

  2. 下载安装webpack:(webpack4以上的版本需要全局/本地都安装webpack-cli)

    全局安装:cnpm i webpack webpack-cli -g

    本地安装:cnpm i webpack webpack-cli -D

2.2 编译打包应用

创建 src 下的 js 等文件后,不需要配置 webpack.config.js 文件,在命令行就可以编译打包。

指令:

  • 开发环境:webpack ./src/index.js -o ./build/built.js --mode=development

    webpack会以 ./src/index.js 为入口文件开始打包,打包后输出到 ./build/built.js 整体打包环境,是开发环境

  • 生产环境:webpack ./src/index.js -o ./build/built.js --mode=production

    webpack会以 ./src/index.js 为入口文件开始打包,打包后输出到 ./build/built.js 整体打包环境,是生产环境

结论:

  1. webpack 本身能处理 js/json 资源,不能处理 css/img 等其他资源
  2. 生产环境和开发环境将 ES6 模块化编译成浏览器能识别的模块化,但是不能处理 ES6 的基本语法转化为 ES5(需要借助 loader)
  3. 生产环境比开发环境多一个压缩 js 代码

三、Webpack 开发环境的基本配置

webpack.config.js 是 webpack 的配置文件。

作用: 指示 webpack 干哪些活(当你运行 webpack 指令时,会加载里面的配置)

所有构建工具都是基于 nodejs 平台运行的,模块化默认采用 commonjs。

开发环境配置主要是为了能让代码运行。主要考虑以下几个方面:

  • 打包样式资源
  • 打包 html 资源
  • 打包图片资源
  • 打包其他资源
  • devServer

webpack5 之前我们处理静态资源比如。图片字体之类的资源的时候等,需要用到 url-loaderfile-loaderraw-loader,webpack5则放弃了这三个loader,这三个loader在github上也停止了更新。

webpack5 使用四种新增的资源模块(Asset Modules)替代了这些loader的功能。

asset/resource 将资源分割为单独的文件,并导出url,就是之前的 file-loader的功能.
asset/inline 将资源导出为dataURL(url(data:))的形式,之前的 url-loader的功能.
asset/source 将资源导出为源码(source code). 之前的 raw-loader 功能.
asset 自动选择导出为单独文件或者 dataURL 形式(默认为8KB). 之前有url-loader设置asset size limit 限制实现。

下面是一个简单的开发环境webpack.confg.js配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
/*
开发环境配置:能让代码运行
运行项目指令:
webpack 会将打包结果输出出去(build文件夹下)
npx webpack-dev-server 只会在内存中编译打包,没有输出
*/
/*
loader: 1. 下载 2. 使用(配置loader)
plugins: 1.下载 2. 引入 3. 使用
*/

// resolve用来拼接绝对路径的方法
const {resolve} = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin') // 引用plugin

module.exports = {
// webpack配置
entry: './src/js/index.js', // 入口起点
output: {
// 输出
// 输出文件名
filename: 'js/index.js',
// __dirname是nodejs的变量,代表当前文件的目录绝对路径
path: resolve(__dirname, 'dist'), // 输出路径,所有资源打包都会输出到这个文件夹下
},
// loader配置
module: {
rules: [
// 详细的loader配置
// 不同文件必须配置不同loader处理
{
// 匹配哪些文件
test: /\.less$/,
// 使用哪些loader进行处理
use: [
// use数组中loader执行顺序:从右到左,从下到上,依次执行(先执行css-loader)
// style-loader:创建style标签,将js中的样式资源插入进去,添加到head中生效
'style-loader',
// css-loader:将css文件变成commonjs模块加载到js中,里面内容是样式字符串
'css-loader',
// less-loader:将less文件编译成css文件,需要下载less-loader和less
'less-loader'
],
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
// options 会传进 loader
options:{}
},
{
// webpack 5 之前
// test: /\.(jpg|png|gif)$/,
// loader: 'url-loader',
// options: {
// limit: 8 * 1024,
// name: '[hash:10].[ext]',
// esModule: false,
// outputPath: 'imgs',
// },

// 处理图片资源 ( webpack5 )
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: "asset",
//解析
parser: {
// 小于 8kb 的图片转base64 建议 8-12kb 的图片转base64
// 优点:减少请求数量(减轻服务器压力)
// 缺点:图片体积会更大(文件请求速度更慢)
dataUrlCondition: {
maxSize: 8 * 1024, // 25kb
}
},
generator: {
//与output.assetModuleFilename是相同的,这个写法引入的时候也会添加好这个路径
//给图片重命名
// [hash:6] 取图片的 hash 的前 6 位
// [ext] 图片的原扩展名
filename: 'img/[name].[hash:6][ext]',
//打包后对资源的引入,文件命名已经有/img了
publicPath: './'
},
},
{
test: /\.html$/,
// 处理html文件的img图片(负责引入img,从而能被url-loader进行处理)
loader: 'html-loader',
},
{
// 处理字体图标资源
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
// 打包其他资源(除了html/js/css资源以外的资源)
{
// 排除html|js|css|less|jpg|png|gif文件
exclude: /\.(html|js|css|less|jpg|png|gif)/,
type: 'asset/resource',
generator: {
filename: 'static/[hash][ext][query]', // 单独指定 名字
publicPath: './'
}
}
],
},
// plugin的配置
plugins: [
// html-webpack-plugin:默认会创建一个空的html文件,自动引入打包输出的所有资源(JS/CSS)
// 需要有结构的HTML文件可以加一个template
new HtmlWebpackPlugin({
// 复制这个./src/index.html文件,并自动引入打包输出的所有资源(JS/CSS)
template: './src/index.html',
}),
],
// 模式
mode: 'development', // 开发模式
// 开发服务器 devServer:用来自动化(自动编译,自动打开浏览器,自动刷新浏览器)
// 只会在内存中编译打包,不会有任何输出
// 启动devServer指令为:npx webpack-dev-server 或 webpack serve(5.0 可用)
devServer: {
// 项目构建后路径
static: {
directory: resolve(__dirname, 'dist'),
},
// 启动gzip压缩
compress: true,
// 端口
port: 3000,
// 自动打开浏览器
open: true
}
}

其中,大部分配置都在注释中给出解释。

  • 运行项目的两个指令:
    webpack 会将打包结果输出出去(编译)
    webpack server 只会在内存中编译打包,没有输出

  • loader 和 plugin 的不同:(plugin 一定要先引入才能使用)

    loader:1. 下载 2. 使用(配置 loader)

    plugins:1.下载 2. 引入 3. 使用

四、Webpack 生产环境的基本配置

而生产环境的配置需要考虑以下几个方面:

  • 提取 css 成单独文件
  • css 兼容性处理
  • 压缩 css
  • js 语法检查
  • js 兼容性处理
  • js 压缩
  • html 压缩

下面是一个基本的生产环境下的webpack.config.js配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
const {resolve} = require('path')
const miniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const HtmlWebpackPlugin = require('html-webpack-plugin')

// 定义node.js的环境变量,决定使用browserslist的哪个环境
process.env.NODE_ENV = 'production'

// 复用loader的写法
const commonCssLoader = [
// 这个loader取代style-loader。作用:提取js中的css成单独文件然后通过link加载
miniCssExtractPlugin.loader,
// css-loader:将css文件整合到js文件中
// 经过css-loader处理后,样式文件是在js文件中的
// 问题:1.js文件体积会很大2.需要先加载js再动态创建style标签,样式渲染速度就慢,会出现闪屏现象
// 解决:用MiniCssExtractPlugin.loader替代style-loader
'css-loader',
/*
postcss-loader:css兼容性处理:postcss --> 需要安装:postcss-loader postcss-preset-env
postcss需要通过package.json中browserslist里面的配置加载指定的css兼容性样式
在package.json中定义browserslist:
"browserslist": {
// 开发环境 --> 设置node环境变量:process.env.NODE_ENV = development
"development": [ // 只需要可以运行即可
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
],
// 生产环境。默认是生产环境
"production": [ // 需要满足绝大多数浏览器的兼容
">0.2%",
"not dead",
"not op_mini all"
]
},
*/
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
[
'postcss-preset-env',
],
],
},
},
},
]

module.exports = {
entry: './src/js/index.js',
output: {
filename: 'js/built.js',
path: resolve(__dirname, 'build'),
},
module: {
rules: [
{
test: /\.css$/,
use: [...commonCssLoader],
},
{
test: /\.less$/,
//use数组中loader执行顺序:从右到左,从下到上,依次执行
use: [...commonCssLoader, 'less-loader'],
},

/*
js兼容性处理:需要下载 babel-loader @babel/core
1. 基本js兼容性处理 --> @babel/preset-env
问题:只能转换基本语法,如promise高级语法不能转换
2. 全部js兼容性处理 --> @babel/polyfill
问题:只要解决部分兼容性问题,但是将所有兼容性代码全部引入,体积太大了
3. 需要做兼容性处理的就做:按需加载 --> core-js
*/
{
// 第三种方式:按需加载
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
// 预设:指示babel做怎样的兼容性处理
presets: [[
'@babel/preset-env', // 基本预设
{
useBuiltIns: 'usage', //按需加载
corejs: {version: 3}, // 指定core-js版本
targets: { // 指定兼容到什么版本的浏览器
chrome: '60',
firefox: '50',
ie: '9',
safari: '10',
edge: '17'
},
},
]],
},
},
{
// 处理图片资源
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: "asset",
//解析
parser: {
// 小于 8kb 的图片转base64 建议 8-12kb 的图片转base64
// 优点:减少请求数量(减轻服务器压力)
// 缺点:图片体积会更大(文件请求速度更慢)
dataUrlCondition: {
maxSize: 8 * 1024, // 25kb
}
},
generator: {
//与output.assetModuleFilename是相同的,这个写法引入的时候也会添加好这个路径
//给图片重命名
// [hash:6] 取图片的 hash 的前 6 位
// [ext] 图片的原扩展名
filename: 'img/[name].[hash:6][ext]',
//打包后对资源的引入,文件命名已经有/img了
publicPath: './'
},
},
// html中的图片处理
{
test: /\.html$/,
loader: 'html-loader',
},
// 处理其他文件
{
exclude: /\.(js|css|less|html|jpg|png|gif)/,
loader: 'file-loader',
options: {
outputPath: 'media',
},
},
],
},
optimization: {
minimizer: [
// 压缩js
'...',
// 压缩css
new CssMinimizerPlugin({
minimizerOptions: {
preset: [
"default",
{
// 移除所有注释
discardComments: {removeAll: true},
},
],
},
}),
],
},
plugins: [
// HtmlWebpackPlugin:html文件的打包和压缩处理
// 通过这个插件会自动将单独打包的样式文件通过link标签引入
new HtmlWebpackPlugin({
template: './src/index.html',
// 压缩html代码
minify: {
// 移除空格
collapseWhitespace: true,
// 移除注释
removeComments: true,
},
}),
// 提取 css 成单独的文件
new miniCssExtractPlugin({
filename: 'css/built.css'
})
],
// 生产环境下会自动压缩js代码
mode: 'production',
}

五、Webpack 优化配置

5.1 开发环境性能优化

5.1.1 HMR(模块热替换)

HMR: hot module replacement 热模块替换 / 模块热替换

作用:一个模块发生变化,只会重新打包构建这一个模块(而不是打包所有模块) ,极大提升构建速度

代码:只需要在 devServer 中设置 hot 为 true,就会自动开启HMR功能(只能在开发模式下使用)

1
2
3
4
5
6
7
8
9
devServer: {
contentBase: resolve(__dirname, 'build'),
compress: true,
port: 3000,
open: true,
// 开启HMR功能 默认开启
// 当修改了webpack配置,新配置要想生效,必须重启webpack服务
hot: true
}

每种文件实现热模块替换的情况:

  • 样式文件:可以使用HMR功能,因为开发环境下使用的 style-loader 内部默认实现了热模块替换功能

  • js 文件:默认不能使用HMR功能(修改一个 js 模块所有 js 模块都会刷新)

    –> 实现 HMR 需要修改 js 代码(添加支持 HMR 功能的代码)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 绑定
    if (module.hot) {
    // 一旦 module.hot 为true,说明开启了HMR功能。 --> 让HMR功能代码生效
    module.hot.accept('./print.js', function() {
    // 方法会监听 print.js 文件的变化,一旦发生变化,只有这个模块会重新打包构建,其他模块不会。
    // 会执行后面的回调函数
    print();
    });
    }

    注意:HMR 功能对 js 的处理,只能处理非入口 js 文件的其他文件。

  • html 文件: 默认不能使用 HMR 功能(html 不用做 HMR 功能,因为只有一个 html 文件,不需要再优化)

    使用 HMR 会导致问题:html 文件不能热更新了(不会自动打包构建)

    解决:修改 entry 入口,将 html 文件引入(这样 html 修改整体刷新)

    1
    entry: ['./src/js/index.js', './src/index.html']

5.1.2 source-map

source-map:一种提供源代码到构建后代码的映射的技术 (如果构建后代码出错了,通过映射可以追踪源代码错误)

参数:[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map

代码:

1
devtool: 'eval-source-map'

可选方案:[生成source-map的位置|给出的错误代码信息]

  • source-map:外部,错误代码准确信息 和 源代码的错误位置
  • inline-source-map:内联,只生成一个内联 source-map,错误代码准确信息 和 源代码的错误位置
  • hidden-source-map:外部,错误代码错误原因,但是没有错误位置(为了隐藏源代码),不能追踪源代码错误,只能提示到构建后代码的错误位置
  • eval-source-map:内联,每一个文件都生成对应的 source-map,都在 eval 中,错误代码准确信息 和 源代码的错误位
  • nosources-source-map:外部,错误代码准确信息,但是没有任何源代码信息(为了隐藏源代码)
  • cheap-source-map:外部,错误代码准确信息 和 源代码的错误位置,只能把错误精确到整行,忽略列
  • cheap-module-source-map:外部,错误代码准确信息 和 源代码的错误位置,module 会加入 loader 的 source-map

内联 和 外部的区别:1. 外部生成了文件,内联没有 2. 内联构建速度更快

开发/生产环境可做的选择:

开发环境:需要考虑速度快,调试更友好

  • 速度快( eval > inline > cheap >… )
    1. eval-cheap-souce-map
    2. eval-source-map
  • 调试更友好
    1. souce-map
    2. cheap-module-souce-map
    3. cheap-souce-map

最终得出最好的两种方案 --> eval-source-map(完整度高,内联速度快) / eval-cheap-module-souce-map(错误提示忽略列但是包含其他信息,内联速度快)

生产环境:需要考虑源代码要不要隐藏,调试要不要更友好

  • 内联会让代码体积变大,所以在生产环境不用内联
  • 隐藏源代码
    1. nosources-source-map 全部隐藏
    2. hidden-source-map 只隐藏源代码,会提示构建后代码错误信息

最终得出最好的两种方案 --> source-map(最完整) / cheap-module-souce-map(错误提示一整行忽略列)

5.2 生产环境性能优化

5.2.1 优化打包构建速度

5.2.1.1 oneOf

oneOf:匹配到 loader 后就不再向后进行匹配,优化生产环境的打包构建速度

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
module: {
rules: [
{
// js 语法检查
test: /\.js$/,
exclude: /node_modules/,
// 优先执行
enforce: 'pre',
loader: 'eslint-loader',
options: {
fix: true
}
},
{
// oneOf 优化生产环境的打包构建速度
// 以下loader只会匹配一个(匹配到了后就不会再往下匹配了)
// 注意:不能有两个配置处理同一种类型文件(所以把eslint-loader提取出去放外面)
oneOf: [
{
test: /\.css$/,
use: [...commonCssLoader]
},
{
test: /\.less$/,
use: [...commonCssLoader, 'less-loader']
},
{
// js 兼容性处理
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs: {version: 3},
targets: {
chrome: '60',
firefox: '50'
}
}
]
]
}
},
{
test: /\.(jpg|png|gif)/,
loader: 'url-loader',
options: {
limit: 8 * 1024,
name: '[hash:10].[ext]',
outputPath: 'imgs',
esModule: false
}
},
{
test: /\.html$/,
loader: 'html-loader'
},
{
exclude: /\.(js|css|less|html|jpg|png|gif)/,
loader: 'file-loader',
options: {
outputPath: 'media'
}
}
]
}
]
},

5.2.1.2 babel 缓存

babel 缓存:类似 HMR,将 babel 处理后的资源缓存起来(哪里的 js 改变就更新哪里,其他 js 还是用之前缓存的资源),让第二次打包构建速度更快

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs: { version: 3 },
targets: {
chrome: '60',
firefox: '50'
}
}
]
],
// 开启babel缓存
// 第二次构建时,会读取之前的缓存
cacheDirectory: true
}
},

文件资源缓存

文件名不变,就不会重新请求,而是再次用之前缓存的资源

1.hash: 每次 wepack 打包时会生成一个唯一的 hash 值。

问题:重新打包,所有文件的 hsah 值都改变,会导致所有缓存失效。(可能只改动了一个文件)

2.chunkhash:根据 chunk 生成的 hash 值。来源于同一个 chunk的 hash 值一样

问题:js 和 css 来自同一个chunk,hash 值是一样的(因为 css-loader 会将 css 文件加载到 js 中,所以同属于一个chunk)

3.contenthash: 根据文件的内容生成 hash 值。不同文件 hash 值一定不一样(文件内容修改,文件名里的 hash 才会改变)

修改 css 文件内容,打包后的 css 文件名 hash 值就改变,而 js 文件没有改变 hash 值就不变,这样 css 和 js 缓存就会分开判断要不要重新请求资源 --> 让代码上线运行缓存更好使用

5.2.1.3 多进程打包

多进程打包:某个任务消耗时间较长会卡顿,多进程可以同一时间干多件事,效率更高。

优点是提升打包速度,缺点是每个进程的开启和交流都会有开销(babel-loader消耗时间最久,所以使用thread-loader针对其进行优化)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
{
test: /\.js$/,
exclude: /node_modules/,
use: [
/*
thread-loader会对其后面的loader(这里是babel-loader)开启多进程打包。
进程启动大概为600ms,进程通信也有开销。(启动的开销比较昂贵,不要滥用)
只有工作消耗时间比较长,才需要多进程打包
*/
{
loader: 'thread-loader',
options: {
workers: 2 // 进程2个
}
},
{
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs: { version: 3 },
targets: {
chrome: '60',
firefox: '50'
}
}
]
],
// 开启babel缓存
// 第二次构建时,会读取之前的缓存
cacheDirectory: true
}
}
]
},

5.2.1.4 externals

externals:让某些库不打包,通过 cdn 引入

webpack.config.js 中配置:

1
2
3
4
5
externals: {
// 拒绝jQuery被打包进来(通过cdn引入,速度会快一些)
// 忽略的库名 -- npm包名
jquery: 'jQuery'
}

需要在 index.html 中通过 cdn 引入:

1
<script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>

5.2.1.5 dll

dll:让某些库单独打包,后直接引入到 build 中。可以在 code split 分割出 node_modules 后再用 dll 更细的分割,优化代码运行的性能。

webpack.dll.js 配置:(将 jquery 单独打包)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/*
node_modules的库会打包到一起,但是很多库的时候打包输出的js文件就太大了
使用dll技术,对某些库(第三方库:jquery、react、vue...)进行单独打包
当运行webpack时,默认查找webpack.config.js配置文件
需求:需要运行webpack.dll.js文件
--> webpack --config webpack.dll.js(运行这个指令表示以这个配置文件打包)
*/
const { resolve } = require('path');
const webpack = require('webpack');

module.exports = {
entry: {
// 最终打包生成的[name] --> jquery
// ['jquery] --> 要打包的库是jquery
jquery: ['jquery']
},
output: {
// 输出出口指定
filename: '[name].js', // name就是jquery
path: resolve(__dirname, 'dll'), // 打包到dll目录下
library: '[name]_[hash]', // 打包的库里面向外暴露出去的内容叫什么名字
},
plugins: [
// 打包生成一个manifest.json --> 提供jquery的映射关系(告诉webpack:jquery之后不需要再打包和暴露内容的名称)
new webpack.DllPlugin({
name: '[name]_[hash]', // 映射库的暴露的内容名称
path: resolve(__dirname, 'dll/manifest.json') // 输出文件路径
})
],
mode: 'production'
};

webpack.config.js 配置:(告诉 webpack 不需要再打包 jquery,并将之前打包好的 jquery 跟其他打包好的资源一同输出到 build 目录下)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 引入插件
const webpack = require('webpack');
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');

// plugins中配置:
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
}),
// 告诉webpack哪些库不参与打包,同时使用时的名称也得变
new webpack.DllReferencePlugin({
manifest: resolve(__dirname, 'dll/manifest.json')
}),
// 将某个文件打包输出到build目录下,并在html中自动引入该资源
new AddAssetHtmlWebpackPlugin({
filepath: resolve(__dirname, 'dll/jquery.js')
})
],

5.2.2 优化代码运行的性能

5.2.2.1 缓存

5.2.2.2 tree shaking(树摇)

tree shaking:去除无用代码

前提:1. 必须使用 ES6 模块化 2. 开启 production 环境 (这样就自动会把无用代码去掉)

作用:减少代码体积

在 package.json 中配置:

"sideEffects": false 表示所有代码都没有副作用(都可以进行 tree shaking)

这样会导致的问题:可能会把 css / @babel/polyfill 文件干掉(副作用)

所以可以配置:"sideEffects": ["*.css", "*.less"] 不会对css/less文件tree shaking处理

5.2.2.3 code split(代码分割)

代码分割。将打包输出的一个大的 bundle.js 文件拆分成多个小文件,这样可以并行加载多个文件,比加载一个文件更快。

1.多入口拆分

1
2
3
4
5
6
7
8
9
10
entry: {
// 多入口:有一个入口,最终输出就有一个bundle
index: './src/js/index.js',
test: './src/js/test.js'
},
output: {
// [name]:取文件名
filename: 'js/[name].[contenthash:10].js',
path: resolve(__dirname, 'build')
},

2.optimization:

1
2
3
4
5
optimization: {
splitChunks: {
chunks: 'all'
}
},
  • 将 node_modules 中的代码单独打包(大小超过30kb)
  • 自动分析多入口chunk中,有没有公共的文件。如果有会打包成单独一个chunk(比如两个模块中都引入了jquery会被打包成单独的文件)(大小超过30kb)

3.import 动态导入语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
通过js代码,让某个文件被单独打包成一个chunk
import动态导入语法:能将某个文件单独打包(test文件不会和index打包在同一个文件而是单独打包)
webpackChunkName:指定test单独打包后文件的名字
*/
import(/* webpackChunkName: 'test' */'./test')
.then(({ mul, count }) => {
// 文件加载成功~
// eslint-disable-next-line
console.log(mul(2, 5));
})
.catch(() => {
// eslint-disable-next-line
console.log('文件加载失败~');
});

5.2.2.4 lazy loading(懒加载/预加载)

1.懒加载:当文件需要使用时才加载(需要代码分割)。但是如果资源较大,加载时间就会较长,有延迟。

2.正常加载:可以认为是并行加载(同一时间加载多个文件)没有先后顺序,先加载了不需要的资源就会浪费时间。

3.预加载 prefetch(兼容性很差):会在使用之前,提前加载。等其他资源加载完毕,浏览器空闲了,再偷偷加载这个资源。这样在使用时已经加载好了,速度很快。所以在懒加载的基础上加上预加载会更好。

代码:

1
2
3
4
5
6
7
8
9
10
document.getElementById('btn').onclick = function() {
// 将import的内容放在异步回调函数中使用,点击按钮,test.js才会被加载(不会重复加载)
// webpackPrefetch: true表示开启预加载
import(/* webpackChunkName: 'test', webpackPrefetch: true */'./test').then(({ mul }) => {
console.log(mul(4, 5));
});
import('./test').then(({ mul }) => {
console.log(mul(2, 5))
})
};

5.2.2.5 pwa(离线可访问技术)

pwa:离线可访问技术(渐进式网络开发应用程序),使用 serviceworker 和 workbox 技术。优点是离线也能访问,缺点是兼容性差。

webpack.config.js 中配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
const WorkboxWebpackPlugin = require('workbox-webpack-plugin'); // 引入插件

// plugins中加入:
new WorkboxWebpackPlugin.GenerateSW({
/*
1. 帮助serviceworker快速启动
2. 删除旧的 serviceworker

生成一个 serviceworker 配置文件
*/
clientsClaim: true,
skipWaiting: true
})

index.js 中还需要写一段代码来激活它的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
1. eslint不认识 window、navigator全局变量
解决:需要修改package.json中eslintConfig配置
"env": {
"browser": true // 支持浏览器端全局变量
}
2. sw代码必须运行在服务器上
--> nodejs
或-->
npm i serve -g
serve -s build 启动服务器,将打包输出的build目录下所有资源作为静态资源暴露出去
*/
if ('serviceWorker' in navigator) { // 处理兼容性问题
window.addEventListener('load', () => {
navigator.serviceWorker
.register('/service-worker.js') // 注册serviceWorker
.then(() => {
console.log('sw注册成功了~');
})
.catch(() => {
console.log('sw注册失败了~');
});
});
}

六、Webpack 配置详情

6.1 entry

entry: 入口起点

  1. string --> ‘./src/index.js’,单入口

    打包形成一个 chunk。 输出一个 bundle 文件。此时 chunk 的名称默认是 main

  2. array --> [’./src/index.js’, ‘./src/add.js’],多入口

    所有入口文件最终只会形成一个 chunk,输出出去只有一个 bundle 文件。

    (一般只用在 HMR 功能中让 html 热更新生效)

  3. object,多入口

    有几个入口文件就形成几个 chunk,输出几个 bundle 文件,此时 chunk 的名称是 key 值

–> 特殊用法:

1
2
3
4
5
6
entry: {
// 最终只会形成一个chunk, 输出出去只有一个bundle文件。
index: ['./src/index.js', './src/count.js'],
// 形成一个chunk,输出一个bundle文件。
add: './src/add.js'
}

6.2 output

1
2
3
4
5
6
7
8
9
10
11
12
13
output: {
// 文件名称(指定名称+目录)
filename: 'js/[name].js',
// 输出文件目录(将来所有资源输出的公共目录)
path: resolve(__dirname, 'build'),
// 所有资源引入公共路径前缀 --> 'imgs/a.jpg' --> '/imgs/a.jpg'
publicPath: '/',
chunkFilename: 'js/[name]_chunk.js', // 指定非入口chunk的名称
library: '[name]', // 打包整个库后向外暴露的变量名
libraryTarget: 'window' // 变量名添加到哪个上 browser:window
// libraryTarget: 'global' // node:global
// libraryTarget: 'commonjs' // conmmonjs模块 exports
},

6.3 module

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
module: {
rules: [
// loader的配置
{
test: /\.css$/,
// 多个loader用use
use: ['style-loader', 'css-loader']
},
{
test: /\.js$/,
// 排除node_modules下的js文件
exclude: /node_modules/,
// 只检查src下的js文件
include: resolve(__dirname, 'src'),
enforce: 'pre', // 优先执行
// enforce: 'post', // 延后执行
// 单个loader用loader
loader: 'eslint-loader',
options: {} // 指定配置选项
},
{
// 以下配置只会生效一个
oneOf: []
}
]
},

6.4 resolve

1
2
3
4
5
6
7
8
9
10
11
// 解析模块的规则
resolve: {
// 配置解析模块路径别名: 优点:当目录层级很复杂时,简写路径;缺点:路径不会提示
alias: {
$css: resolve(__dirname, 'src/css')
},
// 配置省略文件路径的后缀名(引入时就可以不写文件后缀名了)
extensions: ['.js', '.json', '.jsx', '.css'],
// 告诉 webpack 解析模块应该去找哪个目录
modules: [resolve(__dirname, '../../node_modules'), 'node_modules']
}

这样配置后,引入文件就可以这样简写:import '$css/index';

6.5 dev server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
devServer: {
// 运行代码所在的目录
contentBase: resolve(__dirname, 'build'),
// 监视contentBase目录下的所有文件,一旦文件变化就会reload
watchContentBase: true,
watchOptions: {
// 忽略文件
ignored: /node_modules/
},
// 启动gzip压缩
compress: true,
// 端口号
port: 5000,
// 域名
host: 'localhost',
// 自动打开浏览器
open: true,
// 开启HMR功能
hot: true,
// 不要显示启动服务器日志信息
clientLogLevel: 'none',
// 除了一些基本信息外,其他内容都不要显示
quiet: true,
// 如果出错了,不要全屏提示
overlay: false,
// 服务器代理,--> 解决开发环境跨域问题
proxy: {
// 一旦devServer(5000)服务器接收到/api/xxx的请求,就会把请求转发到另外一个服务器3000
'/api': {
target: 'http://localhost:3000',
// 发送请求时,请求路径重写:将/api/xxx --> /xxx (去掉/api)
pathRewrite: {
'^/api': ''
}
}
}
}

其中,跨域问题:同源策略中不同的协议、端口号、域名就会产生跨域。

正常的浏览器和服务器之间有跨域,但是服务器之间没有跨域。代码通过代理服务器运行,所以浏览器和代理服务器之间没有跨域,浏览器把请求发送到代理服务器上,代理服务器替你转发到另外一个服务器上,服务器之间没有跨域,所以请求成功。代理服务器再把接收到的响应响应给浏览器。这样就解决开发环境下的跨域问题。

6.6 optimization

contenthash 缓存会导致一个问题:修改 a 文件导致 b 文件 contenthash 变化。
因为在 index.js 中引入 a.js,打包后 index.js 中记录了 a.js 的 hash 值,而 a.js 改变,其重新打包后的 hash 改变,导致 index.js 文件内容中记录的 a.js 的 hash 也改变,从而重新打包后 index.js 的 hash 值也会变,这样就会使缓存失效。(改变的是a.js文件但是 index.js 文件的 hash 值也改变了)
解决办法:runtimeChunk --> 将当前模块记录其他模块的 hash 单独打包为一个文件 runtime,这样 a.js 的 hash 改变只会影响 runtime 文件,不会影响到 index.js 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
output: {
filename: 'js/[name].[contenthash:10].js',
path: resolve(__dirname, 'build'),
chunkFilename: 'js/[name].[contenthash:10]_chunk.js' // 指定非入口文件的其他chunk的名字加_chunk
},
optimization: {
splitChunks: {
chunks: 'all',
/* 以下都是splitChunks默认配置,可以不写
miniSize: 30 * 1024, // 分割的chunk最小为30kb(大于30kb的才分割)
maxSize: 0, // 最大没有限制
minChunks: 1, // 要提取的chunk最少被引用1次
maxAsyncRequests: 5, // 按需加载时并行加载的文件的最大数量为5
maxInitialRequests: 3, // 入口js文件最大并行请求数量
automaticNameDelimiter: '~', // 名称连接符
name: true, // 可以使用命名规则
cacheGroups: { // 分割chunk的组
vendors: {
// node_modules中的文件会被打包到vendors组的chunk中,--> vendors~xxx.js
// 满足上面的公共规则,大小超过30kb、至少被引用一次
test: /[\\/]node_modules[\\/]/,
// 优先级
priority: -10
},
default: {
// 要提取的chunk最少被引用2次
minChunks: 2,
prority: -20,
// 如果当前要打包的模块和之前已经被提取的模块是同一个,就会复用,而不是重新打包
reuseExistingChunk: true
}
} */
},
// 将index.js记录的a.js的hash值单独打包到runtime文件中
runtimeChunk: {
name: entrypoint => `runtime-${entrypoint.name}`
},
minimizer: [
// 配置生产环境的压缩方案:js/css
new TerserWebpackPlugin({
// 开启缓存
cache: true,
// 开启多进程打包
parallel: true,
// 启用sourceMap(否则会被压缩掉)
sourceMap: true
})
]
}

七、Webpack5 介绍和使用

此版本重点关注以下内容:

  • 通过持久缓存提高构建性能.
  • 使用更好的算法和默认值来改善长期缓存.
  • 通过更好的树摇和代码生成来改善捆绑包大小.
  • 清除处于怪异状态的内部结构,同时在 v4 中实现功能而不引入任何重大更改.
  • 通过引入重大更改来为将来的功能做准备,以使我们能够尽可能长时间地使用 v5.

下载

npm i webpack@next webpack-cli -D

自动删除 Node.js Polyfills

早期,webpack 的目标是允许在浏览器中运行大多数 node.js 模块,但是模块格局发生了变化,许多模块用途现在主要是为前端目的而编写的。webpack <= 4 附带了许多 node.js 核心模块的 polyfill,一旦模块使用任何核心模块(即 crypto 模块),这些模块就会自动应用。

尽管这使使用为 node.js 编写的模块变得容易,但它会将这些巨大的 polyfill 添加到包中。在许多情况下,这些 polyfill 是不必要的。

webpack 5 会自动停止填充这些核心模块,并专注于与前端兼容的模块。

迁移:

  • 尽可能尝试使用与前端兼容的模块。
  • 可以为 node.js 核心模块手动添加一个 polyfill。错误消息将提示如何实现该目标。

Chunk 和模块 ID

添加了用于长期缓存的新算法。在生产模式下默认情况下启用这些功能。

1
chunkIds: "deterministic", moduleIds: "deterministic"

Chunk ID

你可以不用使用 import(/* webpackChunkName: "name" */ "module") 在开发环境来为 chunk 命名,生产环境还是有必要的

webpack 内部有 chunk 命名规则,不再是以 id(0, 1, 2)命名了

Tree Shaking

  1. webpack 现在能够处理对嵌套模块的 tree shaking
1
2
3
4
5
6
7
8
9
10
11
// inner.js
export const a = 1;
export const b = 2;

// module.js
import * as inner from './inner';
export { inner };

// user.js
import * as module from './module';
console.log(module.inner.a);

在生产环境中, inner 模块暴露的 b 会被删除

  1. webpack 现在能够多个模块之前的关系
1
2
3
4
5
6
7
8
9
import { something } from './something';

function usingSomething() {
return something;
}

export function test() {
return usingSomething();
}

当设置了"sideEffects": false时,一旦发现test方法没有使用,不但删除test,还会删除"./something"

  1. webpack 现在能处理对 Commonjs 的 tree shaking

Output

webpack 4 默认只能输出 ES5 代码

webpack 5 开始新增一个属性 output.ecmaVersion, 可以生成 ES5 和 ES6 / ES2015 代码.

如:output.ecmaVersion: 2015

SplitChunk

1
2
3
4
5
6
7
// webpack4
minSize: 30000;
// webpack5
minSize: {
javascript: 30000,
style: 50000,
}

Caching

1
2
3
4
5
6
7
8
9
// 配置缓存
cache: {
// 磁盘存储
type: "filesystem",
buildDependencies: {
// 当配置修改时,缓存失效
config: [__filename]
}
}

缓存将存储到 node_modules/.cache/webpack

监视输出文件

之前 webpack 总是在第一次构建时输出全部文件,但是监视重新构建时会只更新修改的文件。

此次更新在第一次构建时会找到输出文件看是否有变化,从而决定要不要输出全部文件。

默认值

  • entry: "./src/index.js
  • output.path: path.resolve(__dirname, "dist")
  • output.filename: "[name].js"

更多内容

github