提升开发体验

SourceMap

为什么

开发时我们运行的代码是经过 webpack 编译后的,例如下面这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
/******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ({

/***/ "./node_modules/css-loader/dist/cjs.js!./node_modules/less-loader/dist/cjs.js!./src/less/index.less":
/*!**********************************************************************************************************!*\
!*** ./node_modules/css-loader/dist/cjs.js!./node_modules/less-loader/dist/cjs.js!./src/less/index.less ***!
\**********************************************************************************************************/
/***/ ((module, __webpack_exports__, __webpack_require__) => {

eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var _node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../node_modules/css-loader/dist/runtime/noSourceMaps.js */ \"./node_modules/css-loader/dist/runtime/noSourceMaps.js\");\n/* harmony import */ var _node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../node_modules/css-loader/dist/runtime/api.js */ \"./node_modules/css-loader/dist/runtime/api.js\");\n/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__);\n// Imports\n\n\nvar ___CSS_LOADER_EXPORT___ = _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default()((_node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default()));\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \".box2 {\\n width: 100px;\\n height: 100px;\\n background-color: deeppink;\\n}\\n\", \"\"]);\n// Exports\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (___CSS_LOADER_EXPORT___);\n\n\n//# sourceURL=webpack://webpack5/./src/less/index.less?./node_modules/css-loader/dist/cjs.js!./node_modules/less-loader/dist/cjs.js");

/***/ }),
// 其他省略

所有 css 和 js 合并成了一个文件,并且多了其他代码。此时如果代码运行出错那么提示代码错误位置我们是看不懂的。一旦将来开发代码文件很多,那么很难去发现错误出现在哪里。

所以我们需要更加准确的错误提示,来帮助我们更好的开发代码。

是什么

SourceMap(源代码映射)是一个用来生成源代码与构建后代码一一映射的文件的方案。

它会生成一个 xxx.map 文件,里面包含源代码和构建后代码每一行、每一列的映射关系。当构建后代码出错了,会通过 xxx.map 文件,从构建后代码出错位置找到映射后源代码出错位置,从而让浏览器提示源代码文件出错位置,帮助我们更快的找到错误根源。

怎么用

通过查看Webpack DevTool 文档可知,SourceMap 的值有很多种情况.

但实际开发时我们只需要关注两种情况即可:

  • 开发模式:cheap-module-source-map
    • 优点:打包编译速度快,只包含行映射
    • 缺点:没有列映射
1
2
3
4
5
module.exports = {
// 其他省略
mode: "development",
devtool: "cheap-module-source-map",
};
  • 生产模式:source-map
    • 优点:包含行/列映射
    • 缺点:打包编译速度更慢
1
2
3
4
5
module.exports = {
// 其他省略
mode: "production",
devtool: "source-map",
};

注意:生产模式建议不要用,可能会暴露源代码,而且打包体积也会增大

提升打包构建速度

HotModuleReplacement

为什么

开发时我们修改了其中一个模块代码,Webpack 默认会将所有模块全部重新打包编译,速度很慢。

所以我们需要做到修改某个模块代码,就只有这个模块代码需要重新打包编译,其他模块不变,这样打包速度就能很快。

是什么

HotModuleReplacement(HMR/热模块替换):在程序运行中,替换、添加或删除模块,而无需重新加载整个页面。

怎么用

  1. 基本配置
1
2
3
4
5
6
7
8
9
module.exports = {
// 其他省略
devServer: {
host: "localhost", // 启动服务器域名
port: "3000", // 启动服务器端口号
open: true, // 是否自动打开浏览器
hot: true, // 开启HMR功能(只能用于开发环境,生产环境不需要了)
},
};

此时 css 样式经过 style-loader 处理,已经具备 HMR 功能了。 但是 js 还不行。

  1. JS 配置
1
2
3
4
5
6
7
8
// main.js
// ...

// 判断是否支持HMR功能,并开启
if (module.hot) {
module.hot.accept("./js/count.js");
module.hot.accept("./js/sum.js");
}

上面这样写会很麻烦,所以实际开发我们会使用其他 loader 来解决。

比如:vue-loader, react-hot-loader

OneOf

为什么

打包时每个文件都会经过所有 loader 处理,虽然因为 test 正则原因实际没有处理上,但是都要过一遍。比较慢。

是什么

顾名思义就是只能匹配上一个 loader, 剩下的就不匹配了。

怎么用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module.exports = {
// ...
module: {
rules: [
{
oneOf: [
{
// 用来匹配 .css 结尾的文件
test: /\.css$/,
// use 数组里面 Loader 执行顺序是从右到左
use: ["style-loader", "css-loader"],
},
// ...
],
},
],
},
// ...
};

生产模式也是如此配置。

Include/Exclude

为什么

开发时我们需要使用第三方的库或插件,所有文件都下载到 node_modules 中了。而这些文件是不需要编译可以直接使用的。

所以我们在对 js 文件处理时,要排除 node_modules 下面的文件。

是什么

  • include

包含,只处理 xxx 文件

  • exclude

排除,除了 xxx 文件以外其他文件都处理

怎么用

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
module.exports = {
// ...
module: {
rules: [
{
oneOf: [
// ...
{
test: /\.js$/,
// exclude: /node_modules/, // 排除node_modules代码不编译
include: path.resolve(__dirname, "../src"), // 也可以用包含
loader: "babel-loader",
},
],
},
],
},
plugins: [
new ESLintWebpackPlugin({
// 指定检查文件的根目录
context: path.resolve(__dirname, "../src"),
exclude: "node_modules", // 默认值
}),
// ...
],
// ..
};

生产模式也是如此配置。

Cache

为什么

每次打包时 js 文件都要经过 Eslint 检查 和 Babel 编译,速度比较慢。

我们可以缓存之前的 Eslint 检查 和 Babel 编译结果,这样第二次打包时速度就会更快了。

是什么

对 Eslint 检查 和 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
25
26
27
28
29
30
31
32
33
34
35
36
37
module.exports = {
// ...
module: {
rules: [
{
oneOf: [
// ...
{
test: /\.js$/,
// exclude: /node_modules/, // 排除node_modules代码不编译
include: path.resolve(__dirname, "../src"), // 也可以用包含
loader: "babel-loader",
options: {
cacheDirectory: true, // 开启babel编译缓存
cacheCompression: false, // 缓存文件不要压缩
},
},
],
},
],
},
plugins: [
new ESLintWebpackPlugin({
// 指定检查文件的根目录
context: path.resolve(__dirname, "../src"),
exclude: "node_modules", // 默认值
cache: true, // 开启缓存
// 缓存目录
cacheLocation: path.resolve(
__dirname,
"../node_modules/.cache/.eslintcache"
),
}),
// ...
],
// ...
};

Thead

为什么

当项目越来越庞大时,打包速度越来越慢,甚至于需要一个下午才能打包出来代码。这个速度是比较慢的。

我们想要继续提升打包速度,其实就是要提升 js 的打包速度,因为其他文件都比较少。

而对 js 文件处理主要就是 eslint 、babel、Terser 三个工具,所以我们要提升它们的运行速度。

我们可以开启多进程同时处理 js 文件,这样速度就比之前的单进程打包更快了。

是什么

多进程打包:开启电脑的多个进程同时干一件事,速度更快。

需要注意:请仅在特别耗时的操作中使用,因为每个进程启动就有大约为 600ms 左右开销。

怎么用

我们启动进程的数量就是我们 CPU 的核数。

  1. 如何获取 CPU 的核数,因为每个电脑都不一样。
1
2
3
4
// nodejs核心模块,直接使用
const os = require("os");
// cpu核数
const threads = os.cpus().length;
  1. 下载包
1
npm i thread-loader -D
  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
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
const os = require("os");
const TerserPlugin = require("terser-webpack-plugin");

// cpu核数
const threads = os.cpus().length;

//...

module.exports = {
// ...
module: {
rules: [
{
oneOf: [
// ...
{
test: /\.js$/,
// exclude: /node_modules/, // 排除node_modules代码不编译
include: path.resolve(__dirname, "../src"), // 也可以用包含
use: [
{
loader: "thread-loader", // 开启多进程
options: {
workers: threads, // 数量
},
},
{
loader: "babel-loader",
options: {
cacheDirectory: true, // 开启babel编译缓存
},
},
],
},
],
},
],
},
plugins: [
new ESLintWebpackPlugin({
// 指定检查文件的根目录
context: path.resolve(__dirname, "../src"),
exclude: "node_modules", // 默认值
cache: true, // 开启缓存
// 缓存目录
cacheLocation: path.resolve(
__dirname,
"../node_modules/.cache/.eslintcache"
),
threads, // 开启多进程
}),
// ...
],
optimization: {
minimizer: [
// 当生产模式会默认开启TerserPlugin,但是我们需要进行其他配置,就要重新写了
new TerserPlugin({
parallel: threads // 开启多进程
})
],
},
// ...
};

如果打包的内容很少,因为启动进程开销原因,使用多进程打包实际上会显著的让打包时间变得很长。

减少代码体积

Tree Shaking

为什么

开发时我们定义了一些工具函数库,或者引用第三方工具函数库或组件库。

如果没有特殊处理的话我们打包时会引入整个库,但是实际上可能我们可能只用上极小部分的功能。

这样将整个库都打包进来,体积就太大了。

是什么

Tree Shaking 是一个术语,通常用于描述移除 JavaScript 中的没有使用上的代码。

注意:它依赖 ES Module

怎么用

Webpack 已经默认开启了这个功能,无需其他配置。

Babel

为什么

Babel 为编译的每个文件都插入了辅助代码,使代码体积过大!

Babel 对一些公共方法使用了非常小的辅助代码,比如 _extend。默认情况下会被添加到每一个需要它的文件中。

你可以将这些辅助代码作为一个独立模块,来避免重复引入。

是什么

@babel/plugin-transform-runtime: 禁用了 Babel 自动对每个文件的 runtime 注入,而是引入 @babel/plugin-transform-runtime 并且使所有辅助代码从这里引用。

怎么用

  1. 下载包
1
npm i @babel/plugin-transform-runtime -D
  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
27
28
29
30
31
32
33
34
module.exports = {
// ...
module: {
rules: [
{
oneOf: [
// ...
{
test: /\.js$/,
// exclude: /node_modules/, // 排除node_modules代码不编译
include: path.resolve(__dirname, "../src"), // 也可以用包含
use: [
{
loader: "thread-loader", // 开启多进程
options: {
workers: threads, // 数量
},
},
{
loader: "babel-loader",
options: {
cacheDirectory: true, // 开启babel编译缓存
cacheCompression: false, // 缓存文件不要压缩
plugins: ["@babel/plugin-transform-runtime"], // 减少代码体积
},
},
],
},
],
},
],
},
// ...
};

Image Minimizer

为什么

开发如果项目中引用了较多图片,那么图片体积会比较大,将来请求速度比较慢。

我们可以对图片进行压缩,减少图片体积。

注意:如果项目中图片都是在线链接,那么就不需要了。本地项目静态图片才需要进行压缩。

是什么

image-minimizer-webpack-plugin: 用来压缩图片的插件

怎么用

  1. 下载包
1
npm i image-minimizer-webpack-plugin imagemin -D

还有剩下包需要下载,有两种模式:

  • 无损压缩(有问题建议用cpm下载)
1
npm install imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo -D
  • 有损压缩(有问题建议用cpm下载)
1
npm install imagemin-gifsicle imagemin-mozjpeg imagemin-pngquant imagemin-svgo -D

有损/无损压缩的区别open in new window

  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
27
28
29
30
31
32
33
34
35
36
37
38
39
const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin"); // 压缩图片的插件

module.exports = {
// ...
optimization: {
minimizer: [
// ...
// 压缩图片
new ImageMinimizerPlugin({
minimizer: {
implementation: ImageMinimizerPlugin.imageminGenerate,
options: {
plugins: [
["gifsicle", { interlaced: true }],
["jpegtran", { progressive: true }],
["optipng", { optimizationLevel: 5 }],
[
"svgo",
{
plugins: [
"preset-default",
"prefixIds",
{
name: "sortAttrs",
params: {
xmlnsOrder: "alphabetical",
},
},
],
},
],
],
},
},
}),
],
},
// ..
};
  1. 打包时会出现报错:
1
2
Error: Error with 'src\images\1.jpeg': '"C:\Users\86176\Desktop\webpack\webpack_code\node_modules\jpegtran-bin\vendor\jpegtran.exe"'
Error with 'src\images\3.gif': spawn C:\Users\86176\Desktop\webpack\webpack_code\node_modules\optipng-bin\vendor\optipng.exe ENOENT

我们需要安装两个文件到 node_modules 中才能解决:

  • jpegtran.exe

需要复制到 node_modules\jpegtran-bin\vendor 下面

jpegtran 官网地址

  • optipng.exe

需要复制到 node_modules\optipng-bin\vendor 下面

OptiPNG 官网地址

优化代码运行性能

Code Split

为什么

打包代码时会将所有 js 文件打包到一个文件中,体积太大了。我们如果只要渲染首页,就应该只加载首页的 js 文件,其他文件不应该加载。

所以我们需要将打包生成的文件进行代码分割,生成多个 js 文件,渲染哪个页面就只加载某个 js 文件,这样加载的资源就少,速度就更快。

是什么

代码分割(Code Split)主要做了两件事:

  1. 分割文件:将打包生成的文件进行分割,生成多个 js 文件。
  2. 按需加载:需要哪个文件就加载哪个文件。

怎么用

1. 提取重复代码

如果多入口文件中都引用了同一份代码,我们不希望这份代码被打包到两个文件中,导致代码重复,体积更大。

我们需要提取多入口的重复代码,只打包生成一个 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
module.exports = {
// ...
optimization: {
// 代码分割配置
splitChunks: {
chunks: "all", // 对所有模块都进行分割
// 以下是默认值
// minSize: 20000, // 分割代码最小的大小
// minRemainingSize: 0, // 类似于minSize,最后确保提取的文件大小不能为0
// minChunks: 1, // 至少被引用的次数,满足条件才会代码分割
// maxAsyncRequests: 30, // 按需加载时并行加载的文件的最大数量
// maxInitialRequests: 30, // 入口js文件最大并行请求数量
// enforceSizeThreshold: 50000, // 超过50kb一定会单独打包(此时会忽略minRemainingSize、maxAsyncRequests、maxInitialRequests)
// cacheGroups: { // 组,哪些模块要打包到一个组
// defaultVendors: { // 组名
// test: /[\\/]node_modules[\\/]/, // 需要打包到一起的模块
// priority: -10, // 权重(越大越高)
// reuseExistingChunk: true, // 如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新的模块
// },
// default: { // 其他没有写的配置会使用上面的默认值
// minChunks: 2, // 这里的minChunks权重更大
// priority: -20,
// reuseExistingChunk: true,
// },
// },
// 修改配置
cacheGroups: {
// 组,哪些模块要打包到一个组
// defaultVendors: { // 组名
// test: /[\\/]node_modules[\\/]/, // 需要打包到一起的模块
// priority: -10, // 权重(越大越高)
// reuseExistingChunk: true, // 如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新的模块
// },
default: {
// 其他没有写的配置会使用上面的默认值
minSize: 0, // 我们定义的文件体积太小了,所以要改打包的最小文件体积
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
},
},
};

会发现生成多个 js 文件,其中有一个就是提取的公共模块。

2. 按需加载,动态导入

  • main.js
1
2
3
4
5
6
7
8
9
console.log("hello main");

document.getElementById("btn").onclick = function () {
// 动态导入 --> 实现按需加载
// 即使只被引用了一次,也会代码分割
import("./math.js").then(({ sum }) => {
alert(sum(1, 2, 3, 4, 5));
});
};

通过 import 动态导入语法导入的模块,也会被代码分割,同时也能按需加载了。

3. 更新配置

最终会使用单入口+代码分割+动态导入方式来进行配置。更新之前的配置文件。

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
// ...
optimization: {
// 代码分割配置
splitChunks: {
chunks: "all", // 对所有模块都进行分割
// 其他内容用默认配置即可
},
},
// ...
};

4. 给动态导入文件取名称

  1. 修改文件
  • main.js
1
2
3
4
5
6
7
8
document.getElementById("btn").onClick = function () {
// eslint会对动态导入语法报错,需要修改eslint配置文件
// webpackChunkName: "math":这是webpack动态导入模块命名的方式
// "math"将来就会作为[name]的值显示。
import(/* webpackChunkName: "math" */ "./js/math.js").then(({ count }) => {
console.log(count(2, 1));
});
};
  1. eslint 配置(没问题可以不用配置)
  • 下载包
1
npm i eslint-plugin-import -D
  • 配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// .eslintrc.js
module.exports = {
// 继承 Eslint 规则
extends: ["eslint:recommended"],
env: {
node: true, // 启用node中全局变量
browser: true, // 启用浏览器中全局变量
},
plugins: ["import"], // 解决动态导入import语法报错问题 --> 实际使用eslint-plugin-import的规则解决的
parserOptions: {
ecmaVersion: 6,
sourceType: "module",
},
rules: {
"no-var": 2, // 不能使用 var 定义变量
},
};

5. 统一命名配置

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
// ...
module.exports = {
// ...
output: {
// ...
filename: "static/js/[name].js", // 入口文件打包输出资源命名方式
chunkFilename: "static/js/[name].chunk.js", // 动态导入输出资源命名方式
assetModuleFilename: "static/media/[name].[hash][ext]", // 图片、字体等资源命名方式(注意用hash)
// ...
},
module: {
rules: [
{
oneOf: [
// ...
{
test: /\.(png|jpe?g|gif|svg)$/,
type: "asset",
parser: {
dataUrlCondition: {
maxSize: 10 * 1024, // 小于10kb的图片会被base64处理
},
},
// generator: {
// // 将图片文件输出到 static/imgs 目录中
// // 将图片文件命名 [hash:8][ext][query]
// // [hash:8]: hash值取8位
// // [ext]: 使用之前的文件扩展名
// // [query]: 添加之前的query参数
// filename: "static/imgs/[hash:8][ext][query]",
// },
},
{
test: /\.(ttf|woff2?)$/,
type: "asset/resource",
// generator: {
// filename: "static/media/[hash:8][ext][query]",
// },
},
],
},
],
},
plugins: [
// ...
// 提取css成单独文件
new MiniCssExtractPlugin({
// 定义输出文件名和目录
filename: "static/css/[name].css",
chunkFilename: "static/css/[name].chunk.css",
}),
// css压缩
// new CssMinimizerPlugin(),
],

// ..
};

Preload / Prefetch

为什么

我们前面已经做了代码分割,同时会使用 import 动态导入语法来进行代码按需加载(我们也叫懒加载,比如路由懒加载就是这样实现的)。

但是加载速度还不够好,比如:是用户点击按钮时才加载这个资源的,如果资源体积很大,那么用户会感觉到明显卡顿效果。

我们想在浏览器空闲时间,加载后续需要使用的资源。我们就需要用上 PreloadPrefetch 技术。

是什么

  • Preload:告诉浏览器立即加载资源。
  • Prefetch:告诉浏览器在空闲时才开始加载资源。

它们共同点:

  • 都只会加载资源,并不执行。
  • 都有缓存。

它们区别:

  • Preload加载优先级高,Prefetch加载优先级低。
  • Preload只能加载当前页面需要使用的资源,Prefetch可以加载当前页面资源,也可以加载下一个页面需要使用的资源。

总结:

  • 当前页面优先级高的资源用 Preload 加载。
  • 下一个页面需要使用的资源用 Prefetch 加载。

它们的问题:兼容性较差。

  • 我们可以去 Can I Use 网站查询 API 的兼容性问题。
  • Preload 相对于 Prefetch 兼容性好一点。

怎么用

  1. 下载包
1
npm i @vue/preload-webpack-plugin -D
  1. 配置 webpack.prod.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const PreloadWebpackPlugin = require("@vue/preload-webpack-plugin");

module.exports = {
// ...
plugins: [
// ...
new PreloadWebpackPlugin({
rel: "preload", // preload兼容性更好
as: "script", // 标签优先级
// rel: 'prefetch' // prefetch兼容性更差
}),
],
// ..
};

Network Cache

为什么

将来开发时我们对静态资源会使用缓存来优化,这样浏览器第二次请求资源就能读取缓存了,速度很快。

但是这样的话就会有一个问题, 因为前后输出的文件名是一样的,都叫 main.js,一旦将来发布新版本,因为文件名没有变化导致浏览器会直接读取缓存,不会加载新资源,项目也就没法更新了。

所以我们从文件名入手,确保更新前后文件名不一样,这样就可以做缓存了。

是什么

它们都会生成一个唯一的 hash 值。

  • fullhash(webpack4 是 hash)

每次修改任何一个文件,所有文件名的 hash 至都将改变。所以一旦修改了任何一个文件,整个项目的文件缓存都将失效。

  • chunkhash

根据不同的入口文件(Entry)进行依赖文件解析、构建对应的 chunk,生成对应的哈希值。我们 js 和 css 是同一个引入,会共享一个 hash 值。

  • contenthash

根据文件内容生成 hash 值,只有文件内容变化了,hash 值才会变化。所有文件 hash 值是独享且不同的。

怎么用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ...
module.exports = {
// ...
output: {
// ...
// [contenthash:8]使用contenthash,取8位长度
filename: "static/js/[name].[contenthash:8].js", // 入口文件打包输出资源命名方式
chunkFilename: "static/js/[name].[contenthash:8].chunk.js", // 动态导入输出资源命名方式
// ...
},
// ...
plugins: [
// ...
// 提取css成单独文件
new MiniCssExtractPlugin({
// 定义输出文件名和目录
filename: "static/css/[name].[contenthash:8].css",
chunkFilename: "static/css/[name].[contenthash:8].chunk.css",
}),
// ...
],
// ...
};
  • 问题:

当我们修改 math.js 文件再重新打包的时候,因为 contenthash 原因,math.js 文件 hash 值发生了变化(这是正常的)。

但是 main.js 文件的 hash 值也发生了变化,这会导致 main.js 的缓存失效。明明我们只修改 math.js, 为什么 main.js 也会变身变化呢?

  • 原因:
    • 更新前:math.xxx.js, main.js 引用的 math.xxx.js
    • 更新后:math.yyy.js, main.js 引用的 math.yyy.js, 文件名发生了变化,间接导致 main.js 也发生了变化
  • 解决:

将 hash 值单独保管在一个 runtime 文件中。

我们最终输出三个文件:main、math、runtime。当 math 文件发送变化,变化的是 math 和 runtime 文件,main 不变。

runtime 文件只保存文件的 hash 值和它们与文件关系,整个文件体积就比较小,所以变化重新请求的代价也小。

1
2
3
4
5
6
7
8
9
10
11
12
// ...
module.exports = {
// ...
optimization: {
// ...
// 提取runtime文件
runtimeChunk: {
name: (entrypoint) => `runtime~${entrypoint.name}`, // runtime文件命名规则
},
},
// ...
};

Core-js

为什么

过去我们使用 babel 对 js 代码进行了兼容性处理,其中使用@babel/preset-env 智能预设来处理兼容性问题。

它能将 ES6 的一些语法进行编译转换,比如箭头函数、点点点运算符等。但是如果是 async 函数、promise 对象、数组的一些方法(includes)等,它没办法处理。

所以此时我们 js 代码仍然存在兼容性问题,一旦遇到低版本浏览器会直接报错。所以我们想要将 js 兼容性问题彻底解决

是什么

core-js 是专门用来做 ES6 以及以上 API 的 polyfill

polyfill翻译过来叫做垫片/补丁。就是用社区上提供的一段代码,让我们在不兼容某些新特性的浏览器上,使用该新特性。

怎么用

Eslint 对 Promise 报错

  • 下载包
1
npm i @babel/eslint-parser -D
  • .eslintrc.js
1
2
3
4
5
module.exports = {
// ...
parser: "@babel/eslint-parser", // 支持最新的最终 ECMAScript 标准
// ...
};

使用 core-js 来进行 polyfill,对 Promise 语法编译转换

  1. 使用core-js
  • 下载包
1
npm i core-js
  • 手动全部引入
1
2
// main.js
import "core-js";

这样引入会将所有兼容性代码全部引入,体积太大了。只想引入 promise 的 polyfill

  • 手动按需引入
1
2
// main.js
import "core-js/es/promise";

只引入打包 promise 的 polyfill,打包体积更小。但是将来如果还想使用其他语法,需要手动引入库很麻烦。

  • 自动按需引入
1
2
3
4
5
6
7
8
9
10
11
// babel.config.js
module.exports = {
// 智能预设:能够编译ES6语法
presets: [
[
"@babel/preset-env",
// 按需加载core-js的polyfill
{ useBuiltIns: "usage", corejs: { version: "3", proposals: true } }, // 没作用可以改成3.8或者具体版本号
],
],
};

此时就会自动根据码中使用的语法,来按需加载相应的 polyfill 了。

PWA

为什么

开发 Web App 项目,项目一旦处于网络离线情况,就没法访问了。

我们希望给项目提供离线体验。

是什么

渐进式网络应用程序(progressive web application - PWA):是一种可以提供类似于 native app(原生应用程序) 体验的 Web App 的技术。

其中最重要的是,在 离线(offline) 时应用程序能够继续运行功能。

内部通过 Service Workers 技术实现的。

怎么用

  1. 下载包
1
npm i workbox-webpack-plugin -D
  1. 修改配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const WorkboxPlugin = require("workbox-webpack-plugin");

module.exports = {
// ...
plugins: [
// ...
new WorkboxPlugin.GenerateSW({
// 这些选项帮助快速启用 ServiceWorkers
// 不允许遗留任何“旧的” ServiceWorkers
clientsClaim: true,
skipWaiting: true,
}),
],
// ...
};
  1. 修改 main.js
1
2
3
4
5
6
7
8
9
10
11
12
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
navigator.serviceWorker
.register("/service-worker.js")
.then((registration) => {
console.log("SW registered: ", registration);
})
.catch((registrationError) => {
console.log("SW registration failed: ", registrationError);
});
});
}
  1. 运行指令
1
npm run build

此时如果直接通过 VSCode 访问打包后页面,在浏览器控制台会发现 SW registration failed

因为我们打开的访问路径是:http://127.0.0.1:5500/dist/index.html。此时页面会去请求 service-worker.js 文件,请求路径是:http://127.0.0.1:5500/service-worker.js,这样找不到会 404。

实际 service-worker.js 文件路径是:http://127.0.0.1:5500/dist/service-worker.js

  1. 解决路径问题
  • 下载包
1
npm i serve -g

serve 也是用来启动开发服务器来部署代码查看效果的。

  • 运行指令
1
serve dist

此时通过 serve 启动的服务器 service-worker 就能注册成功了。

总结

从 4 个角度对 webpack 和代码进行了优化:

  1. 提升开发体验
  • 使用 Source Map 让开发或上线时代码报错能有更加准确的错误提示。
  1. 提升 webpack 提升打包构建速度
  • 使用 HotModuleReplacement 让开发时只重新编译打包更新变化了的代码,不变的代码使用缓存,从而使更新速度更快。
  • 使用 OneOf 让资源文件一旦被某个 loader 处理了,就不会继续遍历了,打包速度更快。
  • 使用 Include/Exclude 排除或只检测某些文件,处理的文件更少,速度更快。
  • 使用 Cache 对 eslint 和 babel 处理的结果进行缓存,让第二次打包速度更快。
  • 使用 Thead 多进程处理 eslint 和 babel 任务,速度更快。(需要注意的是,进程启动通信都有开销的,要在比较多代码处理时使用才有效果)
  1. 减少代码体积
  • 使用 Tree Shaking 剔除了没有使用的多余代码,让代码体积更小。
  • 使用 @babel/plugin-transform-runtime 插件对 babel 进行处理,让辅助代码从中引入,而不是每个文件都生成辅助代码,从而体积更小。
  • 使用 Image Minimizer 对项目中图片进行压缩,体积更小,请求速度更快。(需要注意的是,如果项目中图片都是在线链接,那么就不需要了。本地项目静态图片才需要进行压缩。)
  1. 优化代码运行性能
  • 使用 Code Split 对代码进行分割成多个 js 文件,从而使单个文件体积更小,并行加载 js 速度更快。并通过 import 动态导入语法进行按需加载,从而达到需要使用时才加载该资源,不用时不加载资源。
  • 使用 Preload / Prefetch 对代码进行提前加载,等未来需要使用时就能直接使用,从而用户体验更好。
  • 使用 Network Cache 能对输出资源文件进行更好的命名,将来好做缓存,从而用户体验更好。
  • 使用 Core-js 对 js 进行兼容性处理,让我们代码能运行在低版本浏览器。
  • 使用 PWA 能让代码离线也能访问,从而提升用户体验。

全部配置

config/webpack.dev.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
const os = require("os"); // nodejs核心模块,直接使用
const path = require('path') // nodejs核心模块,专门用来处理路径问题
const ESLintPlugin = require('eslint-webpack-plugin'); // 使用 eslint 来查找和修复 JavaScript 代码中的问题的插件
const HtmlWebpackPlugin = require('html-webpack-plugin'); // 生成 HTML 文件的插件

// cpu核数
const threads = os.cpus().length;

module.exports = {
// 入口
// 相对路径和绝对路径都行
entry: './src/main.js', // 相对路径和绝对路径都行
// 输出
output: {
path: undefined,// 开发模式没有输出,不需要指定输出目录
// 输出文件名
filename: 'static/js/[name].js',// 将 js 文件输出到 static/js 目录中
// 给打包输出的其他文件命名,比如动态导入
chunkFilename:'static/js/[name].chunk.js',
// 图片字体等通过type:asset处理资源命名方式
assetModuleFilename:'static/media/[hash:10][ext][query]',
},
// 加载器
module: {
rules: [
// loader的配置
{
// 每个文件只能匹配其中一个loader配置处理
oneOf: [
{
// 用来匹配 .css 结尾的文
test: /\.css$/,
// use 数组里面 Loader 执行顺序是从右到左(从下到上)
use: [
"style-loader", // 将js中css通过创建style标签添加到html文件中生效
"css-loader" // 将css资源编译成commonjs的模块到js中
],
},
{
test: /\.less$/,
use: [
'style-loader',
'css-loader',
'less-loader', // 将less编译成css文件
],
},
{
test: /\.s[ac]ss$/,
use: [
// 将 JS 字符串生成为 style 节点
'style-loader',
// 将 CSS 转化成 CommonJS 模块
'css-loader',
// 将 Sass 编译成 CSS
'sass-loader',
],
},
{
test: /\.styl$/,
use: [
"style-loader",
"css-loader",
"stylus-loader"// 将 Stylus 文件编译为 CSS
],
},
{
test: /\.(png|jpe?g|gif|webp|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
// 小于10kb的图片会转base64
// 优点:减少请求数量 缺点:体积会更大
maxSize: 10 * 1024 // 10kb
}
},
// generator: {
// // 将图片文件输出到 static/imgs 目录中
// // 将图片文件命名 [hash:8][ext][query]
// // [hash:8]: hash值取8位
// // [ext]: 使用之前的文件扩展名
// // [query]: 添加之前的query参数
// filename: 'static/images/[hash:10][ext][query]'
// }
},
{
test: /\.(ttf|woff2?|mp3|mp4|avi)$/,
type: 'asset/resource',
// generator: {
// // 输出名称
// filename: 'static/media/[hash:10][ext][query]'
// }
},
{
test: /\.js$/,
include: path.resolve(__dirname, '../src'), // 只处理src下的文件,其他文件不处理
// exclude: /node_modules/, // 排除node_modules中的js文件(这些文件不处理)
use: [
{
loader: 'thread-loader', // 开启多进程
options: {
workers: threads, // 数量
}
}
,
{
loader: 'babel-loader',
options: {
// presets: ['@babel/preset-env']
cacheDirectory: true, // 开启babel编译缓存
cacheCompression: false, // 缓存文件不要压缩(更占电脑内存些,但是打包速度更快)
plugins: ["@babel/plugin-transform-runtime"], // 减少代码体积
}
}
]
}
]
}
]
},
// 插件
plugins: [
// plugins的配置
new ESLintPlugin({
// 检测哪些文件
context: path.resolve(__dirname, '../src'),
exclude: 'node_modules', // 默认值
cache: true, // 开启缓存
cacheLocation: path.resolve(__dirname, '../node_modules/.cache/eslintcache'),
threads, // 开启多进程和设置进程数量
}),
new HtmlWebpackPlugin({
// 以 public/index.html 为模板创建文件
// 新的html文件有两个特点:1. 内容和源文件一致 2. 自动引入打包生成的js等资源
template: path.resolve(__dirname, '../public/index.html')
})
],
// 开发服务器:不会输出资源,在内存中打包
devServer: {
host: "localhost", // 启动服务器域名
port: "3000", // 启动服务器端口号
open: true, // 是否自动打开浏览器
hot: true, // 开启HMR功能(只能用于开发环境,生产环境不需要了)默认开启
},
// 模式
mode: 'development', // 开发模式
// 源代码映射(调试时方便找到错误位置)
devtool: "cheap-module-source-map", // 打包编译速度快,只包含行映射
}

config/webpack.prod.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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
const os = require("os"); // nodejs核心模块,直接使用
const path = require('path') // nodejs核心模块,专门用来处理路径问题
const ESLintPlugin = require('eslint-webpack-plugin'); // eslint 插件
const HtmlWebpackPlugin = require('html-webpack-plugin'); // 生成 HTML 文件的插件
const MiniCssExtractPlugin = require("mini-css-extract-plugin"); // 将 CSS 提取到单独的文件中
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); // 压缩 CSS
const TerserPlugin = require('terser-webpack-plugin'); // 压缩js
const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin"); // 压缩图片
const PreloadWebpackPlugin = require("@vue/preload-webpack-plugin"); // Preload / Prefetch资源加载插件
const WorkboxPlugin = require('workbox-webpack-plugin'); // 离线应用插件

const threads = os.cpus().length; // cpu核数

// 获取用来处理样式的loader
function getStyleLoader(pre) {
return [
MiniCssExtractPlugin.loader, // 提取 Css 成单独文件
"css-loader", // 将css资源编译成commonjs的模块到js中
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
"postcss-preset-env", // 能解决大多数样式兼容性问题
],
},
},
},
pre
].filter(Boolean)
}

module.exports = {
// 入口
entry: './src/main.js', // 相对路径和绝对路径都行
// 输出
output: {
// 所有文件输出目录,必须是绝对路径
// path.resolve()方法返回一个绝对路径
// __dirname 当前文件的文件夹绝对路径
path: path.resolve(__dirname, '../dist'), // 绝对路径
// 输出文件名
// contenthash 根据文件内容生成 hash 值,只有文件内容变化了,hash 值才会变化。所有文件 hash 值是独享且不同的。
// [contenthash:8]使用contenthash,取8位长度
filename: 'static/js/[name].[contenthash:8].js',
// 给打包输出的其他文件命名,比如动态导入
chunkFilename: 'static/js/[name].chunk.[contenthash:8].js',
// 图片字体等通过type:asset处理资源命名方式(注意用hash)
assetModuleFilename: 'static/media/[hash:8][ext][query]',
// 自动清空上次打包结果
// 过程:在打包前将path整个目录清空,再进行打包
clean: true
},
// 加载器
module: {
rules: [
// loader的配置
{
// 每个文件只能匹配其中一个loader配置处理
oneOf: [
{
// 用来匹配 .css 结尾的文
test: /\.css$/,
// use 数组里面 Loader 执行顺序是从右到左(从下到上)
use: getStyleLoader()
},
{
test: /\.less$/,
use: getStyleLoader('less-loader') // 将less编译成css文件
},
{
test: /\.s[ac]ss$/,
use: getStyleLoader('sass-loader') // 将 Sass 编译成 CSS
},
{
test: /\.styl$/,
use: getStyleLoader('stylus-loader') // 将 Stylus 编译成 CSS
},
{
test: /\.(png|jpe?g|gif|webp|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
// 小于100kb的图片会转base64
// 优点:减少请求数量 缺点:体积会更大
maxSize: 100 * 1024 // 100kb
}
},
// generator: {
// // 将图片文件输出到 static/imgs 目录中
// // 将图片文件命名 [hash:8][ext][query]
// // [hash:8]: hash值取8位
// // [ext]: 使用之前的文件扩展名
// // [query]: 添加之前的query参数
// filename: 'static/images/[hash:8][ext][query]'
// }
},
{
test: /\.(ttf|woff2?|mp3|mp4|avi)$/,
type: 'asset/resource',
// generator: {
// // 输出名称
// filename: 'static/media/[hash:8][ext][query]'
// }
},
{
test: /\.js$/,
include: path.resolve(__dirname, "../src"), // 只处理src下的js文件,其他文件不处理
// exclude: /node_modules/, // 排除node_modules中的js文件(这些文件不处理)
use: [
{
loader: 'thread-loader', // 开启多进程
options: {
workers: threads, // 数量
}
},
{
loader: 'babel-loader',
options: {
// presets: ['@babel/preset-env']
cacheDirectory: true, // 开启babel编译缓存
cacheCompression: false, // 缓存文件不要压缩(更占电脑内存些,但是打包速度更快)
plugins: ["@babel/plugin-transform-runtime"], // 减少代码体积
}
}
]
}
]
}
]
},
// 插件
plugins: [
// plugins的配置
new ESLintPlugin({
// 检测哪些文件
context: path.resolve(__dirname, '../src'),
exclude: 'node_modules', // 默认值 排除node_modules代码不检测
cache: true, // 开启缓存
cacheLocation: path.resolve(__dirname, '../node_modules/.cache/eslintcache'),
threads, // 开启多进程和设置进程数量
}),
new HtmlWebpackPlugin({
// 以 public/index.html 为模板创建文件
// 新的html文件有两个特点:1. 内容和源文件一致 2. 自动引入打包生成的js等资源
template: path.resolve(__dirname, '../public/index.html')
}),
new MiniCssExtractPlugin({
// 提取css到单独的文件中
filename: 'static/css/[name].[contenthash:8].css',
chunkFilename: 'static/css/[name].chunk.[contenthash:8].css'
}),
new PreloadWebpackPlugin({
// rel: 'preload', // 告诉浏览器立即加载资源 兼容性更好
// as: 'script' // 优先级
rel: 'prefetch' // 告诉浏览器在空闲时加载资源 兼容性更差
}),
new WorkboxPlugin.GenerateSW({
// 这些选项帮助快速启用 ServiceWorkers
// 不允许遗留任何“旧的” ServiceWorkers
clientsClaim: true,
skipWaiting: true,
}),
],
// 优化
optimization: {
// 压缩的操作
minimizer: [
// css压缩
new CssMinimizerPlugin(),
// js压缩 当生产模式会默认开启TerserPlugin,但是我们需要进行其他配置,就要重新写了
new TerserPlugin({
parallel: threads // 开启多进程和设置进程数量
}),
// 图片压缩
new ImageMinimizerPlugin({
minimizer: {
implementation: ImageMinimizerPlugin.imageminGenerate,
options: {
plugins: [
["gifsicle", { interlaced: true }],
["jpegtran", { progressive: true }],
["optipng", { optimizationLevel: 5 }],
[
"svgo",
{
plugins: [
"preset-default",
"prefixIds",
{
name: "sortAttrs",
params: {
xmlnsOrder: "alphabetical",
},
},
],
},
],
],
},
},
}),
],
// 代码分割配置
splitChunks: {
chunks: "all", // 对所有模块都进行分割
},
// 缓存依赖的hash值提取
runtimeChunk: {
name: (entrypoint) => `runtime-${entrypoint.name}.js` // runtime文件命名规则
}
},
// 模式
mode: 'production', // 开发模式
// 源代码映射(调试时方便找到错误位置)
devtool: "source-map", // 包含行/列映射,打包编译速度更慢
}

src/main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (module.hot) {// 判断是否支持热模块替换功能,并开启
// 需要开启的模块
module.hot.accept('./js/count.js')
module.hot.accept('./js/sum.js')
}

// 渐进式网络应用程序(progressive web application - PWA) 的js配置
// 在 离线(offline) 时应用程序能够继续运行功能
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js').then(registration => {
console.log('SW registered: ', registration);
}).catch(registrationError => {
console.log('SW registration failed: ', registrationError);
});
});
}

.eslintignore

1
2
3
4
# 忽略vscode eslint插件检查

# 忽略dist目录下所有文件
dist

.eslintrc.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
// eslint 配置文件
module.exports = {
// 继承 Eslint 规则
extends: ["eslint:recommended"],
// parser: "@babel/eslint-parser", // 支持最新的最终 ECMAScript 标准
env: {
node: true, // 启用node中全局变量
browser: true, // 启用浏览器中全局变量
},
parserOptions: {
ecmaVersion: 6,// ES 语法版本
sourceType: "module",// ES 模块化
// ecmaFeatures: { // ES 其他特性
// jsx: true // 如果是 React 项目,就需要开启 jsx 语法
// }
},
// "off" 或 0 - 关闭规则
// "warn" 或 1 - 开启规则,使用警告级别的错误:warn (不会导致程序退出)
// "error" 或 2 - 开启规则,使用错误级别的错误:error (当被触发的时候,程序会退出)
rules: {
"no-var": 2, // 不能使用 var 定义变量
},
plugins:['import'], // 解决动态导入语法报错(貌似已经支持了)
};

babel.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// babel 配置文件
// 主要用于将 ES6 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中

module.exports = {
// 预设(需要下载)
// 简单理解:就是一组 Babel 插件, 扩展 Babel 功能
// @babel/preset-env: 一个智能预设,允许您使用最新的 JavaScript。
// @babel/preset-react:一个用来编译 React jsx 语法的预设
// @babel/preset-typescript:一个用来编译 TypeScript 语法的预设
presets: [
[
'@babel/preset-env',
// 按需加载core-js的polyfill
{
useBuiltIns: 'usage', // 按需加载自动引入
corejs: { version: '3.8', proposals: true } // 3 用不了可以用3.8 或者具体版本号
}
]
]
}

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
{
"name": "webpack_code",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "npm run dev",
"dev": "webpack serve --config ./config/webpack.dev.js",
"build": "webpack --config ./config/webpack.prod.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.20.12",
"@babel/eslint-parser": "^7.19.1",
"@babel/plugin-transform-runtime": "^7.19.6",
"@babel/preset-env": "^7.20.2",
"@vue/preload-webpack-plugin": "^2.0.0",
"babel-loader": "^9.1.2",
"css-loader": "^6.7.3",
"css-minimizer-webpack-plugin": "^4.2.2",
"eslint": "^8.33.0",
"eslint-webpack-plugin": "^3.2.0",
"html-webpack-plugin": "^5.5.0",
"image-minimizer-webpack-plugin": "^3.8.1",
"imagemin": "^8.0.1",
"imagemin-gifsicle": "^7.0.0",
"imagemin-jpegtran": "^7.0.0",
"imagemin-optipng": "^8.0.0",
"imagemin-svgo": "^10.0.1",
"less": "^4.1.3",
"less-loader": "^11.1.0",
"mini-css-extract-plugin": "^2.7.2",
"postcss": "^8.4.21",
"postcss-loader": "^7.0.2",
"postcss-preset-env": "^8.0.1",
"sass": "^1.57.1",
"sass-loader": "^13.2.0",
"style-loader": "^3.3.1",
"stylus-loader": "^7.1.0",
"thread-loader": "^3.0.4",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.11.1",
"workbox-webpack-plugin": "^6.5.4"
},
"browserslist": [
"last 2 version",
"> 1%",
"not dead"
],
"dependencies": {
"core-js": "^3.27.2"
}
}

高级

webpack高级