尚硅谷Webpack5新版视频教程
B站直达:https://www.bilibili.com/video/BV14T4y1z7sw
百度网盘:https://pan.baidu.com/s/114lJRGua2uHBdLq_iVLOOQ 提取码:yyds
阿里云盘:https://www.aliyundrive.com/s/UMkmCzdWsGh(教程配套资料请从百度网盘下载)
围观尚硅谷前端课程:http://www.atguigu.com/web
更多尚硅谷视频教程请访问:http://www.atguigu.com/download.shtml
在线课程地址:https://yk2012.github.io/sgg_webpack5/
Webpack5中文文档:Loaders | webpack 中文文档 (docschina.org)
1. 简单使用
1.1 搭建项目

1.2 编写JS代码
count.js
1 2 3
| export default function count(a,b){ return a + b }
|
sum.js
1 2 3
| export default function sum(...args){ return args.reduce((a,b)=>a+b,0) }
|
main.js
1 2 3 4 5
| import count from "./js/count"; import sum from "./js/sum";
console.log(count(1,2)) console.log(sum(1,2,3,4))
|
index.html
1 2 3 4 5 6 7 8 9 10 11
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>Hello World</h1> </body> <script src="./src/main.js"></script> </html>
|
1.3 运行代码

可以发现浏览器报错。默认不支持ES module语法
1.4 初始化依赖
会自动生成 package.json

继续安装依赖
1
| npm i webpack webpack-cli -D
|
1.5 启动webpack
开发环境构建
1
| npx webpack ./src/main.js --mode=development
|
生产环境构建
1
| npx webpack ./src/main.js --mode=production
|
修改 main.js 文件的引用地址。浏览器正常打印
2. 基本配置
2.1 5大核心概念
- entry(入口)
指示 Webpack 从哪个文件开始打包
- output(输出)
指示 Webpack 打包完的文件输出到哪里去,如何命名等
- loader(加载器)
webpack 本身只能处理 js、json 等资源,其他资源需要借助 loader,Webpack 才能解析
- plugins(插件)
扩展 Webpack 的功能
- mode(模式)
主要由两种模式:
- 开发模式:development
- 生产模式:production
2.2 基础配置文件
在项目根目录新建文件 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
| const path = require("path"); module.exports = { entry: "./src/main.js", output: { path: path.resolve(__dirname,"dist"), filename: "main.js" }, module: { rules: [ ] }, plugins: [ ], mode: "development" }
|
有了这个配置文件,我们再进行打包时只需输入如下命令即可
这个命令会自动找根目录中的 webpa.config.js 文件中的配置进行打包
3. 处理样式资源
3.1 处理CSS资源
webpack默认只支持JS文件,我们编写CSS后打包会出错,我们可以通过添加loader来处理CSS资源
更多loader可以通过webpack官方文档来查看
当我们在 main.js 中引入 css 文件后,执行打包会出现下面的错误

安装 loader
1
| npm i css-loader style-loader -D
|
然后配置规则
1 2 3 4 5 6 7 8 9 10 11 12 13
| module.exports = { module: { rules: [ { test: /\.css$/, use: [ 'style-loader', 'css-loader', ], }, ], }, };
|
此时再次执行打包即可
3.2 处理Less资源
安装
1
| npm install less less-loader --save-dev
|
配置规则
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| module.exports = { module: { rules: [ { test: /\.less$/i, use: [ 'style-loader', 'css-loader', 'less-loader', ], }, ], }, };
|
3.3 处理Scss资源
安装
1
| npm install sass-loader sass --save-dev
|
配置规则
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| module.exports = { module: { rules: [ { test: /\.s[ac]ss$/i, use: [ 'style-loader', 'css-loader', 'sass-loader', ], }, ], }, };
|
4. 处理图片资源
4.1 图片转Base64
我们可以通过配置,将小于一定大小的文件转成base64处理,从而减少请求
添加以下配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| module.exports = { module: { rules: [ { test: /\.(png|jpe?g|gif|webp)$/, type: "asset", parser: { dataUrlCondition: { maxSize: 10 * 1024 } } }, ], }, };
|
转成base64后请求不会耗时间

5. 修改文件输出地址
目前我们打包生成的文件都在一个文件下

我想让他变成 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 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| const path = require("path"); module.exports = { // 入口文件,相对路径 entry: "./src/main.js", // 输出 output: { // 要输出到的位置,绝对路径 path: path.resolve(__dirname,"dist"), // 文件名 + filename: "js/main.js", }, // 加载器 module: { rules: [ { test: /\.(png|jpe?g|gif|webp)$/, type: "asset", parser: { dataUrlCondition: { maxSize: 18 * 1024 // 小于10kb的图片会被base64处理 } }, + generator: { + // 修改图片文件的输出地址 + // hash:10 只取10位哈希值 + // ext图片后缀名 + // query 图片文件后面携带的参数 + filename: 'images/[hash:10][ext][query]' + } }, ] }, // 插件 plugins: [ // plugins的配置 ], // 模式 mode: "development" }
|
再次打包看效果

6. 自动清空上次打包文件
在 output 下新增 clean 属性即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| const path = require("path");
module.exports = { entry: "./src/main.js", output: { path: path.resolve(__dirname,"dist"), filename: "js/main.js", clean: true }, }
|
7. 处理字体图标资源
我们可以在 阿里巴巴矢量图标库 中找一些图标资源,然后加入到项目中,点击下载到本地

使用第二种方式引入

在项目中导入下载下来的 css 和图标文件

在 main.js 中引入 iconfont.css
然后配置规则,将字体图标打包输出到 font 文件夹中
1 2 3 4 5 6 7 8 9 10 11 12 13
| module.exports = { module: { rules: [ { test: /\.(ttf|woff2?)$/, type: "asset/resource", generator: { filename: 'font/[hash:10][ext][query]' } }, ] }, }
|
然后再页面中可以通过下面的方式使用
1 2 3 4
| <span class="iconfont icon-aixin" style="font-size: 30px;color: orangered"></span> <span class="iconfont icon-bianji"></span> <span class="iconfont icon-dianzan"></span> <span class="iconfont icon-dingwei"></span>
|

8. 处理其他资源
其他资源我们直接在下面的规则中添加对应的文件后缀即可,webpack 会原封不动的将文件输出到指定目录
1 2 3 4 5 6 7 8 9 10 11 12 13
| module.exports = { module: { rules: [ { test: /\.(ttf|woff2?|mp3|mp4|word)$/, type: "asset/resource", generator: { filename: 'font/[hash:10][ext][query]' } }, ] }, }
|
9. 处理JS文件
9.1 Eslint 插件
可组装的 JavaScript 和 JSX 检查工具。
这句话意思就是:它是用来检测 js 和 jsx 语法的工具,可以配置各项功能
我们使用 Eslint,关键是写 Eslint 配置文件,里面写上各种 rules 规则,将来运行 Eslint 时就会以写的规则对代码进行检查
配置文件的写法
配置文件有很多种写法:
ESLint 会查找和自动读取它们,所以以上配置文件只需要存在一个即可
具体配置
以 .eslintrc.js
为例
1 2 3 4 5 6 7 8 9 10
| module.exports = { parserOptions: {}, rules: {}, extends: [], };
|
parserOptions 解析选项
1 2 3 4 5 6 7
| parserOptions: { ecmaVersion: 6, sourceType: "module", ecmaFeatures: { jsx: true } }
|
rules 具体规则
"off"
或 0
- 关闭规则
"warn"
或 1
- 开启规则,使用警告级别的错误:warn
(不会导致程序退出)
"error"
或 2
- 开启规则,使用错误级别的错误:error
(当被触发的时候,程序会退出)
1 2 3 4 5 6 7 8 9 10 11 12
| rules: { semi: "error", 'array-callback-return': 'warn', 'default-case': [ 'warn', { commentPattern: '^no default$' } ], eqeqeq: [ 'warn', 'smart' ], }
|
更多规则详见:规则文档
extends 继承
开发中一点点写 rules 规则太费劲了,所以有更好的办法,继承现有的规则。
现有以下较为有名的规则:
1 2 3 4 5 6 7 8
| module.exports = { extends: ["eslint:recommended"], rules: { eqeqeq: ["warn", "smart"], }, };
|
Esline在Webpack中的使用
安装
1
| npm install eslint-webpack-plugin eslint --save-dev
|
然后把插件添加到你的 webpack 配置。例如:
1 2 3 4 5 6 7 8 9 10 11 12
| const ESLintPlugin = require('eslint-webpack-plugin');
module.exports = { plugins: [ new ESLintPlugin({ context:path.resolve(__dirname, "src"), }) ], };
|
在根目录新建 .eslintrc.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| module.exports = { extends: ["eslint:recommended"], env: { node: true, browser: true, }, parserOptions: { ecmaVersion: 6, sourceType: "module", }, rules: { "no-var": 2, }, };
|
我们编写一个错误的代码

在打包时会提示错误,并终止打包

在 webstorm 中可以开启 eslint 检测

这样在编写代码时就可以发现错误
添加eslint忽略文件
新建 .eslintignore
文件
9.2 Babel 插件
JavaScript 编译器。
主要用于将 ES6 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中
配置文件
配置文件由很多种写法:
Babel 会查找和自动读取它们,所以以上配置文件只需要存在一个即可
配置示例
我们以 babel.config.js
配置文件为例:
1 2 3 4
| module.exports = { presets: ["@babel/preset-env"], };
|
presets 预设
简单理解:就是一组 Babel 插件, 扩展 Babel 功能
@babel/preset-env
: 一个智能预设,允许您使用最新的 JavaScript。
@babel/preset-react
:一个用来编译 React jsx 语法的预设
@babel/preset-typescript
:一个用来编译 TypeScript 语法的预设
在webpack中使用
安装
1
| npm install -D babel-loader @babel/core @babel/preset-env
|
用法一:
在 webpack 配置对象中,需要将 babel-loader 添加到 module 列表中,就像下面这样
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| module: { rules: [ { test: /\.m?js$/, exclude: /(node_modules|bower_components)/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } } } ] }
|
用法二(推荐用法):
在项目根目录新建 babel.config.js
1 2 3
| module.exports = { presets: ['@babel/preset-env'] }
|
然后在 webpack 配置对象中添加
1 2 3 4 5 6 7 8 9 10 11
| module: { rules: [ { test: /\.m?js$/, exclude: /(node_modules|bower_components)/, use: { loader: 'babel-loader', } } ] }
|
打包前生成的文件代码

打包后的文件代码

修改输出的结果不以箭头输出
在 output 中 添加 environment 配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| output: { path: path.resolve(__dirname,"dist"), filename: "js/main.js", clean: true, environment: { arrowFunction: false } },
|
10. 处理HTML文件
我们希望每次打包后可以为我们自动的引入JS,通过如下方式设置
安装
1
| npm install --save-dev html-webpack-plugin
|
引入和使用
1 2 3 4 5 6 7 8 9 10 11 12 13
| const HtmlWebpackPlugin = require('html-webpack-plugin'); const path = require('path');
module.exports = { plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname, "index.html"), title: "学习Webpack", hash:true, }) ], };
|
设置了 title 后需要在模板中修改 title 的设置方式,需要动态获取
<title><%= htmlWebpackPlugin.options.title %></title>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title><%= htmlWebpackPlugin.options.title %></title> </head> <body> <span class="iconfont icon-aixin" style="font-size: 30px;color: orangered"></span> <span class="iconfont icon-bianji"></span> <span class="iconfont icon-dianzan"></span> <span class="iconfont icon-dingwei"></span> <div class="box"></div> <div class="box1"></div> <div class="box2"> <span class="desc">哈哈</span> </div> <h1>Hello World</h1> </body> </html>
|
更多 options 配置查看文档:https://github.com/jantimon/html-webpack-plugin#options
打包后生成的文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>学习Webpack</title> <script defer src="js/main.js?15bb642196d30b484393"></script></head> <body> <span class="iconfont icon-aixin" style="font-size: 30px;color: orangered"></span> <span class="iconfont icon-bianji"></span> <span class="iconfont icon-dianzan"></span> <span class="iconfont icon-dingwei"></span> <div class="box"></div> <div class="box1"></div> <div class="box2"> <span class="desc">哈哈</span> </div> <h1>Hello World</h1> </body> </html>
|
11. 搭建开发服务器
我们次每次修改完代码后都需要重新打包,然后运行html文件才能看到效果,我们可以通过搭建开发服务器,可以实时的查看改动
安装
1
| npm i webpack-dev-server -D
|
添加配置
1 2 3 4 5 6
| devServer: { host: "localhost", port: "3000", open: true, },
|
然后通过如下命令启动
启动成功后光标会停留在下面,同时自动打开浏览器,我们修改代码后刷新浏览器会自动生效

12. 生产环境准备工作
根目录新建 config 文件,将原来的 webpck.config.js 放在 config 文件中,并复制一份配置
修改两个配置文件名分别为
- webpack.dev.js
- webpack.prod.js
然后再所有的相对路径前添加 ../
往外跳一层
删除 dev 配置文件中的 output 配置,因为dev环境不需要打包,删除 prod 配置中的 devServer 配置
设置好的配置文件分别如下
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
| const path = require("path"); const ESLintPlugin = require('eslint-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = { entry: "./src/main.js", module: { rules: [ { test: /\.css$/, use: ["style-loader", "css-loader"], }, { test: /\.less$/i, use: [ 'style-loader', 'css-loader', 'less-loader', ], }, { test: /\.s[ac]ss$/i, use: [ 'style-loader', 'css-loader', 'sass-loader', ], }, { test: /\.(png|jpe?g|gif|webp)$/, type: "asset", parser: { dataUrlCondition: { maxSize: 18 * 1024 } }, generator: { filename: 'images/[hash:10][ext][query]' } }, { test: /\.(ttf|woff2?|mp3|mp4|word)$/, type: "asset/resource", generator: { filename: 'font/[hash:10][ext][query]' } }, { test: /\.m?js$/, exclude: /(node_modules|bower_components)/, use: { loader: 'babel-loader', } } ] }, plugins: [ new ESLintPlugin({ context:path.resolve(__dirname, "../src"), }), new HtmlWebpackPlugin({ template: path.resolve(__dirname, "../index.html"), title: "学习Webpack", hash:true, }) ], mode: "development", devServer: { host: "localhost", port: "3000", open: true, liveReload:true, }, }
|
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
| const path = require("path"); const ESLintPlugin = require('eslint-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = { entry: "./src/main.js", output: { path: path.resolve(__dirname,"../dist"), filename: "js/main.js", clean: true, environment: { arrowFunction: false } },
module: { rules: [ { test: /\.css$/, use: ["style-loader", "css-loader"], }, { test: /\.less$/i, use: [ 'style-loader', 'css-loader', 'less-loader', ], }, { test: /\.s[ac]ss$/i, use: [ 'style-loader', 'css-loader', 'sass-loader', ], }, { test: /\.(png|jpe?g|gif|webp)$/, type: "asset", parser: { dataUrlCondition: { maxSize: 18 * 1024 } }, generator: { filename: 'images/[hash:10][ext][query]' } }, { test: /\.(ttf|woff2?|mp3|mp4|word)$/, type: "asset/resource", generator: { filename: 'font/[hash:10][ext][query]' } }, { test: /\.m?js$/, exclude: /(node_modules|bower_components)/, use: { loader: 'babel-loader', } } ] }, plugins: [ new ESLintPlugin({ context:path.resolve(__dirname, "../src"), }), new HtmlWebpackPlugin({ template: path.resolve(__dirname, "../index.html"), title: "学习Webpack", hash:true, }) ], mode: "production", }
|
然后修改 package.json 文件,设置启动命令和打包命令
1 2 3 4
| "scripts": { "dev": "webpack serve --config ./config/webpack.dev.js", "build": "webpack --config ./config/webpack.prod.js" }
|
13. CSS处理
13.1 提取CSS成单独文件
安装
1
| npm install --save-dev mini-css-extract-plugin
|
需要将原来的 style-loader 换成 MiniCssExtractPlugin.loader,并在 plugins 使用插件
1 2 3 4 5 6 7 8 9 10 11 12 13
| module.exports = { plugins: [new MiniCssExtractPlugin({ filename:"state/css/main.css" })], module: { rules: [ { test: /\.css$/i, use: [MiniCssExtractPlugin.loader, "css-loader"], }, ], }, };
|
然后配合上面的 html-webpack-plugin 插件,可以自动完成资源引入
打包后生成的目录如下

13.2 处理CSS兼容性问题
有些CSS样式在部分浏览器中可能会存在兼容性问题,我们通过配置可以来处理这部分的兼容问题
安装
1
| npm i postcss-loader postcss postcss-preset-env -D
|
添加配置。这个配置必须放在 css-loader 后面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| rules:[ { test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader", { loader: "postcss-loader", options: { postcssOptions: { plugins: [ "postcss-preset-env", ], }, }, }], }, ]
|
在 package.json 文件中添加配置
1 2 3
| "browserslist": [ "ie >= 6" ]
|
这个意思表示兼容到 ie6
我们尝试在代码中添加如下样式
1 2 3 4 5 6 7 8 9 10 11
| .box{ width: 200px; height: 200px; background-color: pink; background-image: url("../images/1.png"); transition: 0.5s; } .box:hover{ transform: scale(1.2); transition: 0.5s; }
|
然后执行打包命令,查看打包后的代码
1 2 3 4 5 6 7 8 9 10 11 12
| .box{ width: 200px; height: 200px; background-color: pink; background-image: url(../../state/images/626c3888ec.png); transition: 0.5s; } .box:hover{ -ms-transform: scale(1.2); transform: scale(1.2); transition: 0.5s; }
|
可以看到加了一些兼容性的代码
但是通常情况下我们一般不考虑旧版本浏览器了,所以我们可以这样设置:
1 2 3 4 5
| "browserslist": [ "last 2 version", "> 1%", "not dead" ]
|
last 2 version
只考虑浏览器最近的两个版本
> 1%
覆盖99%的浏览器
not dead
不考虑已经死掉的版本
上面三个配置取交集做兼容处理
13.3 提取重复配置
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
| const path = require("path"); const ESLintPlugin = require('eslint-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin'); const MiniCssExtractPlugin = require("mini-css-extract-plugin");
function myCssLoader(preProcessor){ return[ MiniCssExtractPlugin.loader, "css-loader", { loader: "postcss-loader", options: { postcssOptions: { plugins: [ "postcss-preset-env", ], }, }, }, preProcessor ].filter(Boolean) }
module.exports = { entry: "./src/main.js", output: { path: path.resolve(__dirname,"../dist"), filename: "state/js/main.js", clean: true, environment: { arrowFunction: false } },
module: { rules: [ { test: /\.css$/, use: myCssLoader(), }, { test: /\.less$/i, use: myCssLoader('less-loader') }, { test: /\.s[ac]ss$/i, use: myCssLoader('sass-loader') }, { test: /\.(png|jpe?g|gif|webp)$/, type: "asset", parser: { dataUrlCondition: { maxSize: 18 * 1024 } }, generator: { filename: 'state/images/[hash:10][ext][query]' } }, { test: /\.(ttf|woff2?|mp3|mp4|word)$/, type: "asset/resource", generator: { filename: 'state/resource/[hash:10][ext][query]' } }, { test: /\.m?js$/, exclude: /(node_modules|bower_components)/, use: { loader: 'babel-loader', } } ] }, plugins: [ new ESLintPlugin({ context:path.resolve(__dirname, "../src"), }), new HtmlWebpackPlugin({ template: path.resolve(__dirname, "../index.html"), title: "学习Webpack", hash:true, }), new MiniCssExtractPlugin({ filename:"state/css/main.css" }) ], mode: "production", }
|
13.4 压缩CSS
下载依赖
1
| const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
|
1 2 3 4
| plugins:[ new CssMinimizerPlugin() ]
|
打包看效果

14. 高级优化
14.1 提升开发体验SourceMap
开发时我们运行的代码是经过 webpack 编译后的,例如下面这个样子:

如果代码在某一行出现问题,我们调试起来很不方便,但是通过 SourceMap 会创建一种映射,我们可以直接定位到源代码,从而快速方便的查看是哪一行代码出问题
通过查看Webpack DevTool 文档open in new window可知,SourceMap 的值有很多种情况.
但实际开发时我们只需要关注两种情况即可:
- 开发模式:
cheap-module-source-map
- 优点:打包编译速度快,只包含行映射
- 缺点:没有列映射
1 2 3 4 5
| module.exports = { mode: "development", devtool: "cheap-module-source-map", };
|
1 2 3 4 5
| module.exports = { mode: "production", devtool: "source-map", };
|
14.2 Hot热替换
为什么
开发时我们修改了其中一个模块代码,Webpack 默认会将所有模块全部重新打包编译,速度很慢。
所以我们需要做到修改某个模块代码,就只有这个模块代码需要重新打包编译,其他模块不变,这样打包速度就能很快。
是什么
HotModuleReplacement(HMR/热模块替换):在程序运行中,替换、添加或删除模块,而无需重新加载整个页面。
怎么用
- 基本配置
1 2 3 4 5 6 7 8 9
| module.exports = { devServer: { host: "localhost", port: "3000", open: true, hot: true, }, };
|
此时 css 样式经过 style-loader 处理,已经具备 HMR 功能了。 但是 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 24 25 26 27 28
| import count from "./js/count"; import sum from "./js/sum";
import "./css/iconfont.css"; import "./css/index.css"; import "./less/index.less"; import "./sass/index.sass"; import "./sass/index.scss"; import "./styl/index.styl";
const result1 = count(2, 1); console.log(result1); const result2 = sum(1, 2, 3, 4); console.log(result2);
if (module.hot) { module.hot.accept("./js/count.js", function (count) { const result1 = count(2, 1); console.log(result1); });
module.hot.accept("./js/sum.js", function (sum) { const result2 = sum(1, 2, 3, 4); console.log(result2); }); }
|
上面这样写会很麻烦,所以实际开发我们会使用其他 loader 来解决。
比如:vue-loaderopen in new window, react-hot-loaderopen in new window
14.3 oneOf
为什么
打包时每个文件都会经过所有 loader 处理,虽然因为 test
正则原因实际没有处理上,但是都要过一遍。比较慢。
是什么
顾名思义就是只能匹配上一个 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 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
| const path = require("path"); const ESLintPlugin = require('eslint-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin'); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
function myCssLoader(preProcessor){ return[ MiniCssExtractPlugin.loader, "css-loader", { loader: "postcss-loader", options: { postcssOptions: { plugins: [ "postcss-preset-env", ], }, }, }, preProcessor ].filter(Boolean) }
module.exports = { entry: "./src/main.js", output: { path: path.resolve(__dirname,"../dist"), filename: "state/js/main.js", clean: true, environment: { arrowFunction: false } },
module: { rules: [ { oneOf: [ { test: /\.css$/, use: myCssLoader(), }, { test: /\.less$/i, use: myCssLoader('less-loader') }, { test: /\.s[ac]ss$/i, use: myCssLoader('sass-loader') }, { test: /\.(png|jpe?g|gif|webp)$/, type: "asset", parser: { dataUrlCondition: { maxSize: 18 * 1024 } }, generator: { filename: 'state/images/[hash:10][ext][query]' } }, { test: /\.(ttf|woff2?|mp3|mp4|word)$/, type: "asset/resource", generator: { filename: 'state/resource/[hash:10][ext][query]' } }, { test: /\.m?js$/, exclude: /(node_modules|bower_components)/, use: { loader: 'babel-loader', } } ] } ] }, plugins: [ new ESLintPlugin({ context:path.resolve(__dirname, "../src"), }), new HtmlWebpackPlugin({ template: path.resolve(__dirname, "../index.html"), title: "学习Webpack", hash:true, }), new MiniCssExtractPlugin({ filename:"state/css/main.css" }), new CssMinimizerPlugin() ], mode: "production", devtool: "source-map" }
|
14.4 Include/Exclude
为什么
开发时我们需要使用第三方的库或插件,所有文件都下载到 node_modules 中了。而这些文件是不需要编译可以直接使用的。
所以我们在对 js 文件处理时,要排除 node_modules 下面的文件。
是什么
包含,只处理 xxx 文件
排除,除了 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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| const path = require("path"); const ESLintPlugin = require('eslint-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = { entry: "./src/main.js", module: { rules: [ { test: /\.m?js$/, include: path.resolve(__dirname,"../src"), use: { loader: 'babel-loader', } } ] }, plugins: [ new ESLintPlugin({ context:path.resolve(__dirname, "../src"), exclude:["node_modules"] }), new HtmlWebpackPlugin({ template: path.resolve(__dirname, "../index.html"), title: "学习Webpack", hash:true, }) ], mode: "development", devtool: "cheap-module-source-map", devServer: { host: "localhost", port: "3000", open: true, hot:true, }, }
|
14.5 Catch
为什么
每次打包时 js 文件都要经过 Eslint 检查 和 Babel 编译,速度比较慢。
我们可以缓存之前的 Eslint 检查 和 Babel 编译结果,这样第二次打包时速度就会更快了
怎么用
1 2 3 4 5 6 7 8 9 10 11 12 13
| rules:[ { test: /\.m?js$/, exclude: /(node_modules|bower_components)/, use: { loader: 'babel-loader', options: { cacheDirectory: true, cacheCompression: false, } } } ]
|
eslint-webpack-plugin
默认已经开启了缓存
打包后查看 node_modules/.catch

14.6 开启多线程打包
为什么
当我们项目文件足够大,数量足够多时,执行打包操作会耗时很长,这时我们就可以通过设置开启多线程打包提高打包速度
安装
1
| cnpm install terser-webpack-plugin thread-loader --save-dev
|
terser-webpack-plugin
在webpack5中默认自带,但是我们要修改默认配置开启多线程打包,所以也要重新安装一下
TerserWebpackPlugin | webpack 中文文档 (docschina.org)
thread-loader
是用来开启多线程,放在需要开启多线程的loader前面
thread-loader | webpack 中文文档 (docschina.org)
开启的线程数默认是:cpu核心数 -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 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
| const path = require("path");
const ESLintPlugin = require('eslint-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin'); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); const TerserPlugin = require("terser-webpack-plugin");
function myCssLoader(preProcessor){ return[ MiniCssExtractPlugin.loader, "css-loader", { loader: "postcss-loader", options: { postcssOptions: { plugins: [ "postcss-preset-env", ], }, }, }, preProcessor ].filter(Boolean) }
module.exports = { entry: "./src/main.js", output: { path: path.resolve(__dirname,"../dist"), filename: "state/js/main.js", clean: true, environment: { arrowFunction: false } },
module: { rules: [ { oneOf: [ { test: /\.css$/, use: myCssLoader(), }, { test: /\.less$/i, use: myCssLoader('less-loader') }, { test: /\.s[ac]ss$/i, use: myCssLoader('sass-loader') }, { test: /\.(png|jpe?g|gif|webp)$/, type: "asset", parser: { dataUrlCondition: { maxSize: 18 * 1024 } }, generator: { filename: 'state/images/[hash:10][ext][query]' } }, { test: /\.(ttf|woff2?|mp3|mp4|word)$/, type: "asset/resource", generator: { filename: 'state/resource/[hash:10][ext][query]' } }, { test: /\.m?js$/, exclude: /(node_modules|bower_components)/, use: [ "thread-loader", { loader: 'babel-loader', options: { cacheDirectory: true, cacheCompression: false, } } ] } ] } ] }, plugins: [ new ESLintPlugin({ context:path.resolve(__dirname, "../src"), threads:true }), new HtmlWebpackPlugin({ template: path.resolve(__dirname, "../index.html"), title: "学习Webpack", hash:true, }), new MiniCssExtractPlugin({ filename:"state/css/main.css", }), ], optimization: { minimize: true, minimizer: [ new CssMinimizerPlugin({ parallel:true, }), new TerserPlugin({ parallel:true }) ] }, mode: "production", devtool: "source-map" }
|
14.7 减少代码体积
为什么
Babel 为编译的每个文件都插入了辅助代码,使代码体积过大!
Babel 对一些公共方法使用了非常小的辅助代码,比如 _extend
。默认情况下会被添加到每一个需要它的文件中。
你可以将这些辅助代码作为一个独立模块,来避免重复引入。
是什么
@babel/plugin-transform-runtime
: 禁用了 Babel 自动对每个文件的 runtime 注入,而是引入 @babel/plugin-transform-runtime
并且使所有辅助代码从这里引用。
怎么用
1
| npm i @babel/plugin-transform-runtime -D
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| rules:[ { test: /\.m?js$/, include: path.resolve(__dirname,"../src"), use: { loader: 'babel-loader', options: { cacheDirectory: true, cacheCompression: false, plugins: ["@babel/plugin-transform-runtime"], } } } ]
|
14.8 压缩图片
下载
1
| npm i image-minimizer-webpack-plugin imagemin -D
|
根据情况继续下载依赖
无损压缩
1
| npm install imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo -D
|
有损压缩
1
| npm install imagemin-gifsicle imagemin-mozjpeg imagemin-pngquant imagemin-svgo -D
|
我们使用无损压缩使用
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
| const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
module.exports = { optimization: { minimize: true, 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", }, }, ], }, ], ], }, }, }), ] }, mode: "production", devtool: "source-map" }
|
打包看效果

15. 提高代码运行效率
15.1 Code Split
打包代码时会将所有 js 文件打包到一个文件中,体积太大了。我们如果只要渲染首页,就应该只加载首页的 js 文件,其他文件不应该加载。所以我们需要将打包生成的文件进行代码分割,生成多个 js 文件,渲染哪个页面就只加载某个 js 文件,这样加载的资源就少,速度就更快。
代码分割(Code Split)主要做了两件事:
- 分割文件:将打包生成的文件进行分割,生成多个 js 文件。
- 按需加载:需要哪个文件就加载哪个文件。
代码分割实现方式有不同的方式,为了更加方便体现它们之间的差异,我们会分别创建新的文件来演示
多入口配置
需要将 entry 设置成一个对象,key 是生成的文件名,value 对应文件相对路径
在 output 中使用 [name].js
设置输出的文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| const path = require("path") const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = { entry: { app:"./src/app.js", main:"./src/main.js" }, output:{ path:path.resolve(__dirname,"./dist"), filename: "js/[name].js", clean: true }, plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname,"index.html") }) ], mode: "production" }
|
代码目录结构
1 2 3 4 5 6 7
| demo01 ├── src │ ├── app.js │ └── main.js ├── index.html ├── package.json └── webpack.config.js
|
打包后的目录
1 2 3 4 5
| dist ├── js │ ├── app.js │ └── main.js └── index.html
|
提取重复文件
在项目中如果我们有两个文件都引用了另外一个文件的内容,但是打包时会吧共同的引用的文件打包两份,这样会造成打包后的体积变大,从而降低加载速度,我们可以通过配置 splitChunks
来解决这种问题
首先创建一个 sum.js
1 2 3
| export function sum(...args){ return args.reduce((a,b)=>a+b,0) }
|
然后 app.js
和 main.js
分别使用 sum 函数
1 2 3 4 5
| import {sum} from "./sum";
console.log("app") console.log(sum(1,2,3,4))
|
1 2 3 4 5
| import {sum} from "./sum";
console.log("main") console.log(sum(5,6,7,8))
|
我们现在执行一次打包操作,看看打包后的输出是什么样子


可以看到还是输出了两个文件,并且每个文件里面都有sum函数
添加以下配置信息
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
| const path = require("path") const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = { entry: { app:"./src/app.js", main:"./src/main.js" }, output:{ path:path.resolve(__dirname,"./dist"), filename: "js/[name].js", clean: true }, plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname,"index.html") }) ], optimization: { splitChunks: { chunks: "all", cacheGroups: { default: { minSize: 0, minChunks: 2, priority: -20, reuseExistingChunk: true, }, }, }, }, mode: "production" }
|
然后再次打包可以看到 sum 函数被单独打包成了一个文件

按需加载
例如页面中一个按钮,点击按钮后会调用一个函数进行加法运算,不点击按钮就不会进行加法运算。这时我们可以使用按需加载的方式来加载这个JS,如果不点击,则不会请求这个文件,从而提高页面首次加载速度
新建 count.js
1 2 3
| export default function (a,b){ return a + b }
|
在 main.js
中添加代码
1 2 3 4 5
| document.getElementById("btn").onclick = function (){ import("./count").then(res=>{ console.log(res.default(1,2)) }) }
|
然后 index.html
中添加按钮
1 2 3
| <body> <button id="btn">按钮</button> </body>
|
打包文件,打开 index.html


Esllint 默认不支持 import 动态导入语法,在 .eslintrc.js 文件中添加配置即可
给动态生成的文件命名
细心的同学已经发现,我们上面点击按钮是,动态加载了一个叫 293.js 的文件,但是我们在代码中并不是这个名,将来在我们调试的时候可能会分不清楚那个文件是我们要找的文件。
所以我们可以通过编写特定对的注释代码,来对动态加载的文件命名
1 2 3 4 5
| document.getElementById("btn").onclick = function () { import( "./js/count").then(res=>{ console.log(res.default(1, 2)) }) }
|
固定写法,在动态加载的文件前添加 /* webpackChunkName: "count" */
这段注释。count 就是我们想要生成的文件名称
然后再配置文件中添加下面的代码即可
1
| chunkFilename: "state/js/chunk/[name].js",
|
具体配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| entry: "./src/main.js",
output: { path: path.resolve(__dirname,"../dist"), filename: "state/js/main.js", chunkFilename: "state/js/chunk/[name].js", clean: true, environment: { arrowFunction: false } },
|
然后打包后的文件会自动放在 chunk 文件夹下

16. 文件统一命名
将凡是涉及到文件输出的地方,统一用 [name].文件后缀
的方式来命名,这样webpack会自动将源文件名设置为打包后的文件名
修改后的配置
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
| const path = require("path");
const ESLintPlugin = require('eslint-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin'); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); const TerserPlugin = require("terser-webpack-plugin"); const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
function myCssLoader(preProcessor){ return[ MiniCssExtractPlugin.loader, "css-loader", { loader: "postcss-loader", options: { postcssOptions: { plugins: [ "postcss-preset-env", ], }, }, }, preProcessor ].filter(Boolean) }
module.exports = { entry: "./src/main.js", output: { path: path.resolve(__dirname,"../dist"), filename: "state/js/[name].js", chunkFilename: "state/js/chunk/[name].js", assetModuleFilename: "state/asset/[hash:10][ext][query]", clean: true, environment: { arrowFunction: false } },
module: { rules: [ { oneOf: [ { test: /\.css$/, use: myCssLoader(), }, { test: /\.less$/i, use: myCssLoader('less-loader') }, { test: /\.s[ac]ss$/i, use: myCssLoader('sass-loader') }, { test: /\.(png|jpe?g|gif|webp)$/, type: "asset", parser: { dataUrlCondition: { maxSize: 18 * 1024 } }, }, { test: /\.(ttf|woff2?|mp3|mp4|word)$/, type: "asset/resource", }, { test: /\.m?js$/, exclude: /(node_modules|bower_components)/, use: [ "thread-loader", { loader: 'babel-loader', options: { cacheDirectory: true, cacheCompression: false, plugins: ["@babel/plugin-transform-runtime"], }, } ] } ] } ] }, plugins: [ new ESLintPlugin({ context:path.resolve(__dirname, "../src"), threads:true }), new HtmlWebpackPlugin({ template: path.resolve(__dirname, "../index.html"), title: "学习Webpack", hash:true, }), new MiniCssExtractPlugin({ filename:"state/css/[name].css", }), ], optimization: { minimize: true, minimizer: [ new CssMinimizerPlugin({ parallel:true, }), new TerserPlugin({ parallel:true }), 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", }, }, ], }, ], ], }, }, }), ] }, mode: "production", devtool: "source-map" }
|
17. Preload / Prefetch
为什么
我们前面已经做了代码分割,同时会使用 import 动态导入语法来进行代码按需加载(我们也叫懒加载,比如路由懒加载就是这样实现的)。
但是加载速度还不够好,比如:是用户点击按钮时才加载这个资源的,如果资源体积很大,那么用户会感觉到明显卡顿效果。
我们想在浏览器空闲时间,加载后续需要使用的资源。我们就需要用上 Preload
或 Prefetch
技术。
是什么
Preload
:告诉浏览器立即加载资源。
Prefetch
:告诉浏览器在空闲时才开始加载资源。
它们共同点:
它们区别:
Preload
加载优先级高,Prefetch
加载优先级低。
Preload
只能加载当前页面需要使用的资源,Prefetch
可以加载当前页面资源,也可以加载下一个页面需要使用的资源。
总结:
- 当前页面优先级高的资源用
Preload
加载。
- 下一个页面需要使用的资源用
Prefetch
加载。
它们的问题:兼容性较差。
怎么用
安装
1
| npm i @vue/preload-webpack-plugin -D
|
配置
1 2 3 4 5 6 7 8 9 10 11
| const PreloadWebpackPlugin = require("@vue/preload-webpack-plugin");
plugins: [ new PreloadWebpackPlugin({ as: "script", rel: 'prefetch' }) ],
|
配置完成后,点击按钮加载JS就会从缓存中获取,速度比较快

18. contenthash
为什么
我们通过打包发现,每次打包后的文件都叫 main.js,这样浏览器在加载文件时可能会存在缓存问题,我们可以通过设置让每次打包后都加一个哈希值,这样在加载资源时就不会存在缓存问题
是什么
webpack 设置哈希值有几种方式,它们都会生成一个唯一的 hash 值。
- fullhash(webpack4 是 hash)
每次修改任何一个文件,所有文件名的 hash 至都将改变。所以一旦修改了任何一个文件,整个项目的文件缓存都将失效。
根据不同的入口文件(Entry)进行依赖文件解析、构建对应的 chunk,生成对应的哈希值。我们 js 和 css 是同一个引入,会共享一个 hash 值。
根据文件内容生成 hash 值,只有文件内容变化了,hash 值才会变化。所有文件 hash 值是独享且不同的。
通过对比,contenthash 最适合我们使用
怎么用
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
| module.exports = { entry: "./src/main.js", output: { path: path.resolve(__dirname,"../dist"), filename: "state/js/[name].[contenthash:8].js", chunkFilename: "state/js/chunk/[name].[contenthash:8].js", assetModuleFilename: "state/asset/[hash:10][ext][query]", clean: true, environment: { arrowFunction: false } }, plugins: [ new MiniCssExtractPlugin({ filename:"state/css/[name].[contenthash:8].css", }), ], }
|
静态资源还是使用 hash:10
19. runtimeChunk
为什么
我们通过上面的配置设置打包哈希值,但是在某些场景下,例如:a 文件依赖了 b文件,当b文件发生变化时只改变b文件的哈希值,不要改变a文件的哈希值。这样做的目的是为了在控制缓存的前提下提高加载效率,而不是只改了一个文件,其他的文件都要发生变化。
是什么
将 hash 值单独保管在一个 runtime 文件中。
我们最终输出三个文件:main、math、runtime。当 math 文件发送变化,变化的是 math 和 runtime 文件,main 不变。
runtime 文件只保存文件的 hash 值和它们与文件关系,整个文件体积就比较小,所以变化重新请求的代价也小。
怎么用
添加如下配置即可
1 2 3 4 5 6 7 8
| module.exports = { optimization: { runtimeChunk: { name: (entrypoint) => `runtime~${entrypoint.name}`, }, }, }
|
修改一下 count.js

然后再次打包,通过对比发现,只有count.js和runtime文件的哈希值发生改变,其他均未改变

20. CoreJS
为什么
过去我们使用 babel 对 js 代码进行了兼容性处理,其中使用@babel/preset-env 智能预设来处理兼容性问题。
它能将 ES6 的一些语法进行编译转换,比如箭头函数、点点点运算符等。但是如果是 async 函数、promise 对象、数组的一些方法(includes)等,它没办法处理。
所以此时我们 js 代码仍然存在兼容性问题,一旦遇到低版本浏览器会直接报错。所以我们想要将 js 兼容性问题彻底解决
是什么
core-js
是专门用来做 ES6 以及以上 API 的 polyfill
。
polyfill
翻译过来叫做垫片/补丁。就是用社区上提供的一段代码,让我们在不兼容某些新特性的浏览器上,使用该新特性
怎么用
安装
使用,修改 babel.config.js
1 2 3 4 5 6 7 8 9 10
| module.exports = { presets: [ [ "@babel/preset-env", { useBuiltIns: "usage", corejs: { version: "3", proposals: true } }, ], ], };
|
21. 优化总结
我们从 4 个角度对 webpack 和代码进行了优化:
- 提升开发体验
- 使用
Source Map
让开发或上线时代码报错能有更加准确的错误提示。
- 提升 webpack 提升打包构建速度
- 使用
HotModuleReplacement
让开发时只重新编译打包更新变化了的代码,不变的代码使用缓存,从而使更新速度更快。
- 使用
OneOf
让资源文件一旦被某个 loader 处理了,就不会继续遍历了,打包速度更快。
- 使用
Include/Exclude
排除或只检测某些文件,处理的文件更少,速度更快。
- 使用
Cache
对 eslint 和 babel 处理的结果进行缓存,让第二次打包速度更快。
- 使用
Thead
多进程处理 eslint 和 babel 任务,速度更快。(需要注意的是,进程启动通信都有开销的,要在比较多代码处理时使用才有效果)
- 减少代码体积
- 使用
Tree Shaking
剔除了没有使用的多余代码,让代码体积更小。
- 使用
@babel/plugin-transform-runtime
插件对 babel 进行处理,让辅助代码从中引入,而不是每个文件都生成辅助代码,从而体积更小。
- 使用
Image Minimizer
对项目中图片进行压缩,体积更小,请求速度更快。(需要注意的是,如果项目中图片都是在线链接,那么就不需要了。本地项目静态图片才需要进行压缩。)
- 优化代码运行性能
- 使用
Code Split
对代码进行分割成多个 js 文件,从而使单个文件体积更小,并行加载 js 速度更快。并通过 import 动态导入语法进行按需加载,从而达到需要使用时才加载该资源,不用时不加载资源。
- 使用
Preload / Prefetch
对代码进行提前加载,等未来需要使用时就能直接使用,从而用户体验更好。
- 使用
Network Cache
能对输出资源文件进行更好的命名,将来好做缓存,从而用户体验更好。
- 使用
Core-js
对 js 进行兼容性处理,让我们代码能运行在低版本浏览器。
- 使用
PWA
能让代码离线也能访问,从而提升用户体验。
22. 手动搭建Vue脚手架
22.1 项目结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| MyVueCli ├── config │ └── webpack.dev.js │ └── webpack.prod.js ├── public │ └── index.html ├── src │ ├── router │ │ └── index.js │ ├── view │ │ ├── About │ │ │ └── index.vue │ │ └── Home │ │ └── index.vue │ ├── App.vue │ └── main.js ├── .eslintrc.js ├── babel.config.js └── package.json
|
22.2 开发模式配置
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
| const path = require("path"); const ESLintWebpackPlugin = require("eslint-webpack-plugin"); const HtmlWebpackPlugin = require("html-webpack-plugin"); const { VueLoaderPlugin } = require("vue-loader"); const { DefinePlugin } = require("webpack");
const getStyleLoaders = (preProcessor) => { return [ "vue-style-loader", "css-loader", { loader: "postcss-loader", options: { postcssOptions: { plugins: [ "postcss-preset-env", ], }, }, }, preProcessor, ].filter(Boolean); };
module.exports = { entry: "./src/main.js", output: { path: undefined, filename: "static/js/[name].js", chunkFilename: "static/js/[name].chunk.js", assetModuleFilename: "static/js/[hash:10][ext][query]", }, module: { rules: [ { test: /\.css$/, use: getStyleLoaders(), }, { test: /\.less$/, use: getStyleLoaders("less-loader"), }, { test: /\.s[ac]ss$/, use: getStyleLoaders("sass-loader"), }, { test: /\.(png|jpe?g|gif|svg)$/, type: "asset", parser: { dataUrlCondition: { maxSize: 10 * 1024, }, }, }, { test: /\.(ttf|woff2?)$/, type: "asset/resource", }, { test: /\.vue$/, loader: "vue-loader", options: { cacheDirectory: path.resolve( __dirname, "node_modules/.cache/vue-loader" ), }, }, ], }, plugins: [ new ESLintWebpackPlugin({ context: path.resolve(__dirname, "../src"), exclude: "node_modules", cache: true, cacheLocation: path.resolve( __dirname, "../node_modules/.cache/.eslintcache" ), }), new HtmlWebpackPlugin({ template: path.resolve(__dirname, "../public/index.html"), }), new VueLoaderPlugin(), new DefinePlugin({ __VUE_OPTIONS_API__: "true", __VUE_PROD_DEVTOOLS__: "false", }), ], optimization: { splitChunks: { chunks: "all", }, runtimeChunk: { name: (entrypoint) => `runtime~${entrypoint.name}`, }, }, resolve: { extensions: [".vue", ".js", ".json"], }, devServer: { open: true, host: "localhost", port: 3000, hot: true, compress: true, historyApiFallback: true, }, mode: "development", devtool: "cheap-module-source-map", };
|
22.3 生产模式配置
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
| const path = require("path"); const ESLintWebpackPlugin = require("eslint-webpack-plugin"); const HtmlWebpackPlugin = require("html-webpack-plugin"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); const TerserWebpackPlugin = require("terser-webpack-plugin"); const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin"); const CopyPlugin = require("copy-webpack-plugin") const { VueLoaderPlugin } = require("vue-loader"); const { DefinePlugin } = require("webpack");
const getStyleLoaders = (preProcessor) => { return [ MiniCssExtractPlugin.loader, "css-loader", { loader: "postcss-loader", options: { postcssOptions: { plugins: [ "postcss-preset-env", ], }, }, }, preProcessor, ].filter(Boolean); };
module.exports = { entry: "./src/main.js", output: { path: path.resolve(__dirname,"dist"), filename: "static/js/[name].[contenthash:10].js", chunkFilename: "static/js/[name].[contenthash:10].chunk.js", assetModuleFilename: "static/asset/[hash:10][ext][query]", clean: true, }, module: { rules: [ { test: /\.css$/, use: getStyleLoaders(), }, { test: /\.less$/, use: getStyleLoaders("less-loader"), }, { test: /\.s[ac]ss$/, use: getStyleLoaders("sass-loader"), }, { test: /\.(png|jpe?g|gif|svg)$/, type: "asset", parser: { dataUrlCondition: { maxSize: 10 * 1024, }, }, }, { test: /\.(ttf|woff2?)$/, type: "asset/resource", }, { test: /\.(jsx|js)$/, include: path.resolve(__dirname, "../src"), loader: "babel-loader", options: { cacheDirectory: true, cacheCompression: false, plugins: [ ], }, }, { test: /\.vue$/, loader: "vue-loader", options: { cacheDirectory: path.resolve( __dirname, "node_modules/.cache/vue-loader" ), }, }, ], }, plugins: [ new ESLintWebpackPlugin({ context: path.resolve(__dirname, "../src"), exclude: "node_modules", cache: true, cacheLocation: path.resolve( __dirname, "../node_modules/.cache/.eslintcache" ), }), new HtmlWebpackPlugin({ template: path.resolve(__dirname, "../public/index.html"), }), new CopyPlugin({ patterns: [ { from: path.resolve(__dirname, "../public"), to: path.resolve(__dirname, "../dist"), toType: "dir", noErrorOnMissing: true, globOptions: { ignore: ["**/index.html"], }, info: { minimized: true, }, }, ], }), new MiniCssExtractPlugin({ filename: "static/css/[name].[contenthash:10].css", chunkFilename: "static/css/[name].[contenthash:10].chunk.css", }), new VueLoaderPlugin(), new DefinePlugin({ __VUE_OPTIONS_API__: "true", __VUE_PROD_DEVTOOLS__: "false", }), ], optimization: { minimizer: [ new CssMinimizerPlugin(), new TerserWebpackPlugin(), 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", }, runtimeChunk: { name: (entrypoint) => `runtime~${entrypoint.name}`, }, }, resolve: { extensions: [".vue", ".js", ".json"], }, mode: "production", devtool: "source-map", };
|
22.4 其他文件代码
.eslintrc.js
1 2 3 4 5 6 7 8 9 10
| module.exports = { root: true, env: { node: true, }, extends: ["plugin:vue/vue3-essential", "eslint:recommended"], parserOptions: { parser: "@babel/eslint-parser", }, };
|
babel.config.js
1 2 3
| module.exports = { presets: ["@vue/cli-plugin-babel/preset"], };
|
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
| { "name": "myvuecli", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "dev": "cross-env NODE_ENV=development webpack serve --config ./config/webpack.dev.js", "build": "cross-env NODE_ENV=production webpack --config ./config/webpack.prod.js" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "@babel/eslint-parser": "^7.22.11", "vue": "^3.3.4" }, "devDependencies": { "@vue/cli-plugin-babel": "^5.0.8", "babel-loader": "^9.1.3", "cross-env": "^7.0.3", "css-loader": "^6.8.1", "eslint": "^8.48.0", "eslint-plugin-vue": "^9.17.0", "eslint-webpack-plugin": "^4.0.1", "html-webpack-plugin": "^5.5.3", "less": "^3.13.1", "less-loader": "^10.2.0", "postcss-loader": "^7.3.3", "postcss-preset-env": "^9.1.1", "sass-loader": "^13.3.2", "vue-loader": "^17.2.2", "vue-router": "^4.2.4", "vue-style-loader": "^4.1.3", "vue-template-compiler": "^2.7.14", "webpack": "^5.88.2", "webpack-cli": "^5.1.4", "webpack-dev-server": "^4.15.1" } }
|
main.js
1 2 3 4 5 6 7 8
| import {createApp} from "vue"; import App from "./App"; import router from "./router"
const app = createApp(App); app.use(router) app.mount(document.getElementById("app"))
|
router/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import {createRouter,createWebHashHistory} from "vue-router";
export default createRouter({ history:createWebHashHistory(), routes:[ { path:"/", redirect:"/home" }, { path:"/home", component:()=> import("../view/Home") },{ path:"/about", component:()=> import("../view/About") } ] })
|
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 33
| <template> <ul> <li> <router-link to="/home">Home</router-link> </li> <li> <router-link to="/about">About</router-link> </li> </ul>
<router-view/> </template>
<script setup>
</script>
<style scoped lang="less"> ul{ display: flex; flex-direction: column; gap: 15px; margin: 0; padding: 0; li{ margin-left: 0; list-style: none; line-height: 35px; height: 35px; background-color: pink; } } </style>
|
22.5 测试运行
运行测试

运行效果

打包测试

打包后的文件

运行没问题

22.6 合并配置
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
| const path = require("path"); const ESLintWebpackPlugin = require("eslint-webpack-plugin"); const HtmlWebpackPlugin = require("html-webpack-plugin"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); const TerserWebpackPlugin = require("terser-webpack-plugin"); const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin"); const CopyPlugin = require("copy-webpack-plugin") const { VueLoaderPlugin } = require("vue-loader"); const { DefinePlugin } = require("webpack");
const isProduction = process.env.NODE_ENV === "production";
const getStyleLoaders = (preProcessor) => { return [ isProduction ? MiniCssExtractPlugin.loader : "vue-style-loader", "css-loader", { loader: "postcss-loader", options: { postcssOptions: { plugins: [ "postcss-preset-env", ], }, }, }, preProcessor, ].filter(Boolean); };
module.exports = { entry: "./src/main.js", output: { path: isProduction ? path.resolve(__dirname,"../dist") : undefined, filename: isProduction ? "static/js/[name].[contenthash:10].js" : "static/js/[name].js", chunkFilename:isProduction ? "static/js/[name].[contenthash:10].chunk.js" : "static/js/[name].chunk.js", assetModuleFilename: "static/asset/[hash:10][ext][query]", clean: true, }, module: { rules: [ { test: /\.css$/, use: getStyleLoaders(), }, { test: /\.less$/, use: getStyleLoaders("less-loader"), }, { test: /\.s[ac]ss$/, use: getStyleLoaders("sass-loader"), }, { test: /\.(png|jpe?g|gif|svg)$/, type: "asset", parser: { dataUrlCondition: { maxSize: 10 * 1024, }, }, }, { test: /\.(ttf|woff2?)$/, type: "asset/resource", }, { test: /\.(jsx|js)$/, include: path.resolve(__dirname, "../src"), loader: "babel-loader", options: { cacheDirectory: true, cacheCompression: false, plugins: [ ], }, }, { test: /\.vue$/, loader: "vue-loader", options: { cacheDirectory: path.resolve( __dirname, "node_modules/.cache/vue-loader" ), }, }, ], }, plugins: [ new ESLintWebpackPlugin({ context: path.resolve(__dirname, "../src"), exclude: "node_modules", cache: true, cacheLocation: path.resolve( __dirname, "../node_modules/.cache/.eslintcache" ), }), new HtmlWebpackPlugin({ template: path.resolve(__dirname, "../public/index.html"), }), isProduction && new CopyPlugin({ patterns: [ { from: path.resolve(__dirname, "../public"), to: path.resolve(__dirname, "../dist"), toType: "dir", noErrorOnMissing: true, globOptions: { ignore: ["**/index.html"], }, info: { minimized: true, }, }, ], }), isProduction && new MiniCssExtractPlugin({ filename: "static/css/[name].[contenthash:10].css", chunkFilename: "static/css/[name].[contenthash:10].chunk.css", }), new VueLoaderPlugin(), new DefinePlugin({ __VUE_OPTIONS_API__: "true", __VUE_PROD_DEVTOOLS__: "false", }), ].filter(Boolean), optimization: { minimize: isProduction, minimizer: [ new CssMinimizerPlugin(), new TerserWebpackPlugin(), 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", }, runtimeChunk: { name: (entrypoint) => `runtime~${entrypoint.name}`, }, }, resolve: { extensions: [".vue", ".js", ".json"], }, mode: isProduction ? "production" : "development", devtool: isProduction ? "source-map" : "cheap-module-source-map", devServer: { open: true, host: "localhost", port: 3000, hot: true, compress: true, historyApiFallback: true, }, };
|
23. ElementPlus按需导入
安装 element-plus
1
| npm install element-plus --save
|
安装按需导入的依赖
1
| npm install -D unplugin-vue-components unplugin-auto-import
|
配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const AutoImport = require('unplugin-auto-import/webpack') const Components = require('unplugin-vue-components/webpack') const { ElementPlusResolver } = require('unplugin-vue-components/resolvers')
module.exports = { plugins: [ AutoImport({ resolvers: [ElementPlusResolver()], }), Components({ resolvers: [ElementPlusResolver()], }), ], }
|
然后页面中直接使用即可
1 2 3 4
| <template> <div>Home</div> <el-button type="primary">按钮</el-button> </template>
|

24. loader原理
24.1 loader概念
帮助 webpack 将不同类型的文件转换为 webpack 可识别的模块。
24.2 loader的执行顺序
- 分类
- pre: 前置 loader
- normal: 普通 loader
- inline: 内联 loader
- post: 后置 loader
- 执行顺序
- 4 类 loader 的执行优级为:
pre > normal > inline > post
。
- 相同优先级的 loader 执行顺序为:
从右到左,从下到上
。
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| module: { rules: [ { test: /\.js$/, loader: "loader1", }, { test: /\.js$/, loader: "loader2", }, { test: /\.js$/, loader: "loader3", }, ], },
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| module: { rules: [ { enforce: "pre", test: /\.js$/, loader: "loader1", }, { test: /\.js$/, loader: "loader2", }, { enforce: "post", test: /\.js$/, loader: "loader3", }, ], },
|
24.3 自定义loader
初始化项目结构
1 2 3 4 5 6 7 8 9
| MyLoader ├── js │ └── main.js ├── loader │ └── test-loader.js ├── src │ └── index.html ├── package.json └── webpack.config.js
|
main.js
1
| console.log("hello word")
|
编写一个简单的loader,test-loader.js
代码如下
1 2 3 4
| module.exports = function (content){ console.log("loader打印内容",content) return content }
|
使用
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
|
const path = require("path") const HtmlWebpackPlugins = require("html-webpack-plugin") module.exports = { entry: "./js/main.js", output: { path: path.resolve(__dirname,"dist"), filename: "js/[name].[hash:10].js", clean: true }, module: { rules: [ { test: /\.js$/, loader: "./loader/test-loader.js" } ] }, plugins: [ new HtmlWebpackPlugins({ template:path.resolve(__dirname,"./src/index.html") }) ], mode: "development" }
|
然后安装依赖
1
| npm install webpack webpack-cli html-webpack-plugin -D
|
然后执行打包命令
观察输出

总结:
loader 接收要处理的文件的源码,在loader内部处理完毕后再返回出去
24.4 同步loader和异步loader
新建同步loader
1 2 3 4 5 6 7 8 9 10 11 12 13
|
module.exports = function (content,map,meta){ console.log("同步loader打印") console.log(meta)
this.callback(null,content,map,meta) }
|
新建异步loader
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
module.exports = function (content,map,meta){ console.log("异步loader打印") let callback = this.async()
setTimeout(()=>{ callback(null,content,map, { name:"我是来自异步loader的参数" }) },2000) }
|
使用
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
| const path = require("path") const HtmlWebpackPlugins = require("html-webpack-plugin") module.exports = { entry: "./js/main.js", output: { path: path.resolve(__dirname,"dist"), filename: "js/[name].[hash:10].js", clean: true }, module: { rules: [ { test:/\.js$/, use: [ "./loader/async/loader1.js", "./loader/async/loader2.js", ] } ] }, plugins: [ new HtmlWebpackPlugins({ template:path.resolve(__dirname,"./src/index.html") }) ], mode: "development" }
|
打包输出结果

24.5 raw loader
raw loader接收到的文件时Buffer数据,一般用来处理图片,字体等文件
定义 raw-loader
1 2 3 4 5 6 7 8 9
|
module.exports = function (content){ console.log(content) return content }
module.exports.raw = true
|
使用
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
| const path = require("path") const HtmlWebpackPlugins = require("html-webpack-plugin") module.exports = { entry: "./js/main.js", output: { path: path.resolve(__dirname,"dist"), filename: "js/[name].[hash:10].js", clean: true }, module: { rules: [ { test: /\.js$/, loader: "./loader/raw/raw-loader.js" } ] }, plugins: [ new HtmlWebpackPlugins({ template:path.resolve(__dirname,"./src/index.html") }) ], mode: "development" }
|
查看输出

24.6 pitch loader
pitch loader会在 normal 之前执行,并且当存在多个 pitch loader 时执行的循序和normal loader循序相反,从左往右,从上到下
定义 pitch loader1
1 2 3 4 5 6 7 8 9 10
|
module.exports = function (content){ console.log("pitch-normal-1",content) return content }
module.exports.pitch = function (){ console.log("pitch1") }
|
定义 pitch loader2
1 2 3 4 5 6 7 8 9 10
|
module.exports = function (content){ console.log("pitch-normal-2",content) return content }
module.exports.pitch = function (){ console.log("pitch2") }
|
使用
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
| const path = require("path") const HtmlWebpackPlugins = require("html-webpack-plugin") module.exports = { entry: "./js/main.js", output: { path: path.resolve(__dirname,"dist"), filename: "js/[name].[hash:10].js", clean: true }, module: { rules: [ { test:/\.js$/, use:[ "./loader/pitch/pitch1.js", "./loader/pitch/pitch2.js", ] } ] }, plugins: [ new HtmlWebpackPlugins({ template:path.resolve(__dirname,"./src/index.html") }) ], mode: "development" }
|
执行打包输出结果

通过结果可以看到先执行 pitch1 和 pitch2 方法,再接着执行 normal loader 方法
当在一个 pitch loader 中添加的 return 时,会终止后面的 pitch 方法和 normal loader 方法,然后把之前的 normal loader 方法执行完
例如,再新建一个 pitch3
1 2 3 4 5 6 7 8
| module.exports = function (content){ console.log("pitch-normal-3",content) return content }
module.exports.pitch = function (){ console.log("pitch3") }
|
然后修改 pitch2
1 2 3 4 5 6 7 8 9
| module.exports = function (content){ console.log("pitch-normal-2",content) return content }
module.exports.pitch = function (){ console.log("pitch2") return "pitch2" }
|
修改配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| module.exports = { module: { rules: [ { test:/\.js$/, use:[ "./loader/pitch/pitch1.js", "./loader/pitch/pitch2.js", "./loader/pitch/pitch3.js", ] } ] }, }
|
执行打包效果


执行流程图如上
24.7. loader Api
方法名 |
含义 |
用法 |
this.async |
异步回调 loader。返回 this.callback |
const callback = this.async() |
this.callback |
可以同步或者异步调用的并返回多个结果的函数 |
this.callback(err, content, sourceMap?, meta?) |
this.getOptions(schema) |
获取 loader 的 options |
this.getOptions(schema) |
this.emitFile |
产生一个文件 |
this.emitFile(name, content, sourceMap) |
this.utils.contextify |
返回一个相对路径 |
this.utils.contextify(context, request) |
this.utils.absolutify |
返回一个绝对路径 |
this.utils.absolutify(context, request) |
更多文档,请查阅 webpack 官方 loader api 文档
25. 自实现loader
25.1 clear-log-loader
在项目开发阶段我们会使用 console.log 打印信息,但是到生产环境后我们不需要这个 log 打印。所以可以通过 loader 在打包时将 console.log 去掉
新建 clear-log-loader.js
1 2 3
| module.exports = function (content){ return content.replace(/console\.log\(.*\);?/g,"") }
|
使用
1 2 3 4 5 6 7 8 9 10 11
| module.exports = { module: { rules: [ { test:/\.js$/, loader: "./loader/clear-log-loader.js" } ] }, }
|
25.2 给loader添加options参数
上面的 loader 中我们通过 option 添加一个 enable 属性,俩控制是否清除 log
首先在 loader 中添加 enable 配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| module.exports = { module: { rules: [ { test:/\.js$/, loader: "./loader/clear-log-loader.js", options: { enable:false } } ] }, }
|
修改 clear-log-loader.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| const schema = { type:"object", properties:{ enable:{ type:"boolean" } }, additionalProperties:false }
module.exports = function (content){ let {enable} = this.getOptions(schema) return enable ? content.replace(/console\.log\(.*\);?/g,"") : content }
|
这个时候我们配置的enable值是false,所以打包后文件是有log的

我们改成 true,再次打包,log就没有了

25.3 自实现babel-loader
babel 官网:Babel 中文文档 | Babel中文网 · Babel 中文文档 | Babel中文网 (babeljs.cn)
将代码转成ES5语法
安装
1
| npm i @babel/core @babel/preset-env -D
|
新建 babel-loader.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| const schema = { "type": "object", "properties": { "presets": { "type": "array" } }, "additionalProperties": true } const babel = require("@babel/core");
module.exports = function (content) { const options = this.getOptions(schema); const callback = this.async(); babel.transform(content, options, function (err, result) { callback(err, result.code); }); };
|
wabpack配置
1 2 3 4 5 6 7 8 9
| rules:[ { test:/\.js$/, loader: "./loader/babel-loader.js", options: { presets:["@babel/preset-env"], } } ]
|
25.4 file-loader
将文件原封不动的输出
安装
file-loader
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| const loaderUtils = require("loader-utils")
module.exports = function (content){ const interpolatedName = loaderUtils.interpolateName( this, "asset/[name].[hash].[ext]", { content:content } )
this.emitFile(interpolatedName,content)
return `module.exports = "${interpolatedName}"` }
module.exports.raw = true
|
使用
1 2 3 4 5 6 7
| rules:[ { test:/\.(png|jpg)$/, loader: "./loader/file-loader.js", type:"javascript/auto" } ]
|
26. Plugin原理
Plugin 的作用
通过插件我们可以扩展 webpack,加入自定义的构建行为,使 webpack 可以执行更广泛的任务,拥有更强的构建能力。
Plugin 工作原理
webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。 插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。webpack 通过 Tapable 来组织这条复杂的生产线。 webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。 webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。 ——「深入浅出 Webpack」
站在代码逻辑的角度就是:webpack 在编译代码过程中,会触发一系列 Tapable
钩子事件,插件所做的,就是找到相应的钩子,往上面挂上自己的任务,也就是注册事件,这样,当 webpack 构建的时候,插件注册的事件就会随着钩子的触发而执行了。
Webpack 内部的钩子
什么是钩子
钩子的本质就是:事件。为了方便我们直接介入和控制编译过程,webpack 把编译过程中触发的各类关键事件封装成事件接口暴露了出来。这些接口被很形象地称做:hooks
(钩子)。开发插件,离不开这些钩子。
Tapable
Tapable
为 webpack 提供了统一的插件接口(钩子)类型定义,它是 webpack 的核心功能库。webpack 中目前有十种 hooks
,在 Tapable
源码中可以看到,他们是:
1 2 3 4 5 6 7 8 9 10 11 12 13
| exports.SyncHook = require("./SyncHook"); exports.SyncBailHook = require("./SyncBailHook"); exports.SyncWaterfallHook = require("./SyncWaterfallHook"); exports.SyncLoopHook = require("./SyncLoopHook"); exports.AsyncParallelHook = require("./AsyncParallelHook"); exports.AsyncParallelBailHook = require("./AsyncParallelBailHook"); exports.AsyncSeriesHook = require("./AsyncSeriesHook"); exports.AsyncSeriesBailHook = require("./AsyncSeriesBailHook"); exports.AsyncSeriesLoopHook = require("./AsyncSeriesLoopHook"); exports.AsyncSeriesWaterfallHook = require("./AsyncSeriesWaterfallHook"); exports.HookMap = require("./HookMap"); exports.MultiHook = require("./MultiHook");
|
Tapable
还统一暴露了三个方法给插件,用于注入不同类型的自定义构建行为:
tap
:可以注册同步钩子和异步钩子。
tapAsync
:回调方式注册异步钩子。
tapPromise
:Promise 方式注册异步钩子。
Plugin 构建对象
Compiler
compiler 对象中保存着完整的 Webpack 环境配置,每次启动 webpack 构建时它都是一个独一无二,仅仅会创建一次的对象。
这个对象会在首次启动 Webpack 时创建,我们可以通过 compiler 对象上访问到 Webapck 的主环境配置,比如 loader 、 plugin 等等配置信息。
它有以下主要属性:
compiler.options
可以访问本次启动 webpack 时候所有的配置文件,包括但不限于 loaders 、 entry 、 output 、 plugin 等等完整配置信息。
compiler.inputFileSystem
和 compiler.outputFileSystem
可以进行文件操作,相当于 Nodejs 中 fs。
compiler.hooks
可以注册 tapable 的不同种类 Hook,从而可以在 compiler 生命周期中植入不同的逻辑。
compiler hooks 文档open in new window
Compilation
compilation 对象代表一次资源的构建,compilation 实例能够访问所有的模块和它们的依赖。
一个 compilation 对象会对构建依赖图中所有模块,进行编译。 在编译阶段,模块会被加载(load)、封存(seal)、优化(optimize)、 分块(chunk)、哈希(hash)和重新创建(restore)。
它有以下主要属性:
compilation.modules
可以访问所有模块,打包的每一个文件都是一个模块。
compilation.chunks
chunk 即是多个 modules 组成而来的一个代码块。入口文件引入的资源组成一个 chunk,通过代码分割的模块又是另外的 chunk。
compilation.assets
可以访问本次打包生成所有文件的结果。
compilation.hooks
可以注册 tapable 的不同种类 Hook,用于在 compilation 编译模块阶段进行逻辑添加以及修改。
compilation hooks 文档open in new window
生命周期简图
27. 自实现Plugin
27.1 TestPlugin
plugin都是一个构造函数,所以我们采用es6的class语法来自定义Plugin
webpack 在打包过程中触发 plugin 中的 apply 方法
1 2 3 4 5 6 7 8 9 10
| class TestPlugin{ constructor() { console.log("TestPlugin constructor") } apply(){ console.log("TestPlugin apply") } }
module.exports = TestPlugin
|
使用
1 2 3 4 5 6 7 8 9 10 11 12 13
| const path = require("path") const HtmlWebpackPlugins = require("html-webpack-plugin") const TestPlugin = require("./plugins/test-plugin")
module.exports = { plugins: [ new HtmlWebpackPlugins({ template:path.resolve(__dirname,"./src/index.html") }), new TestPlugin() ], mode: "development" }
|
打包的时候会先输出plugin中的内容

27.2 注册hook
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
| class TestPlugin{ constructor() { console.log("TestPlugin constructor") } apply(compiler){ console.log("TestPlugin apply")
compiler.hooks.compile.tap("TestPlugin", (compilationParams) => { console.log("compiler.compile()"); });
compiler.hooks.make.tap("TestPlugin", (compilation) => { setTimeout(() => { console.log("compiler.make() 111"); }, 2000); });
compiler.hooks.make.tapAsync("TestPlugin", (compilation, callback) => { setTimeout(() => { console.log("compiler.make() 222"); callback(); }, 1000); });
compiler.hooks.make.tapPromise("TestPlugin", (compilation) => { console.log("compiler.make() 333"); return new Promise((resolve) => { resolve(); }); });
compiler.hooks.emit.tapAsync("TestPlugin", (compilation, callback) => { setTimeout(() => { console.log("compiler.emit() 111"); callback(); }, 3000); });
compiler.hooks.emit.tapAsync("TestPlugin", (compilation, callback) => { setTimeout(() => { console.log("compiler.emit() 222"); callback(); }, 2000); });
compiler.hooks.emit.tapAsync("TestPlugin", (compilation, callback) => { setTimeout(() => { console.log("compiler.emit() 333"); callback(); }, 1000); }); } }
module.exports = TestPlugin
|

27.3 添加node调试命令
在plugin中添加 debugger
然后添加 package.json
添加调试启动命令
1 2 3
| "scripts": { "debug": "node --inspect-brk ./node_modules/webpack-cli/bin/cli.js" },
|
运行
然后随便一个页面打开浏览器的控制台,点击 node 图标

点击后会跳转一个新页面,默认是在程序第一行打断点,我们点击下一步跳转到下一个断点即可跳转到我们打断点的位置

27.4 BannerWebpackPlugin
作用,给打包生成的文件添加作者信息
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
| class BannerWebpackPlugin{ constructor(options) { this.options = options; } apply(compile){ compile.hooks.emit.tapAsync("BannerWebpackPlugin",(compilation,callback)=>{
const assetPaths = Object.keys(compilation.assets).filter(assetsFile=>{ let extensions = ["js","css"] let splits = assetsFile.split(".") let splitEd = splits[splits.length - 1] return extensions.includes(splitEd) })
debugger
assetPaths.forEach(path=>{ const asset = compilation.assets[path]
const newSource = `/** * author:${this.options.auther} */ ${asset.source()} ` compilation.assets[path] = { source(){ return newSource }, size(){ return newSource.length } } }) callback() }) } }
module.exports = BannerWebpackPlugin
|
使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const path = require("path") const HtmlWebpackPlugins = require("html-webpack-plugin") const BannerWebpackPlugin = require("./plugins/banner-webpack-plugin")
module.exports = { entry: "./js/main.js", plugins: [ new HtmlWebpackPlugins({ template:path.resolve(__dirname,"./src/index.html") }), new BannerWebpackPlugin({ auther:"SongZX" }) ], mode: "production" }
|
打包后的效果

27.5 CleanWebpackPlugin
打包前删除旧的打包文件
添加 clean-webpack-plugin.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
| class CleanWebpackPlugin{ apply(compiler) { let outputPath = compiler.options.output.path; let fs = compiler.outputFileSystem;
compiler.hooks.emit.tap("CleanWebpackPlugin",(compilation)=>{ this.removeFiles(fs,outputPath) }) }
removeFiles(fs,filePath){ let files = fs.readdirSync(filePath);
files.forEach(fileName=>{ let path = `${filePath}/${fileName}` let stats = fs.statSync(path); if(stats.isDirectory()){ this.removeFiles(fs,path) }else{ fs.unlinkSync(path) } }) } }
module.exports = CleanWebpackPlugin
|
使用
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") const HtmlWebpackPlugins = require("html-webpack-plugin") const TestPlugin = require("./plugins/test-plugin") const BannerWebpackPlugin = require("./plugins/banner-webpack-plugin") const CleanWebpackPlugin = require("./plugins/clean-webpack-plugin")
module.exports = { entry: "./js/main.js", output: { path: path.resolve(__dirname,"dist"), filename: "js/[name].[hash:10].js", clean: false, assetModuleFilename: "asset/[name].[hash:10][ext]" }, plugins: [ new HtmlWebpackPlugins({ template:path.resolve(__dirname,"./src/index.html") }), new BannerWebpackPlugin({ auther:"SongZX" }), new CleanWebpackPlugin() ], mode: "production" }
|
27.6 AnalyzeWebpackPlugin
在打包时自动生成一个分析文件大小的 md 文件
新建 analyze-webpack-plugin.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
| class AnalyzeWebpackPlugin{ apply(compiler){ compiler.hooks.emit.tap("AnalyzeWebpackPlugin",(compilation)=>{ let assets = Object.entries(compilation.assets)
let content = `| 文件名称 | 文件大小 | | ------ | ------ |` assets.forEach(([fileName,file])=>{ content += `\n|${fileName}|${Math.ceil(file.size()/1024)}kb|` })
compilation.assets['analyze.md'] = { source(){ return content }, size(){ return content.length } } }) } } module.exports = AnalyzeWebpackPlugin
|
使用
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
| const path = require("path") const HtmlWebpackPlugins = require("html-webpack-plugin") const TestPlugin = require("./plugins/test-plugin") const BannerWebpackPlugin = require("./plugins/banner-webpack-plugin") const CleanWebpackPlugin = require("./plugins/clean-webpack-plugin") const AnalyzeWebpackPlugin = require("./plugins/analyze-webpack-plugin")
module.exports = { entry: "./js/main.js", output: { path: path.resolve(__dirname,"dist"), filename: "js/[name].[hash:10].js", clean: false, assetModuleFilename: "asset/[name].[hash:10][ext]" }, plugins: [ new HtmlWebpackPlugins({ template:path.resolve(__dirname,"./src/index.html") }), new BannerWebpackPlugin({ auther:"SongZX" }), new CleanWebpackPlugin(), new AnalyzeWebpackPlugin() ], mode: "production" }
|
效果
