使用Webpack5创建Vue2项目及优化

前言

之前我们大多都是用Vue-Cli来创建项目,但是Vue-Cli已经停止更新了,并且Vue-Cli相当于一堆插件的集合体,我们想替换以下,或者想根据我们的项目优化以下,提升编译的性能,这时候可以自己用Webpack来配置项目。

在搭建的时候最头疼的几个问题

  • 依赖下载不下来
  • 依赖之间不兼容
  • 依赖和NodeJS版本不兼容

安装cnpm 可以解决依赖无法下载的问题

1
npm install -g cnpm --registry=https://registry.npm.taobao.org

本文是在Node v12.22.6的版本下测试的。

配置步骤

基本配置

创建项目文件夹 webpack01

进入项目文件夹根目录,运行

1
npm init

安装基础依赖

1
2
3
4
npm i -D webpack@5.74.0 webpack-cli@4.10.0 webpack-dev-server@4.10.0
npm i -D html-webpack-plugin@5.5.0
npm i vue@2.6.11
npm i -D vue-loader@15.10.0 vue-template-compiler@2.6.11

注意

vue-template-compiler要和vue的版本一致

html-webpack-plugin@5.x才支持webpack@5.x

创建以下文件夹及文件

image-20220826175336877

/public/index.html

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>标题</title>
</head>
<body>
<div id="app"></div>
</body>
</html>

/src/App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div>Hello</div>
</template>

<script>
export default {
name: "App"
}
</script>

<style scoped>

</style>

/src/main.js

1
2
3
4
5
import App from './App';
import Vue from "vue";
new Vue({
render: h => h(App)
}).$mount("#app");

/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
const path = require('path');
const {VueLoaderPlugin} = require('vue-loader')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
mode: 'development',
devtool: 'source-map',
entry: './src/main.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'main.js'
},
watch: true,
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
},
]
},
plugins: [
new VueLoaderPlugin(),
new HtmlWebpackPlugin({template: './public/index.html'}), //JS或者CSS文件可以自动引入到html中
],
resolve: {
extensions: ['.js', '.css', '.vue'], //配置后缀名
},
devServer: {
port: 8080,
hot: true,
open: true,
static: {
directory: path.join(__dirname, './'),
watch: true
}
}
}

pacakge.json 中添加 scripts 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"name": "webpack01",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "webpack-dev-server",
"build": "webpack --mode production"
},
"author": "",
"license": "ISC",
"devDependencies": {
"html-webpack-plugin": "~5.5.0",
"vue-loader": "~15.10.0",
"vue-template-compiler": "~2.6.11",
"webpack": "~5.74.0",
"webpack-cli": "~4.10.0",
"webpack-dev-server": "~4.10.0"
},
"dependencies": {
"vue": "~2.6.11"
}
}

这时候就能运行了

1
npm run start

打包

1
npm run build

查看webpack的版本

1
npx webpack --version

Vue Loader简介

https://vue-loader.vuejs.org/zh/guide/#vue-cli

Vue Loader 的配置和其它的 loader 不太一样。

除了通过一条规则将 vue-loader 应用到所有扩展名为 .vue 的文件上之外,请确保在你的 webpack 配置中添加 Vue Loader 的插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// webpack.config.js
const { VueLoaderPlugin } = require('vue-loader')

module.exports = {
module: {
rules: [
// ... 其它规则
{
test: /\.vue$/,
loader: 'vue-loader'
}
]
},
plugins: [
// 请确保引入这个插件!
new VueLoaderPlugin()
]
}

这个插件是必须的!

它的职责是将你定义过的其它规则复制并应用到 .vue 文件里相应语言的块。

例如,如果你有一条匹配 /\.js$/ 的规则,那么它会应用到 .vue 文件里的 <script> 块。

处理HTML

HTML中根据变量取值

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
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
const IS_PRODUCTION = process.env.NODE_ENV === "production";

// 生产配置
const cdn_production = {
js: ["/librarys/vue@2.6.11/vue.min.js"]
};
// 开发配置
const cdn_development = {
js: ["/librarys/vue@2.6.11/vue.js"]
};

module.exports = {

externals: {
BMap: "BMap",
vue: "Vue",
"vue-router": "VueRouter",
vuex: "Vuex",
echarts: "echarts",
axios: "axios",
"view-design": "iview",
mathjs: "math",
xlsx: "XLSX2",
"xlsx-style": "XLSX",
"crypto-js": "CryptoJS",
"v-viewer": "VueViewer",
AgoraRTC_N: "AgoraRTC",
html2canvas: "html2canvas"
},

plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
cdn: IS_PRODUCTION ? cdn_production : cdn_development
}), //JS或者CSS文件可以自动引入到html中
],
}

HTML中取值

1
2
3
4
5
6
<% for (var i in htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.js) { %>
<script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
<% } %>
<script src="/librarys/axios@0.21.1/axios.min.js"></script>
<script src="/librarys/vue-router@3.2.0/vue-router.min.js"></script>
<script src="/librarys/vuex@3.2.0/vuex.min.js"></script>

处理JS

安装babel

添加依赖

1
2
3
4
npm i -D babel-loader@8.2.5 @babel/core@7.18.13
npm i -D @babel/preset-env@7.18.10 @babel/polyfill@7.12.1
npm i -D @babel/plugin-transform-runtime@7.18.10
npm i -S @babel/runtime@7.18.9 @babel/runtime-corejs2@7.18.9

@babel/plugin-transform-runtime有三大作用,其中之一就是自动移除语法转换后内联的辅助函数(inline Babel helpers),使用@babel/runtime/helpers里的辅助函数来替代。这样就减少了我们手动引入的麻烦。

现在我们除了安装@babel/runtime包提供辅助函数模块,还要安装Babel插件@babel/plugin-transform-runtime来自动替换辅助函数。

作用

babel-loader:只是和webpack之间的桥梁,并不会把es6语法进行转换。

@babel/preset-env @babel/polyfill是做转换的。

以上babel的配置是官网提供主要用来解决业务代码js语法转译用的,当要生成类库或者组件库时上面这种配置会污染全局变量,需要使用@babel/plugin-transform-runtime

在根目录下创建 babel 配置文件 .babelrc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"presets": ["@babel/preset-env"],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 2,
"helpers": true,
"regenerator": true,
"useESModules": false
}
]
]
}

注意:"corejs": 2, // 这里设置2是因为上面安装的版本是 @babel/runtime-corejs2

配置webpack.config.js设置使用babel的规则

1
2
3
4
5
6
7
module.exports = {
module: {
rules: [
{ test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" }
]
}
}

缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
{
test: /\.js$/i,
include: resolve('src'),
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: {
cacheDirectory: true // 启用缓存
}
},
]
},

处理CSS

其中less和sass任选其一即可。

处理css文件

添加依赖

1
npm i -D style-loader@3.3.1 css-loader@6.7.1

在webpack.config.js这个配置文件设置匹配css文件处理的插件

1
{ test: /\.css$/, use: ['style-loader', 'css-loader'] },

处理less

添加依赖

1
npm i less-loader@11.0.0 less@4.1.3 -D

在webpack.config.js配置文件设置匹配less文件的处理

1
{ test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader'] },

处理sass

安装sass-loader node-sass工具来处理sass文件

1
2
npm i sass-loader node-sass -D
npm i sass fiber -D

在webpack.config.js配置文件设置匹配scss文件的处理

1
{ test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },

处理URL

图片

安装url-loader

1
npm i url-loader@4.1.1 file-loader@6.2.0 -D

在webpack.config.js中添加处理url路径的loader模块:

1
{test: /\.(jpg|png|gif|bmp|jpeg)$/, use: 'url-loader?esModule=false&limit=500&name=imgs/[hash:8]-[name].[ext]'},

上面这种输入参数的方式还有另一种方式,以对象的键值对方式,如下:

1
2
3
4
5
6
7
8
9
10
11
{
test: /\.(jpg|png|gif|bmp|jpeg|jfif)$/,
use: [{
loader: 'url-loader',
options: {
esModule: false,
limit: 500, //是把小于500B的文件打成Base64的格式,写入JS
name: 'imgs/[hash:8]-[name].[ext]' // [hash:8] 在名称前面设置8位哈希值,[name] 设置文件的原名, [ext] 设置文件的原后缀
}
}]
},// 处理 图片路径的 loader

对比

file-loader 可以指定要复制和放置资源文件的位置,以及如何使用版本哈希命名以获得更好的缓存。此外,这意味着 你可以就近管理图片文件,可以使用相对路径而不用担心部署时 URL 的问题。使用正确的配置,webpack 将会在打包输出中自动重写文件路径为正确的 URL。

url-loader 允许你有条件地将文件转换为内联的 base-64 URL (当文件小于给定的阈值),这会减少小文件的 HTTP 请求数。如果文件大于该阈值,会自动的交给 file-loader 处理。

字体

不要把字体也用url-loader 来处理,把字体文件转成base64是浏览器无法识别的

1
2
3
4
5
6
7
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
loader: 'file-loader',
options: {
esModule: false
}
}

音频

1
2
3
4
5
6
7
8
{
test: /\.(mp3)(\?.*)?$/,
loader: 'url-loader',
options: {
name:'audios/[name].[ext]',
limit:10
}
}

静态文件处理

https://www.webpackjs.com/plugins/copy-webpack-plugin/#install

https://github.com/webpack-contrib/copy-webpack-plugin/tree/v9.1.0

1
npm install copy-webpack-plugin@9 -D

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//webpack.config.js
const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const mCopyWebpackPlugin = new CopyWebpackPlugin({
patterns: [
{
from: "public",
to: "./",
toType: "dir",
globOptions: {
ignore: [
"**/index.html",
],
},
},
],
});

module.exports = {
plugins:[
mCopyWebpackPlugin,
]
}

注意:

版本不同,配置也不一样。

to配置的相对路径是相对于发布目录的。

如果from所在目录中排除文件后没有文件的时候会报错。

我的配置

package.json

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
{
"name": "webpack01",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "webpack-dev-server",
"build": "webpack --mode production"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "~7.18.13",
"@babel/plugin-transform-runtime": "~7.18.10",
"@babel/polyfill": "~7.12.1",
"@babel/preset-env": "~7.18.10",
"babel-loader": "~8.2.5",
"cache-loader": "^4.1.0",
"copy-webpack-plugin": "~9.1.0",
"css-loader": "~6.7.1",
"file-loader": "~6.2.0",
"html-webpack-plugin": "~5.5.0",
"less": "~4.1.3",
"less-loader": "~11.0.0",
"style-loader": "~3.3.1",
"thread-loader": "~3.0.4",
"url-loader": "~4.1.1",
"vue-loader": "~15.10.0",
"vue-template-compiler": "~2.6.11",
"webpack": "~5.74.0",
"webpack-cli": "~4.10.0",
"webpack-dev-server": "~4.10.0",
"webpackbar": "~5.0.2"
},
"dependencies": {
"@babel/runtime": "~7.18.9",
"@babel/runtime-corejs2": "~7.18.9",
"vue": "~2.6.11"
}
}

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
const path = require('path');

function resolve(dir) {
return path.join(__dirname, dir);
}

const IS_PRODUCTION = process.env.NODE_ENV === "production";
const {VueLoaderPlugin} = require('vue-loader')
const HtmlWebpackPlugin = require('html-webpack-plugin');

// 复制插件
const CopyWebpackPlugin = require('copy-webpack-plugin');
const mCopyWebpackPlugin = new CopyWebpackPlugin({
patterns: [
{
from: "public",
to: "./",
toType: "dir",
globOptions: {
ignore: [
"**/index.html",
],
},
},
],
});

// 生产配置
const cdn_production = {
js: ["/librarys/vue@2.6.11/vue.min.js"]
};
// 开发配置
const cdn_development = {
js: ["/librarys/vue@2.6.11/vue.js"]
};

// 进度条
const WebpackBar = require('webpackbar');
let progressPlugin = new WebpackBar({
color: "#85d", // 默认green,进度条颜色支持HEX
basic: false, // 默认true,启用一个简单的日志报告器
profile: false, // 默认false,启用探查器。
})

module.exports = {
mode: 'development',
devtool: 'source-map',
entry: './src/main.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'main.js'
},
externals: {
"vue": "Vue",
"vue-router": "VueRouter",
"vuex": "Vuex",
"axios": "axios",
},
watch: true,
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.js$/, exclude: /node_modules/,
use: [
{
loader: 'thread-loader', // 开启多进程打包
options: {
worker: 3,
}
},
{
loader: 'babel-loader',
options: {
cacheDirectory: true // 启用缓存
}
},
]
},
{test: /\.css$/, use: ['style-loader', 'css-loader']},
{test: /\.less$/, use: ['style-loader', 'cache-loader', 'css-loader', 'less-loader']},
{
test: /\.(jpg|png|gif|bmp|jpeg|svg)$/,
use: 'url-loader?esModule=false&limit=500&name=imgs/[hash:8]-[name].[ext]'
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
loader: 'file-loader',
options: {
esModule: false
}
},
{
test: /\.(mp3)(\?.*)?$/,
loader: 'url-loader',
options: {
name: 'audios/[name].[ext]',
limit: 10
}
}
]
},
plugins: [
new VueLoaderPlugin(),
new HtmlWebpackPlugin({
template: './public/index.html',
cdn: IS_PRODUCTION ? cdn_production : cdn_development
}),
mCopyWebpackPlugin,
progressPlugin,
],
resolve: {
extensions: ['.js', '.css', '.json', '.vue'], //配置后缀名
alias: {
'~': resolve('src'),
'@': resolve('src'),
'components': resolve('src/components'),
}
},
devServer: {
port: 8080,
hot: true,
open: true,
static: {
directory: path.join(__dirname, './'),
watch: true
}
}
}

.babelrc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"presets": ["@babel/preset-env"],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 2,
"helpers": true,
"regenerator": true,
"useESModules": false
}
]
]
}

/public/index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>标题</title>
</head>

<body>
<div id="app"></div>
</body>
<% for (var i in htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.js) { %>
<script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
<% } %>
<script src="/librarys/vue-router@3.2.0/vue-router.min.js"></script>
<script src="/librarys/vuex@3.2.0/vuex.min.js"></script>
<script src="/librarys/axios@0.21.1/axios.min.js"></script>
<style>
body{
margin: 0;
padding: 0;
}
</style>
</html>

/src/App.vue

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
<template>
<div class="app">
<div class="div1">Hello</div>
<div class="div2">Word</div>
<img src="/imgs/qrcode.png" alt="">
<img src="@/assets/imgs/test.png" alt="">
</div>
</template>

<script>
export default {
name: "App"
}
</script>

<style lang="less" scoped>

.app{
background: #f3f3f3;
width: 100vw;
height: 100vh;
font-size: 60px;

.div1{
font-size: 60px;
}

.div2{
font-size: 80px;
}
}
</style>

/src/main.js

1
2
3
4
5
import App from './App';
import Vue from "vue";
new Vue({
render: h => h(App)
}).$mount("#app");

优化

优化构建速度

耗时分析

首先安装一下

1
npm i -D speed-measure-webpack-plugin

修改我们的配置文件 webpack.config.js

1
2
3
4
5
6
7
// 费时分析
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
module.exports = {
plugins: [
new SpeedMeasurePlugin(),
],
}

范围优化

resolve

1、alias

alias 用的创建 importrequire 的别名,用来简化模块引用,项目中基本都需要进行配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const path = require('path')
// 路径处理方法
function resolve(dir){
return path.join(__dirname, dir);
}

module.exports = {
resolve:{
// 配置别名
alias: {
'~': resolve('src'),
'@': resolve('src'),
'components': resolve('src/components'),
}
}
};

配置完成之后,我们在项目中就可以

1
2
3
4
5
6
7
8
// 使用 src 别名 ~ 
import '~/fonts/iconfont.css'

// 使用 src 别名 @
import '@/fonts/iconfont.css'

// 使用 components 别名
import footer from "components/footer";

2、extensions

webpack 默认配置

1
2
3
4
5
module.exports = {
resolve: {
extensions: ['.js', '.css', '.json', '.vue'], //配置后缀名
},
};

如果用户引入模块时不带扩展名,例如

1
import file from '../path/to/file';

那么 webpack 就会按照 extensions 配置的数组从左到右的顺序去尝试解析模块

需要注意的是:

  1. 高频文件后缀名放前面;
  2. 手动配置后,默认配置会被覆盖

如果想保留默认配置,可以用 ... 扩展运算符代表默认配置,例如

1
2
3
4
5
6
module.exports = {
//...
resolve: {
extensions: ['.ts', '...'],
},
};

3、modules

告诉 webpack 解析模块时应该搜索的目录,常见配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
const path = require('path');

// 路径处理方法
function resolve(dir){
return path.join(__dirname, dir);
}

module.exports = {
//...
resolve: {
modules: [resolve('src'), 'node_modules'],
},
};

告诉 webpack 优先 src 目录下查找需要解析的文件,会大大节省查找时间。

4、resolveLoader

resolveLoader 与上面的 resolve 对象的属性集合相同, 但仅用于解析 webpack 的 loader 包。

一般情况下保持默认配置就可以了,但如果你有自定义的 Loader 就需要配置一下,不配可能会因为找不到 loader 报错。例如:我们在 loader 文件夹下面,放着我们自己写的 loader。我们就可以怎么配置

1
2
3
4
5
6
7
8
9
10
11
12
13
const path = require('path');

// 路径处理方法
function resolve(dir){
return path.join(__dirname, dir);
}

module.exports = {
//...
resolveLoader: {
modules: ['node_modules',resolve('loader')]
},
};

externals

externals 配置选项提供了「从输出的 bundle 中排除依赖」的方法。此功能通常对 library 开发人员来说是最有用的,然而也会有各种各样的应用程序用到它。例如,从 CDN 引入 jQuery,而不是把它打包:

1、引入链接

1
2
3
4
5
<script
src="https://code.jquery.com/jquery-3.1.0.js"
integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
crossorigin="anonymous">
</script>

2、配置 externals

1
2
3
4
5
6
module.exports = {
//...
externals: {
jquery: 'jQuery',
},
};

3、使用 jQuery

1
2
3
import $ from 'jquery';

$('.my-element').animate(/* ... */);

我们可以用这样的方法来剥离不需要改动的一些依赖,大大节省打包构建的时间。

缩小范围

在配置 loader 的时候,我们需要更精确的去指定 loader 的作用目录或者需要排除的目录,通过使用 includeexclude 两个配置项,可以实现这个功能,常见的例如:

  • include:符合条件的模块进行解析
  • exclude:排除符合条件的模块,不解析
  • exclude 优先级更高

例如在配置 babel 的时候

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const path = require('path');

// 路径处理方法
function resolve(dir){
return path.join(__dirname, dir);
}

module.exports = {
//...
module: {
noParse: /jquery|lodash/,
rules: [
{
test: /\.js$/i,
include: resolve('src'),
exclude: /node_modules/,
use: [
'babel-loader',
]
},
// ...
]
}
};

noParse

  • 不需要解析依赖的第三方大型类库等,可以通过这个字段进行配置,以提高构建速度
  • 使用 noParse 进行忽略的模块文件中不会解析 importrequire 等语法
1
2
3
4
5
6
7
module.exports = {
//...
module: {
noParse: /jquery|lodash/,
rules:[]
}
};

IgnorePlugin

防止在 importrequire 调用时,生成以下正则表达式匹配的模块:

  • requestRegExp 匹配(test)资源请求路径的正则表达式。
  • contextRegExp 匹配(test)资源上下文(目录)的正则表达式
1
new webpack.IgnorePlugin({ resourceRegExp, contextRegExp });

以下示例演示了此插件的用法。

1、安装 moment 插件(时间处理库)

1
npm i -S moment

2、配置 IgnorePlugin

1
2
3
4
5
6
7
8
9
10
11
// 引入 webpack
const webpack = require('webpack')

module.exports = {
plugins:[ // 配置插件
new webpack.IgnorePlugin({
resourceRegExp: /^\.\/locale$/,
contextRegExp: /moment$/,
}),
]
};

目的是将插件中的非中文语音排除掉,这样就可以大大节省打包的体积了

多进程

配置在 thread-loader 之后的 loader 都会在一个单独的 worker 池(worker pool)中运行

1、安装

1
npm i -D thread-loader@3.0.4

2、配置

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
const path = require('path');

// 路径处理方法
function resolve(dir){
return path.join(__dirname, dir);
}

module.exports = {
//...
module: {
rules: [
{
test: /\.js$/i,
include: resolve('src'),
exclude: /node_modules/,
use: [
{
loader: 'thread-loader', // 开启多进程打包
options: {
worker: 3,
}
},
'babel-loader',
]
},
// ...
]
}
};

缓存

利用缓存可以大幅提升重复构建的速度

JS缓存

babel-loader 开启缓存

  • babel 在转译 js 过程中时间开销比价大,将 babel-loader 的执行结果缓存起来,重新打包的时候,直接读取缓存
  • 缓存位置: node_modules/.cache/babel-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
const path = require('path');

// 路径处理方法
function resolve(dir){
return path.join(__dirname, dir);
}

module.exports = {
module: {
noParse: /jquery|lodash/,
rules: [
{
test: /\.js$/i,
include: resolve('src'),
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: {
cacheDirectory: true // 启用缓存
}
},
]
},
]
}
}

CSS缓存

cache-loader

  • 缓存一些性能开销比较大的 loader 的处理结果
  • 缓存位置:node_modules/.cache/cache-loader

1、安装

1
npm i -D cache-loader@4.1.0

2、配置 cache-loader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module.exports = {
module: {
// ...
rules: [
{
test: /\.(s[ac]|c)ss$/i, //匹配所有的 sass/scss/css 文件
use: [
'style-loader',
'cache-loader', // 获取前面 loader 转换的结果
'css-loader',
'postcss-loader',
'sass-loader',
]
},
]
}
}

less

1
{test: /\.less$/, use: ['style-loader','cache-loader', 'css-loader', 'less-loader']},

其他

hard-source-webpack-plugin

hard-source-webpack-plugin 为模块提供了中间缓存,重复构建时间大约可以减少 80%,但是在 webpack5 中已经内置了模块缓存,不需要再使用此插件

持久化缓存

通过配置cache缓存生成的 webpack 模块和 chunk,来改善构建速度。

1
2
3
4
5
module.exports = {
cache: {
type: 'filesystem',
},
};

优化构建结果

优化构建结果是为了让打包出来的文件尽可能小,这样势必会增加构建时间。

结果分析

借助插件webpack-bundle-analyzer我们可以直观的看到打包结果中,文件的体积大小、各模块依赖关系、文件是够重复等问题,极大的方便我们在进行项目优化的时候,进行问题诊断。

1、安装

1
npm i -D webpack-bundle-analyzer

2、配置插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 引入插件
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

module.exports = {
// ...
plugins:[
// ...
// 配置插件
new BundleAnalyzerPlugin({
// analyzerMode: 'disabled', // 不启动展示打包报告的http服务器
// generateStatsFile: true, // 是否生成stats.json文件
})
],
};

3、修改启动命令

1
2
3
"scripts": {
"analyzer": "cross-env NODE_ENV=prod webpack --progress --mode production"
},

4、执行编译命令 npm run analyzer

打包结束后,会自行启动地址为 http://127.0.0.1:8888 的 web 服务

如果,我们只想保留数据不想启动 web 服务,这个时候,我们可以加上两个配置

1
2
3
4
new BundleAnalyzerPlugin({
analyzerMode: 'disabled', // 不启动展示打包报告的http服务器
generateStatsFile: true, // 是否生成stats.json文件
})

这样再次执行打包的时候就只会产生 state.json 的文件了

压缩 CSS

1、安装 optimize-css-assets-webpack-plugin

1
npm install -D optimize-css-assets-webpack-plugin

2、修改 webapck.config.js 配置

1
2
3
4
5
6
7
8
9
10
11
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')

module.exports = {
optimization: {
minimize: true,
minimizer: [
// 添加 css 压缩配置
new OptimizeCssAssetsPlugin({}),
]
},
}

压缩 JS

在生成环境下打包默认会开启 js 压缩,但是当我们手动配置optimization选项之后,就不再默认对 js 进行压缩,需要我们手动去配置。

因为 webpack5 内置了terser-webpack-plugin插件,所以我们不需重复安装,直接引用就可以了,具体配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
// ...
optimization: {
minimize: true, // 开启最小化
minimizer: [
// ...
new TerserPlugin({})
]
},
// ...
}

清除无用的 CSS

purgecss-webpack-plugin 会单独提取 CSS 并清除用不到的 CSS

1、安装插件

1
$ npm i -D purgecss-webpack-plugin

2、添加配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ...
const PurgecssWebpackPlugin = require('purgecss-webpack-plugin')
const glob = require('glob'); // 文件匹配模式
// ...

function resolve(dir){
return path.join(__dirname, dir);
}

const PATHS = {
src: resolve('src')
}

module.exports = {
plugins:[ // 配置插件
// ...
new PurgecssPlugin({
paths: glob.sync(`${PATHS.src}/**/*`, {nodir: true})
}),
]
}

3、index.html 新增节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ITEM</title>
</head>
<body>
<p></p>
<!-- 使用字体图标文件 -->
<i class="iconfont icon-member"></i>
<div id="imgBox"></div>

<!-- 新增 div,设置 class 为 used -->
<div class="used"></div>
</body>
</html>

4、在 sass.scss 中添加样式

1
2
3
4
5
6
7
8
9
.used {
width: 200px;
height: 200px;
background: #ccc;
}

.unused {
background: chocolate;
}

5、执行一下打包

我们可以看到只有 .used 被保存下来

如何证明是这个插件的作用呢?注释掉再打包就可以看到,.unused 也会被打包进去,由此可证…

Tree-shaking

Tree-shaking 作用是剔除没有使用的代码,以降低包的体积

了解更多 Tree-shaking 知识,推荐阅读 从过去到现在,聊聊 Tree-shaking

webpack5tree-shaking 中的配置

打开项目下 package.json, 加入配置 "sideEffects"

sideEffects 有三种情况

  1. sideEffects:true 所有文件都有副作用,全都不可 tree-shaking
  2. sideEffects:false 有这些文件有副作用,所有其他文件都可以 tree-shaking,但会保留这些文件
  3. sideEffects:[] 部分 tree-shaking , 除了数组外都 tree-shaking

所谓 副作用 指的是 在导入时会执行特殊行为的代码,而不是仅仅暴露一个 export 或多个 export。

举例说明,例如 polyfill,它影响全局作用域,并且通常不提供 export。

对于某些代码,可能没有被导出和使用,但是却不能删除。

因为仅仅是引入这个文件(比如import './index.less' ),或者执行了某个表达式(比如Array.prototype.slice = null),都会对结果造成影响,所以不能被轻易删除。

webpack认为这些代码是有“副作用(Side Effects)”的。

Scope Hoisting

Scope Hoisting 即作用域提升,原理是将多个模块放在同一个作用域下,并重命名防止命名冲突,通过这种方式可以减少函数声明和内存开销

  • webpack 默认支持,在生产环境下默认开启
  • 只支持 es6 代码

优化运行时体验

运行时优化的核心就是提升首屏的加载速度,主要的方式就是:降低首屏加载文件体积,首屏不需要的文件进行预加载或者按需加载

splitChunks 分包配置

optimization.splitChunks 是基于 SplitChunksPlugin 插件实现的。默认情况下,它只会影响到按需加载的 chunks,因为修改 initial chunks 会影响到项目的 HTML 文件中的脚本标签。

webpack 将根据以下条件自动拆分 chunks:

  • 新的 chunk 可以被共享,或者模块来自于 node_modules 文件夹
  • 新的 chunk 体积大于 20kb(在进行 min+gz 之前的体积)
  • 当按需加载 chunks 时,并行请求的最大数量小于或等于 30
  • 当加载初始化页面时,并发请求的最大数量小于或等于 30

1、默认配置介绍

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.exports = {
//...
optimization: {
splitChunks: {
chunks: 'async', // 有效值为 `all`,`async` 和 `initial`
minSize: 20000, // 生成 chunk 的最小体积(≈ 20kb)
minRemainingSize: 0, // 确保拆分后剩余的最小 chunk 体积超过限制来避免大小为零的模块
minChunks: 1, // 拆分前必须共享模块的最小 chunks 数。
maxAsyncRequests: 30, // 最大的按需(异步)加载次数
maxInitialRequests: 30, // 打包后的入口文件加载时,还能同时加载js文件的数量(包括入口文件)
enforceSizeThreshold: 50000,
cacheGroups: { // 配置提取模块的方案
defaultVendors: {
test: /[\/]node_modules[\/]/,
priority: -10,
reuseExistingChunk: true,
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
},
},
};

2、项目中的使用

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
module.exports = {
//...
optimization: {
splitChunks: {
cacheGroups: { // 配置提取模块的方案
default: false,
styles: {
name: 'styles',
test: /\.(s?css|less|sass)$/,
chunks: 'all',
enforce: true,
priority: 10,
},
common: {
name: 'chunk-common',
chunks: 'all',
minChunks: 2,
maxInitialRequests: 5,
minSize: 0,
priority: 1,
enforce: true,
reuseExistingChunk: true,
},
vendors: {
name: 'chunk-vendors',
test: /[\\/]node_modules[\\/]/,
chunks: 'all',
priority: 2,
enforce: true,
reuseExistingChunk: true,
},
// ... 根据不同项目再细化拆分内容
},
},
},
}

代码懒加载

针对首屏加载不太需要的一些资源,我们可以通过懒加载的方式去实现。

下面看一个小需求:点击图片给图片加一个描述

1、新建图片描述信息 desc.js

1
2
3
const ele = document.createElement('div')
ele.innerHTML = '我是图片描述'
module.exports = ele

2、点击图片引入描述 index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import './main.css';
import './sass.scss'
import logo from '../public/avatar.png'

import '@/fonts/iconfont.css'

const a = 'Hello ITEM'
console.log(a)

const img = new Image()
img.src = logo

document.getElementById('imgBox').appendChild(img)

// 按需加载
img.addEventListener('click', () => {
import('./desc').then(({ default: element }) => {
console.log(element)
document.body.appendChild(element)
})
})

prefetch 与 preload

上面我们使用异步加载的方式引入图片的描述,但是如果需要异步加载的文件比较大时,在点击的时候去加载也会影响到我们的体验,这个时候我们就可以考虑使用 prefetch 来进行预拉取

prefetch

prefetch (预获取):浏览器空闲的时候进行资源的拉取

改造一下上面的代码

1
2
3
4
5
6
7
// 按需加载
img.addEventListener('click', () => {
import( /* webpackPrefetch: true */ './desc').then(({ default: element }) => {
console.log(element)
document.body.appendChild(element)
})
})

preload

  • preload (预加载):提前加载后面会用到的关键资源
  • 因为会提前拉取资源,如果不是特殊需要,谨慎使用

官网示例:

1
import(/* webpackPreload: true */ 'ChartingLibrary');

其他插件

构建进度条插件

1
npm i -D webpackbar@5.0.2

配置

1
2
3
4
5
6
7
const WebpackBar = require('webpackbar');
let progressPlugin = new WebpackBar({
color: "#85d", // 默认green,进度条颜色支持HEX
basic: false, // 默认true,启用一个简单的日志报告器
profile:false, // 默认false,启用探查器。
})
plugins.push(progressPlugin)

当然里面还有一个属性就是reporters还没有写上,可以在里面注册事件,也可以理解为各种钩子函数。

如下:

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
{ 
start(context) {
// 在(重新)编译开始时调用
const { start, progress, message, details, request, hasErrors } = context
},
change(context) {
// 在 watch 模式下文件更改时调用
},
update(context) {
// 在每次进度更新后调用
},
done(context) {
// 编译完成时调用
},
progress(context) {
// 构建进度更新时调用
},
allDone(context) {
// 当编译完成时调用
},
beforeAllDone(context) {
// 当编译完成前调用
},
afterAllDone(context) {
// 当编译完成后调用
},
}

当然多数情况下,我们并不会使用这些,基本默认就足够了。

常见问题

IE白板 Chrome正常

这是典型的ES6不支持的情况,最简单的方法是

1
npm install --save babel-polyfill

main.js中添加

1
import 'babel-polyfill';

或者

1
require('babel-polyfill');

详解

Babel默认只转换语法:(默认情况下Babel可以将箭头函数,class等语法转换为ES5兼容的形式)。\

而不转换新的API,如需使用新的API,比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码,还需要使用对应的转换插件或者polyfill

举例来说:

ES6在Array对象上新增了Array.from方法。Babel就不会转码这个方法。如果想让这个方法运行,必须使用babel-polyfill,为当前环境提供一个垫片。