Webpack概述

一.Webpack是什么

Webpack是一种前端资源构建工具,一个静态模块打包器,在Webpack看来,前端的所有资源文件都会作为模块处理,它将根据模块的依赖关系进行静态分析,打包生成对应的静态资源。

二.Webpack的核心概念

**entry(入口):**指示 webpack 以哪个文件为入口起点开始打包,分析构建内部依赖图。
output(输出):指示webpack打包后的资源bundles输出到哪里去,以及命名
loader:让 webpack 能够去处理那些非 JS 的文件,比如样式文件、图片文件,webpack只能理解Javascript和JSON文件
plugins(插件):loader 用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。包括:打包优化,资源管理,注入环境变量
mode(模式):指示webpack使用相应模式的配置
浏览器兼容性(browser compatibility):webpack 支持所有符合 ES5 标准的浏览器(不支持 IE8 及以下版本)。webpack 的 import() 和 require.ensure() 需要 Promise。如果你想要支持旧版本浏览器,在使用这些表达式之前,还需要 提前加载 polyfill
环境(environment):webpack运行的环境

三.webpack优化配置

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']

(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(错误提示一整行忽略列)

2.生产环境性能优化


优化打包构建速度

(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'
}
}
]
}
]
},

注意:如果oneOf中有相同的loader,一定要只在oneOf中只留一个loader

(2)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 缓存就会分开判断要不要重新请求资源 –> 让代码上线运行缓存更好使用
(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
}
}
]
}

(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)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')
})
]

优化代码运行的性能

(1)缓存

(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处理
(3)code split(代码分割)
代码分割:将打包输出的一个大的 bundle.js 文件拆分成多个小文件,这样可以并行加载多个文件,比加载一个文件更快。
多入口拆分

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')
},

optimization

  • 将 node_modules 中的代码单独打包(大小超过30kb)
  • 自动分析多入口chunk中,有没有公共的文件。如果有会打包成单独一个chunk(比如两个模块中都引入了jquery会被打包成单独的文件)(大小超过30kb)
    1
    2
    3
    4
    5
    optimization: {
    splitChunks: {
    chunks: 'all'
    }
    },
  • 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('文件加载失败~');
    });
  • (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)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入门-学习总结 | Woc12138

视频学习地址:尚硅谷最新版Webpack5实战教程(从入门到精通)_哔哩哔哩_bilibili


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!