Webpack5学习
尚硅谷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)
代码仓库:https://gitee.com/szxio/webpack5
1. 简单使用
1.1 搭建项目
1.2 编写JS代码
count.js
export default function count(a,b){
return a + b
}
sum.js
export default function sum(...args){
return args.reduce((a,b)=>a+b,0)
}
main.js
import count from "./js/count";
import sum from "./js/sum";
console.log(count(1,2))
console.log(sum(1,2,3,4))
index.html
<!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 初始化依赖
npm init -y
会自动生成 package.json
继续安装依赖
npm i webpack webpack-cli -D
1.5 启动webpack
开发环境构建
npx webpack ./src/main.js --mode=development
生产环境构建
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
const path = require("path");
module.exports = {
// 入口文件,相对路径
entry: "./src/main.js",
// 输出
output: {
// 要输出到的位置,绝对路径
path: path.resolve(__dirname,"dist"),
// 文件名
filename: "main.js"
},
// 加载器
module: {
rules: [
// loader 的配置
]
},
// 插件
plugins: [
// plugins的配置
],
// 模式
mode: "development"
}
有了这个配置文件,我们再进行打包时只需输入如下命令即可
npx webpack
这个命令会自动找根目录中的 webpa.config.js 文件中的配置进行打包
3. 处理样式资源
3.1 处理CSS资源
webpack默认只支持JS文件,我们编写CSS后打包会出错,我们可以通过添加loader来处理CSS资源
更多loader可以通过webpack官方文档来查看
当我们在 main.js 中引入 css 文件后,执行打包会出现下面的错误
安装 loader
npm i css-loader style-loader -D
然后配置规则
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
],
},
],
},
};
此时再次执行打包即可
3.2 处理Less资源
安装
npm install less less-loader --save-dev
配置规则
module.exports = {
module: {
rules: [
{
test: /\.less$/i,
use: [
// compiles Less to CSS
'style-loader',
'css-loader',
'less-loader',
],
},
],
},
};
3.3 处理Scss资源
安装
npm install sass-loader sass --save-dev
配置规则
module.exports = {
module: {
rules: [
{
test: /\.s[ac]ss$/i,
use: [
// 将 JS 字符串生成为 style 节点
'style-loader',
// 将 CSS 转化成 CommonJS 模块
'css-loader',
// 将 Sass 编译成 CSS
'sass-loader',
],
},
],
},
};
4. 处理图片资源
4.1 图片转Base64
我们可以通过配置,将小于一定大小的文件转成base64处理,从而减少请求
添加以下配置
module.exports = {
module: {
rules: [
{
test: /\.(png|jpe?g|gif|webp)$/,
type: "asset",
parser: {
dataUrlCondition: {
maxSize: 10 * 1024 // 小于10kb的图片会被base64处理
}
}
},
],
},
};
转成base64后请求不会耗时间
5. 修改文件输出地址
目前我们打包生成的文件都在一个文件下
我想让他变成 JS 文件在 JS 文件下,图片文件在图片文件下
修改配置如下
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 属性即可
const path = require("path");
module.exports = {
// 入口文件,相对路径
entry: "./src/main.js",
// 输出
output: {
// 要输出到的位置,绝对路径
path: path.resolve(__dirname,"dist"),
// 文件名
filename: "js/main.js",
// 每次打包前自动清空path对应的目录文件
clean: true
},
}
7. 处理字体图标资源
我们可以在 阿里巴巴矢量图标库 中找一些图标资源,然后加入到项目中,点击下载到本地
使用第二种方式引入
在项目中导入下载下来的 css 和图标文件
在 main.js 中引入 iconfont.css
然后配置规则,将字体图标打包输出到 font 文件夹中
module.exports = {
module: {
rules: [
{
test: /\.(ttf|woff2?)$/,
type: "asset/resource",
generator: {
filename: 'font/[hash:10][ext][query]'
}
},
]
},
}
然后再页面中可以通过下面的方式使用
<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 会原封不动的将文件输出到指定目录
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 时就会以写的规则对代码进行检查
配置文件的写法
配置文件有很多种写法:
-
.eslintrc.*
:新建文件,位于项目根目录
.eslintrc
.eslintrc.js
.eslintrc.json
- 区别在于配置格式不一样
-
package.json
中eslintConfig
:不需要创建文件,在原有文件基础上写
ESLint 会查找和自动读取它们,所以以上配置文件只需要存在一个即可
具体配置
以 .eslintrc.js
为例
module.exports = {
// 解析选项
parserOptions: {},
// 具体检查规则
rules: {},
// 继承其他规则
extends: [],
// ...
// 其他规则详见:https://eslint.bootcss.com/docs/user-guide/configuring
};
parserOptions 解析选项
parserOptions: {
ecmaVersion: 6, // ES 语法版本
sourceType: "module", // ES 模块化
ecmaFeatures: { // ES 其他特性
jsx: true // 如果是 React 项目,就需要开启 jsx 语法
}
}
rules 具体规则
-
"off"
或0
- 关闭规则 -
"warn"
或1
- 开启规则,使用警告级别的错误:warn
(不会导致程序退出) -
"error"
或2
- 开启规则,使用错误级别的错误:error
(当被触发的时候,程序会退出)
rules: {
semi: "error", // 禁止使用分号
'array-callback-return': 'warn', // 强制数组方法的回调函数中有 return 语句,否则警告
'default-case': [
'warn', // 要求 switch 语句中有 default 分支,否则警告
{ commentPattern: '^no default$' } // 允许在最后注释 no default, 就不会有警告了
],
eqeqeq: [
'warn', // 强制使用 === 和 !==,否则警告
'smart' // https://eslint.bootcss.com/docs/rules/eqeqeq#smart 除了少数情况下不会有警告
],
}
更多规则详见:规则文档
extends 继承
开发中一点点写 rules 规则太费劲了,所以有更好的办法,继承现有的规则。
现有以下较为有名的规则:
-
Eslint 官方的规则open in new window:
eslint:recommended
-
Vue Cli 官方的规则open in new window:
plugin:vue/essential
-
React Cli 官方的规则open in new window:
react-app
module.exports = {
extends: ["eslint:recommended"],
rules: {
// 我们的规则会覆盖掉react-app的规则
// 所以想要修改规则直接改就是了
eqeqeq: ["warn", "smart"],
},
};
Esline在Webpack中的使用
安装
npm install eslint-webpack-plugin eslint --save-dev
然后把插件添加到你的 webpack 配置。例如:
const ESLintPlugin = require('eslint-webpack-plugin');
module.exports = {
// ...
plugins: [
new ESLintPlugin({
// 指定文件的根目录
context:path.resolve(__dirname, "src"),
})
],
// ...
};
在根目录新建 .eslintrc.js
module.exports = {
// 继承 Eslint 规则
extends: ["eslint:recommended"],
env: {
node: true, // 启用node中全局变量
browser: true, // 启用浏览器中全局变量
},
parserOptions: {
ecmaVersion: 6,
sourceType: "module",
},
rules: {
"no-var": 2, // 不能使用 var 定义变量
},
};
我们编写一个错误的代码
在打包时会提示错误,并终止打包
在 webstorm 中可以开启 eslint 检测
这样在编写代码时就可以发现错误
添加eslint忽略文件
新建 .eslintignore
文件
# 忽略dist目录下所有文件
dist
9.2 Babel 插件
JavaScript 编译器。
主要用于将 ES6 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中
配置文件
配置文件由很多种写法:
-
babel.config.*
:新建文件,位于项目根目录
babel.config.js
babel.config.json
-
.babelrc.*
:新建文件,位于项目根目录
.babelrc
.babelrc.js
.babelrc.json
-
package.json
中babel
:不需要创建文件,在原有文件基础上写
Babel 会查找和自动读取它们,所以以上配置文件只需要存在一个即可
配置示例
我们以 babel.config.js
配置文件为例:
module.exports = {
// 预设
presets: ["@babel/preset-env"],
};
presets 预设
简单理解:就是一组 Babel 插件, 扩展 Babel 功能
-
@babel/preset-env
: 一个智能预设,允许您使用最新的 JavaScript。 -
@babel/preset-react
:一个用来编译 React jsx 语法的预设 -
@babel/preset-typescript
:一个用来编译 TypeScript 语法的预设
在webpack中使用
安装
npm install -D babel-loader @babel/core @babel/preset-env
用法一:
在 webpack 配置对象中,需要将 babel-loader 添加到 module 列表中,就像下面这样
module: {
rules: [
{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/, // 排除文件
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
}
用法二(推荐用法):
在项目根目录新建 babel.config.js
module.exports = {
presets: ['@babel/preset-env']
}
然后在 webpack 配置对象中添加
module: {
rules: [
{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/, // 排除文件
use: {
loader: 'babel-loader',
}
}
]
}
打包前生成的文件代码
打包后的文件代码
修改输出的结果不以箭头输出
在 output 中 添加 environment 配置
// 输出
output: {
// 要输出到的位置,绝对路径
path: path.resolve(__dirname,"dist"),
// 文件名
filename: "js/main.js",
// 每次打包前自动清空path对应的目录文件
clean: true,
environment: {
// 关闭箭头函数输出
arrowFunction: false
}
},
10. 处理HTML文件
我们希望每次打包后可以为我们自动的引入JS,通过如下方式设置
安装
npm install --save-dev html-webpack-plugin
引入和使用
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
module.exports = {
plugins: [
// 处理HTML文件
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "index.html"), // 使用的模板地址
title: "学习Webpack", // 网站title
hash:true, // 添加哈希值
})
],
};
设置了 title 后需要在模板中修改 title 的设置方式,需要动态获取
<title><%= htmlWebpackPlugin.options.title %></title>
<!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
打包后生成的文件
<!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文件才能看到效果,我们可以通过搭建开发服务器,可以实时的查看改动
安装
npm i webpack-dev-server -D
添加配置
// 开发服务器
devServer: {
host: "localhost", // 启动服务器域名
port: "3000", // 启动服务器端口号
open: true, // 是否自动打开浏览器
},
然后通过如下命令启动
npx webpack serve
启动成功后光标会停留在下面,同时自动打开浏览器,我们修改代码后刷新浏览器会自动生效
12. 生产环境准备工作
根目录新建 config 文件,将原来的 webpck.config.js 放在 config 文件中,并复制一份配置
修改两个配置文件名分别为
- webpack.dev.js
- webpack.prod.js
然后再所有的相对路径前添加 ../
往外跳一层
删除 dev 配置文件中的 output 配置,因为dev环境不需要打包,删除 prod 配置中的 devServer 配置
设置好的配置文件分别如下
- webpack.dev.js
const path = require("path");
const ESLintPlugin = require('eslint-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// 入口文件,相对路径
entry: "./src/main.js",
// 加载器
module: {
rules: [
{
// 用来匹配 .css 结尾的文件
test: /\.css$/,
// use 数组里面 Loader 执行顺序是从右到左
use: ["style-loader", "css-loader"],
},
{
test: /\.less$/i,
use: [
// compiles Less to CSS
'style-loader',
'css-loader',
'less-loader',
],
},
{
test: /\.s[ac]ss$/i,
use: [
// 将 JS 字符串生成为 style 节点
'style-loader',
// 将 CSS 转化成 CommonJS 模块
'css-loader',
// 将 Sass 编译成 CSS
'sass-loader',
],
},
{
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]'
}
},
{
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: [
// Eslint插件
new ESLintPlugin({
// 指定文件的根目录
context:path.resolve(__dirname, "../src"),
}),
// 处理HTML文件
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "../index.html"), // 使用的模板地址
title: "学习Webpack", // 网站title
hash:true, // 添加哈希值
})
],
// 模式
mode: "development",
// 开发服务器
devServer: {
host: "localhost", // 启动服务器域名
port: "3000", // 启动服务器端口号
open: true, // 是否自动打开浏览器
liveReload:true,
},
}
- webpack.build.js
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",
// 每次打包前自动清空path对应的目录文件
clean: true,
// 关闭箭头函数输出
environment: {
arrowFunction: false
}
},
// 加载器
module: {
rules: [
{
// 用来匹配 .css 结尾的文件
test: /\.css$/,
// use 数组里面 Loader 执行顺序是从右到左
use: ["style-loader", "css-loader"],
},
{
test: /\.less$/i,
use: [
// compiles Less to CSS
'style-loader',
'css-loader',
'less-loader',
],
},
{
test: /\.s[ac]ss$/i,
use: [
// 将 JS 字符串生成为 style 节点
'style-loader',
// 将 CSS 转化成 CommonJS 模块
'css-loader',
// 将 Sass 编译成 CSS
'sass-loader',
],
},
{
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]'
}
},
{
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: [
// Eslint插件
new ESLintPlugin({
// 指定文件的根目录
context:path.resolve(__dirname, "../src"),
}),
// 处理HTML文件
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "../index.html"), // 使用的模板地址
title: "学习Webpack", // 网站title
hash:true, // 添加哈希值
})
],
// 模式
mode: "production",
}
然后修改 package.json 文件,设置启动命令和打包命令
"scripts": {
"dev": "webpack serve --config ./config/webpack.dev.js",
"build": "webpack --config ./config/webpack.prod.js"
}
13. CSS处理
13.1 提取CSS成单独文件
安装
npm install --save-dev mini-css-extract-plugin
需要将原来的 style-loader 换成 MiniCssExtractPlugin.loader,并在 plugins 使用插件
module.exports = {
plugins: [new MiniCssExtractPlugin({
filename:"state/css/main.css" // 指定css文件输出位置
})],
module: {
rules: [
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
],
},
};
然后配合上面的 html-webpack-plugin 插件,可以自动完成资源引入
打包后生成的目录如下
13.2 处理CSS兼容性问题
有些CSS样式在部分浏览器中可能会存在兼容性问题,我们通过配置可以来处理这部分的兼容问题
安装
npm i postcss-loader postcss postcss-preset-env -D
添加配置。这个配置必须放在 css-loader 后面
rules:[
{
// 用来匹配 .css 结尾的文件
test: /\.css$/,
// use 数组里面 Loader 执行顺序是从右到左
use: [MiniCssExtractPlugin.loader, "css-loader", {
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
"postcss-preset-env", // 能解决大多数样式兼容性问题
],
},
},
}],
},
]
在 package.json 文件中添加配置
"browserslist": [
"ie >= 6"
]
这个意思表示兼容到 ie6
我们尝试在代码中添加如下样式
.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;
}
然后执行打包命令,查看打包后的代码
.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;
}
可以看到加了一些兼容性的代码
但是通常情况下我们一般不考虑旧版本浏览器了,所以我们可以这样设置:
"browserslist": [
"last 2 version",
"> 1%",
"not dead"
]
-
last 2 version
只考虑浏览器最近的两个版本 -
> 1%
覆盖99%的浏览器 -
not dead
不考虑已经死掉的版本
上面三个配置取交集做兼容处理
13.3 提取重复配置
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",
// 每次打包前自动清空path对应的目录文件
clean: true,
// 关闭箭头函数输出
environment: {
arrowFunction: false
}
},
// 加载器
module: {
rules: [
{
// 用来匹配 .css 结尾的文件
test: /\.css$/,
// use 数组里面 Loader 执行顺序是从右到左
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 // 小于10kb的图片会被base64处理
}
},
generator: {
// 修改图片文件的输出地址
// hash:10 只取10位哈希值
// ext图片后缀名
// query 图片文件后面携带的参数
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: [
// Eslint插件
new ESLintPlugin({
// 指定文件的根目录
context:path.resolve(__dirname, "../src"),
}),
// 处理HTML文件
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "../index.html"), // 使用的模板地址
title: "学习Webpack", // 网站title
hash:true, // 添加哈希值
}),
// 将CSS处理成单独文件
new MiniCssExtractPlugin({
filename:"state/css/main.css"
})
],
// 模式
mode: "production",
}
13.4 压缩CSS
下载依赖
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
plugins:[
// 压缩CSS
new CssMinimizerPlugin()
]
打包看效果
14. 高级优化
14.1 提升开发体验SourceMap
开发时我们运行的代码是经过 webpack 编译后的,例如下面这个样子:
如果代码在某一行出现问题,我们调试起来很不方便,但是通过 SourceMap 会创建一种映射,我们可以直接定位到源代码,从而快速方便的查看是哪一行代码出问题
通过查看Webpack DevTool 文档open in new window可知,SourceMap 的值有很多种情况.
但实际开发时我们只需要关注两种情况即可:
- 开发模式:
cheap-module-source-map
- 优点:打包编译速度快,只包含行映射
- 缺点:没有列映射
module.exports = {
// 其他省略
mode: "development",
devtool: "cheap-module-source-map",
};
-
生产模式:
source-map
- 优点:包含行/列映射
- 缺点:打包编译速度更慢
module.exports = {
// 其他省略
mode: "production",
devtool: "source-map",
};
14.2 Hot热替换
为什么
开发时我们修改了其中一个模块代码,Webpack 默认会将所有模块全部重新打包编译,速度很慢。
所以我们需要做到修改某个模块代码,就只有这个模块代码需要重新打包编译,其他模块不变,这样打包速度就能很快。
是什么
HotModuleReplacement(HMR/热模块替换):在程序运行中,替换、添加或删除模块,而无需重新加载整个页面。
怎么用
- 基本配置
module.exports = {
// 其他省略
devServer: {
host: "localhost", // 启动服务器域名
port: "3000", // 启动服务器端口号
open: true, // 是否自动打开浏览器
hot: true, // 开启HMR功能(只能用于开发环境,生产环境不需要了)
},
};
此时 css 样式经过 style-loader 处理,已经具备 HMR 功能了。 但是 js 还不行。
- JS 配置
// main.js
import count from "./js/count";
import sum from "./js/sum";
// 引入资源,Webpack才会对其打包
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);
// 判断是否支持HMR功能
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, 剩下的就不匹配了
怎么用
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",
// 每次打包前自动清空path对应的目录文件
clean: true,
// 关闭箭头函数输出
environment: {
arrowFunction: false
}
},
// 加载器
module: {
rules: [
{
oneOf: [
{
// 用来匹配 .css 结尾的文件
test: /\.css$/,
// use 数组里面 Loader 执行顺序是从右到左
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 // 小于10kb的图片会被base64处理
}
},
generator: {
// 修改图片文件的输出地址
// hash:10 只取10位哈希值
// ext图片后缀名
// query 图片文件后面携带的参数
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: [
// Eslint插件
new ESLintPlugin({
// 指定文件的根目录
context:path.resolve(__dirname, "../src"),
}),
// 处理HTML文件
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "../index.html"), // 使用的模板地址
title: "学习Webpack", // 网站title
hash:true, // 添加哈希值
}),
// 将CSS处理成单独文件
new MiniCssExtractPlugin({
filename:"state/css/main.css"
}),
// 压缩CSS
new CssMinimizerPlugin()
],
// 模式
mode: "production",
devtool: "source-map"
}
14.4 Include/Exclude
为什么
开发时我们需要使用第三方的库或插件,所有文件都下载到 node_modules 中了。而这些文件是不需要编译可以直接使用的。
所以我们在对 js 文件处理时,要排除 node_modules 下面的文件。
是什么
- include
包含,只处理 xxx 文件
- exclude
排除,除了 xxx 文件以外其他文件都处理
怎么用
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$/,
// exclude: /(node_modules|bower_components)/, // 排除文件
include: path.resolve(__dirname,"../src"), // 也可以设置只包含某个文件夹下的JS
use: {
loader: 'babel-loader',
}
}
]
},
// 插件
plugins: [
// Eslint插件
new ESLintPlugin({
// 指定文件的根目录
context:path.resolve(__dirname, "../src"),
exclude:["node_modules"] // 排除 node_modules 文件夹下的文件,默认是node_module
}),
// 处理HTML文件
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "../index.html"), // 使用的模板地址
title: "学习Webpack", // 网站title
hash:true, // 添加哈希值
})
],
// 模式
mode: "development",
devtool: "cheap-module-source-map",
// 开发服务器
devServer: {
host: "localhost", // 启动服务器域名
port: "3000", // 启动服务器端口号
open: true, // 是否自动打开浏览器
hot:true, // 热替换,改变JS或者CSS是可以不刷新页面就看到最新效果
},
}
14.5 Catch
为什么
每次打包时 js 文件都要经过 Eslint 检查 和 Babel 编译,速度比较慢。
我们可以缓存之前的 Eslint 检查 和 Babel 编译结果,这样第二次打包时速度就会更快了
怎么用
rules:[
{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/, // 排除文件
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true, // 开启babel编译缓存
cacheCompression: false, // 缓存文件不要压缩
}
}
}
]
eslint-webpack-plugin
默认已经开启了缓存
打包后查看 node_modules/.catch
14.6 开启多线程打包
为什么
当我们项目文件足够大,数量足够多时,执行打包操作会耗时很长,这时我们就可以通过设置开启多线程打包提高打包速度
安装
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
怎么用
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",
// 每次打包前自动清空path对应的目录文件
clean: true,
// 关闭箭头函数输出
environment: {
arrowFunction: false
}
},
// 加载器
module: {
rules: [
{
oneOf: [
{
// 用来匹配 .css 结尾的文件
test: /\.css$/,
// use 数组里面 Loader 执行顺序是从右到左
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 // 小于10kb的图片会被base64处理
}
},
generator: {
// 修改图片文件的输出地址
// hash:10 只取10位哈希值
// ext图片后缀名
// query 图片文件后面携带的参数
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前加上 thread-loader
"thread-loader",
{
loader: 'babel-loader',
options: {
cacheDirectory: true, // 开启babel编译缓存
cacheCompression: false, // 缓存文件不要压缩
}
}
]
}
]
}
]
},
// 插件
plugins: [
// Eslint插件
new ESLintPlugin({
// 指定文件的根目录
context:path.resolve(__dirname, "../src"),
threads:true // 开启多线程,默认线程数是cpu核心数-1
}),
// 处理HTML文件
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "../index.html"), // 使用的模板地址
title: "学习Webpack", // 网站title
hash:true, // 添加哈希值
}),
// 将CSS处理成单独文件
new MiniCssExtractPlugin({
filename:"state/css/main.css",
}),
],
optimization: {
minimize: true,
minimizer: [
// 压缩CSS
new CssMinimizerPlugin({
parallel:true,// 开启多线程
}),
// 当生产模式会默认开启TerserPlugin,但是我们需要进行其他配置,就要重新写了
new TerserPlugin({
parallel:true // 开启多线程
})
]
},
// 模式
mode: "production",
devtool: "source-map"
}
14.7 减少代码体积
为什么
Babel 为编译的每个文件都插入了辅助代码,使代码体积过大!
Babel 对一些公共方法使用了非常小的辅助代码,比如 _extend
。默认情况下会被添加到每一个需要它的文件中。
你可以将这些辅助代码作为一个独立模块,来避免重复引入。
是什么
@babel/plugin-transform-runtime
: 禁用了 Babel 自动对每个文件的 runtime 注入,而是引入 @babel/plugin-transform-runtime
并且使所有辅助代码从这里引用。
怎么用
npm i @babel/plugin-transform-runtime -D
rules:[
{
test: /\.m?js$/,
// exclude: /(node_modules|bower_components)/, // 排除文件
include: path.resolve(__dirname,"../src"), // 也可以设置只包含某个文件夹下的JS
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true, // 开启babel编译缓存
cacheCompression: false, // 缓存文件不要压缩
plugins: ["@babel/plugin-transform-runtime"], // 减少代码体积
}
}
}
]
14.8 压缩图片
下载
npm i image-minimizer-webpack-plugin imagemin -D
根据情况继续下载依赖
无损压缩
npm install imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo -D
有损压缩
npm install imagemin-gifsicle imagemin-mozjpeg imagemin-pngquant imagemin-svgo -D
我们使用无损压缩使用
// 引入
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
设置输出的文件
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"
}
代码目录结构
demo01
├── src
│ ├── app.js
│ └── main.js
├── index.html
├── package.json
└── webpack.config.js
打包后的目录
dist
├── js
│ ├── app.js
│ └── main.js
└── index.html
提取重复文件
在项目中如果我们有两个文件都引用了另外一个文件的内容,但是打包时会吧共同的引用的文件打包两份,这样会造成打包后的体积变大,从而降低加载速度,我们可以通过配置 splitChunks
来解决这种问题
首先创建一个 sum.js
export function sum(...args){
return args.reduce((a,b)=>a+b,0)
}
然后 app.js
和 main.js
分别使用 sum 函数
// app.js
import {sum} from "./sum";
console.log("app")
console.log(sum(1,2,3,4))
// main.js
import {sum} from "./sum";
console.log("main")
console.log(sum(5,6,7,8))
我们现在执行一次打包操作,看看打包后的输出是什么样子
可以看到还是输出了两个文件,并且每个文件里面都有sum函数
添加以下配置信息
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", // 对所有模块都进行分割
// 以下是默认值
// 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,
},
},
},
},
mode: "production"
}
然后再次打包可以看到 sum 函数被单独打包成了一个文件
按需加载
例如页面中一个按钮,点击按钮后会调用一个函数进行加法运算,不点击按钮就不会进行加法运算。这时我们可以使用按需加载的方式来加载这个JS,如果不点击,则不会请求这个文件,从而提高页面首次加载速度
新建 count.js
export default function (a,b){
return a + b
}
在 main.js
中添加代码
document.getElementById("btn").onclick = function (){
import("./count").then(res=>{
console.log(res.default(1,2))
})
}
然后 index.html
中添加按钮
<body>
<button id="btn">按钮</button>
</body>
打包文件,打开 index.html
Esllint 默认不支持 import 动态导入语法,在 .eslintrc.js 文件中添加配置即可
plugins: ["import"]
给动态生成的文件命名
细心的同学已经发现,我们上面点击按钮是,动态加载了一个叫 293.js 的文件,但是我们在代码中并不是这个名,将来在我们调试的时候可能会分不清楚那个文件是我们要找的文件。
所以我们可以通过编写特定对的注释代码,来对动态加载的文件命名
document.getElementById("btn").onclick = function () {
import(/* webpackChunkName: "count" */ "./js/count").then(res=>{
console.log(res.default(1, 2))
})
}
固定写法,在动态加载的文件前添加 /* webpackChunkName: "count" */
这段注释。count 就是我们想要生成的文件名称
然后再配置文件中添加下面的代码即可
chunkFilename: "state/js/chunk/[name].js",
具体配置
// 入口文件,相对路径
entry: "./src/main.js",
// 输出
output: {
// 要输出到的位置,绝对路径
path: path.resolve(__dirname,"../dist"),
// 文件名
filename: "state/js/main.js",
// 给动态生成的文件命名
chunkFilename: "state/js/chunk/[name].js",
// 每次打包前自动清空path对应的目录文件
clean: true,
// 关闭箭头函数输出
environment: {
arrowFunction: false
}
},
然后打包后的文件会自动放在 chunk 文件夹下
16. 文件统一命名
将凡是涉及到文件输出的地方,统一用 [name].文件后缀
的方式来命名,这样webpack会自动将源文件名设置为打包后的文件名
修改后的配置
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",
// 只要是使用 type:asset 的loader统一设置输出名
// hash:10 只取10位哈希值
// ext文件后缀名
// query 图片文件后面携带的参数
assetModuleFilename: "state/asset/[hash:10][ext][query]",
// 每次打包前自动清空path对应的目录文件
clean: true,
// 关闭箭头函数输出
environment: {
arrowFunction: false
}
},
// 加载器
module: {
rules: [
{
oneOf: [
{
// 用来匹配 .css 结尾的文件
test: /\.css$/,
// use 数组里面 Loader 执行顺序是从右到左
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 // 小于10kb的图片会被base64处理
}
},
// generator: {
// // 修改图片文件的输出地址
// // hash:10 只取10位哈希值
// // ext图片后缀名
// // query 图片文件后面携带的参数
// // 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前加上 thread-loader
"thread-loader",
{
loader: 'babel-loader',
options: {
cacheDirectory: true, // 开启babel编译缓存
cacheCompression: false, // 缓存文件不要压缩
plugins: ["@babel/plugin-transform-runtime"], // 减少代码体积
},
}
]
}
]
}
]
},
// 插件
plugins: [
// Eslint插件
new ESLintPlugin({
// 指定文件的根目录
context:path.resolve(__dirname, "../src"),
threads:true // 开启多线程,默认线程数是cpu核心数-1
}),
// 处理HTML文件
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "../index.html"), // 使用的模板地址
title: "学习Webpack", // 网站title
hash:true, // 添加哈希值
}),
// 将CSS处理成单独文件
new MiniCssExtractPlugin({
filename:"state/css/[name].css",
}),
],
optimization: {
minimize: true,
minimizer: [
// 压缩CSS
new CssMinimizerPlugin({
parallel:true,// 开启多线程
}),
// 当生产模式会默认开启TerserPlugin,但是我们需要进行其他配置,就要重新写了
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
加载。
它们的问题:兼容性较差。
- 我们可以去 Can I Useopen in new window 网站查询 API 的兼容性问题。
-
Preload
相对于Prefetch
兼容性好一点。
怎么用
安装
npm i @vue/preload-webpack-plugin -D
配置
const PreloadWebpackPlugin = require("@vue/preload-webpack-plugin");
// 插件
plugins: [
// 资源懒加载
new PreloadWebpackPlugin({
//rel: "preload", // preload 兼容性更好
as: "script",
rel: 'prefetch' // prefetch 兼容性更差
})
],
配置完成后,点击按钮加载JS就会从缓存中获取,速度比较快
18. contenthash
为什么
我们通过打包发现,每次打包后的文件都叫 main.js,这样浏览器在加载文件时可能会存在缓存问题,我们可以通过设置让每次打包后都加一个哈希值,这样在加载资源时就不会存在缓存问题
是什么
webpack 设置哈希值有几种方式,它们都会生成一个唯一的 hash 值。
- fullhash(webpack4 是 hash)
每次修改任何一个文件,所有文件名的 hash 至都将改变。所以一旦修改了任何一个文件,整个项目的文件缓存都将失效。
- chunkhash
根据不同的入口文件(Entry)进行依赖文件解析、构建对应的 chunk,生成对应的哈希值。我们 js 和 css 是同一个引入,会共享一个 hash 值。
- contenthash
根据文件内容生成 hash 值,只有文件内容变化了,hash 值才会变化。所有文件 hash 值是独享且不同的。
通过对比,contenthash 最适合我们使用
怎么用
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",
// 只要是使用 type:asset 的loader统一设置输出名
// hash:10 只取10位哈希值
// ext文件后缀名
// query 图片文件后面携带的参数
assetModuleFilename: "state/asset/[hash:10][ext][query]",
// 每次打包前自动清空path对应的目录文件
clean: true,
// 关闭箭头函数输出
environment: {
arrowFunction: false
}
},
// 插件
plugins: [
// 将CSS处理成单独文件
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 值和它们与文件关系,整个文件体积就比较小,所以变化重新请求的代价也小。
怎么用
添加如下配置即可
module.exports = {
optimization: {
// 提取runtime文件
runtimeChunk: {
name: (entrypoint) => `runtime~${entrypoint.name}`, // runtime文件命名规则
},
},
}
修改一下 count.js
然后再次打包,通过对比发现,只有count.js和runtime文件的哈希值发生改变,其他均未改变
20. CoreJS
为什么
过去我们使用 babel 对 js 代码进行了兼容性处理,其中使用@babel/preset-env 智能预设来处理兼容性问题。
它能将 ES6 的一些语法进行编译转换,比如箭头函数、点点点运算符等。但是如果是 async 函数、promise 对象、数组的一些方法(includes)等,它没办法处理。
所以此时我们 js 代码仍然存在兼容性问题,一旦遇到低版本浏览器会直接报错。所以我们想要将 js 兼容性问题彻底解决
是什么
core-js
是专门用来做 ES6 以及以上 API 的 polyfill
。
polyfill
翻译过来叫做垫片/补丁。就是用社区上提供的一段代码,让我们在不兼容某些新特性的浏览器上,使用该新特性
怎么用
安装
npm i core-js
使用,修改 babel.config.js
module.exports = {
// 智能预设:能够编译ES6语法
presets: [
[
"@babel/preset-env",
// 按需加载core-js的polyfill
{ 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 项目结构
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
// webpack.dev.js
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: [
{
// 用来匹配 .css 结尾的文件
test: /\.css$/,
// use 数组里面 Loader 执行顺序是从右到左
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, // 小于10kb的图片会被base64处理
},
},
},
{
test: /\.(ttf|woff2?)$/,
type: "asset/resource",
},
// vue-loader不支持oneOf
{
test: /\.vue$/,
loader: "vue-loader", // 内部会给vue文件注入HMR功能代码
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"], // 自动补全文件扩展名,让vue可以使用
},
devServer: {
open: true,
host: "localhost",
port: 3000,
hot: true,
compress: true,
historyApiFallback: true, // 解决vue-router刷新404问题
},
mode: "development",
devtool: "cheap-module-source-map",
};
22.3 生产模式配置
webpack.prod.js
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"),
// JS文件打包的命名:文件名.哈希值.js
filename: "static/js/[name].[contenthash:10].js",
// 映射文件名
chunkFilename: "static/js/[name].[contenthash:10].chunk.js",
// type:asset loader的文件生成名
assetModuleFilename: "static/asset/[hash:10][ext][query]",
// 自动清理上次打包文件
clean: true,
},
module: {
rules: [
{
// 用来匹配 .css 结尾的文件
test: /\.css$/,
// use 数组里面 Loader 执行顺序是从右到左
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, // 小于10kb的图片会被base64处理
},
},
},
{
test: /\.(ttf|woff2?)$/,
type: "asset/resource",
},
{
test: /\.(jsx|js)$/,
include: path.resolve(__dirname, "../src"),
loader: "babel-loader",
options: {
cacheDirectory: true,
cacheCompression: false,
plugins: [
// "@babel/plugin-transform-runtime" // presets中包含了
],
},
},
// vue-loader不支持oneOf
{
test: /\.vue$/,
loader: "vue-loader", // 内部会给vue文件注入HMR功能代码
options: {
// 开启缓存
cacheDirectory: path.resolve(
__dirname,
"node_modules/.cache/vue-loader"
),
},
},
],
},
plugins: [
// Eslint
new ESLintWebpackPlugin({
context: path.resolve(__dirname, "../src"),
exclude: "node_modules",
cache: true,
cacheLocation: path.resolve(
__dirname,
"../node_modules/.cache/.eslintcache"
),
}),
// 自动引入js
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "../public/index.html"),
}),
// 复制public文件到打包目录中
new CopyPlugin({
patterns: [
{
from: path.resolve(__dirname, "../public"),
to: path.resolve(__dirname, "../dist"),
toType: "dir",
noErrorOnMissing: true,
globOptions: {
ignore: ["**/index.html"],
},
info: {
minimized: true,
},
},
],
}),
// 将CSS单独放在一个文件中
new MiniCssExtractPlugin({
filename: "static/css/[name].[contenthash:10].css",
chunkFilename: "static/css/[name].[contenthash:10].chunk.css",
}),
// 处理vue
new VueLoaderPlugin(),
// 处理Vue的警告
new DefinePlugin({
__VUE_OPTIONS_API__: "true",
__VUE_PROD_DEVTOOLS__: "false",
}),
],
optimization: {
// 压缩的操作
minimizer: [
// 压缩CSS
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
module.exports = {
root: true,
env: {
node: true,
},
extends: ["plugin:vue/vue3-essential", "eslint:recommended"],
parserOptions: {
parser: "@babel/eslint-parser",
},
};
babel.config.js
module.exports = {
presets: ["@vue/cli-plugin-babel/preset"],
};
package.json
{
"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
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
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
<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 测试运行
运行测试
npm run dev
运行效果
打包测试
npm run build
打包后的文件
运行没问题
22.6 合并配置
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,
// JS文件打包的命名:文件名.哈希值.js
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",
// type:asset loader的文件生成名
assetModuleFilename: "static/asset/[hash:10][ext][query]",
// 自动清理上次打包文件
clean: true,
},
module: {
rules: [
{
// 用来匹配 .css 结尾的文件
test: /\.css$/,
// use 数组里面 Loader 执行顺序是从右到左
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, // 小于10kb的图片会被base64处理
},
},
},
{
test: /\.(ttf|woff2?)$/,
type: "asset/resource",
},
{
test: /\.(jsx|js)$/,
include: path.resolve(__dirname, "../src"),
loader: "babel-loader",
options: {
cacheDirectory: true,
cacheCompression: false,
plugins: [
// "@babel/plugin-transform-runtime" // presets中包含了
],
},
},
// vue-loader不支持oneOf
{
test: /\.vue$/,
loader: "vue-loader", // 内部会给vue文件注入HMR功能代码
options: {
// 开启缓存
cacheDirectory: path.resolve(
__dirname,
"node_modules/.cache/vue-loader"
),
},
},
],
},
plugins: [
// Eslint
new ESLintWebpackPlugin({
context: path.resolve(__dirname, "../src"),
exclude: "node_modules",
cache: true,
cacheLocation: path.resolve(
__dirname,
"../node_modules/.cache/.eslintcache"
),
}),
// 自动引入js
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "../public/index.html"),
}),
// 复制public文件到打包目录中
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,
},
},
],
}),
// 将CSS单独放在一个文件中
isProduction && new MiniCssExtractPlugin({
filename: "static/css/[name].[contenthash:10].css",
chunkFilename: "static/css/[name].[contenthash:10].chunk.css",
}),
// 处理vue
new VueLoaderPlugin(),
// 处理Vue的警告
new DefinePlugin({
__VUE_OPTIONS_API__: "true",
__VUE_PROD_DEVTOOLS__: "false",
}),
].filter(Boolean),
optimization: {
minimize: isProduction,
// 压缩的操作
minimizer: [
// 压缩CSS
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, // 解决vue-router刷新404问题
},
};
23. ElementPlus按需导入
安装 element-plus
npm install element-plus --save
安装按需导入的依赖
npm install -D unplugin-vue-components unplugin-auto-import
配置
// webpack.config.js
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()],
}),
],
}
然后页面中直接使用即可
<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 执行顺序为:
从右到左,从下到上
。
例如:
// 此时loader执行顺序:loader3 - loader2 - loader1
module: {
rules: [
{
test: /\.js$/,
loader: "loader1",
},
{
test: /\.js$/,
loader: "loader2",
},
{
test: /\.js$/,
loader: "loader3",
},
],
},
// 此时loader执行顺序:loader1 - loader2 - loader3
module: {
rules: [
{
enforce: "pre",
test: /\.js$/,
loader: "loader1",
},
{
// 没有enforce就是normal
test: /\.js$/,
loader: "loader2",
},
{
enforce: "post",
test: /\.js$/,
loader: "loader3",
},
],
},
24.3 自定义loader
初始化项目结构
MyLoader
├── js
│ └── main.js
├── loader
│ └── test-loader.js
├── src
│ └── index.html
├── package.json
└── webpack.config.js
main.js
console.log("hello word")
编写一个简单的loader,test-loader.js
代码如下
module.exports = function (content){
console.log("loader打印内容",content)
return content
}
使用
// webpack.config.js
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"
}
然后安装依赖
npm install webpack webpack-cli html-webpack-plugin -D
然后执行打包命令
npx webpack
观察输出
总结:
loader 接收要处理的文件的源码,在loader内部处理完毕后再返回出去
24.4 同步loader和异步loader
新建同步loader
// loader/async/loader1.js
module.exports = function (content,map,meta){
console.log("同步loader打印")
console.log(meta)
/**
* 第一个参数:错误信息,没有就是null,有的话传错误信息
* content:要处理的源文件信息
* map:继续传递source-map
* meta:给下一个loader传递参数
*/
this.callback(null,content,map,meta)
}
新建异步loader
// loader/async/loader2.js
module.exports = function (content,map,meta){
console.log("异步loader打印")
let callback = this.async()
/**
* 第一个参数:错误信息,没有就是null,有的话传错误信息
* content:要处理的源文件信息
* map:继续传递source-map
* meta:给下一个loader传递参数
*/
setTimeout(()=>{
callback(null,content,map, {
name:"我是来自异步loader的参数"
})
},2000)
}
使用
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"
// }
{
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
// loader/raw/raw-loader.js
module.exports = function (content){
console.log(content)
return content
}
// 暴露raw为true,表示这个是raw loader
module.exports.raw = true
使用
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"
// }
// {
// test:/\.js$/,
// use: [
// "./loader/async/loader1.js",
// "./loader/async/loader2.js",
// ]
// }
{
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
// loader/pitch/pitch1.js
module.exports = function (content){
console.log("pitch-normal-1",content)
return content
}
module.exports.pitch = function (){
console.log("pitch1")
}
定义 pitch loader2
// loader/pitch/pitch1.js
module.exports = function (content){
console.log("pitch-normal-2",content)
return content
}
module.exports.pitch = function (){
console.log("pitch2")
}
使用
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"
// }
// {
// test:/\.js$/,
// use: [
// "./loader/async/loader1.js",
// "./loader/async/loader2.js",
// ]
// }
// {
// test: /\.js$/,
// loader: "./loader/raw/raw-loader.js"
// }
{
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
module.exports = function (content){
console.log("pitch-normal-3",content)
return content
}
module.exports.pitch = function (){
console.log("pitch3")
}
然后修改 pitch2
module.exports = function (content){
console.log("pitch-normal-2",content)
return content
}
module.exports.pitch = function (){
console.log("pitch2")
return "pitch2"
}
修改配置
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
module.exports = function (content){
return content.replace(/console\.log\(.*\);?/g,"")
}
使用
module.exports = {
module: {
rules: [
{
test:/\.js$/,
loader: "./loader/clear-log-loader.js"
}
]
},
}
25.2 给loader添加options参数
上面的 loader 中我们通过 option 添加一个 enable 属性,俩控制是否清除 log
首先在 loader 中添加 enable 配置
module.exports = {
module: {
rules: [
{
test:/\.js$/,
loader: "./loader/clear-log-loader.js",
options: {
enable:false
}
}
]
},
}
修改 clear-log-loader.js
// 定义loader options属性的规则约束
const schema = {
// options的类型是一个object
type:"object",
// 定义options中的属性有哪些
properties:{
// 有enable属性
enable:{
// enable属性是一个布尔值
type:"boolean"
}
},
// 不允许使用未定义的属性
additionalProperties:false
}
module.exports = function (content){
// this.getOptions 获取配置项
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语法
安装
npm i @babel/core @babel/preset-env -D
新建 babel-loader.js
const schema = {
"type": "object",
"properties": {
"presets": {
"type": "array"
}
},
"additionalProperties": true
}
const babel = require("@babel/core");
module.exports = function (content) {
const options = this.getOptions(schema);
// 使用异步loader
const callback = this.async();
// 使用babel对js代码进行编译
babel.transform(content, options, function (err, result) {
callback(err, result.code);
});
};
wabpack配置
rules:[
{
test:/\.js$/,
loader: "./loader/babel-loader.js",
options: {
presets:["@babel/preset-env"],
}
}
]
25.4 file-loader
将文件原封不动的输出
安装
npm i loader-utils -D
file-loader
const loaderUtils = require("loader-utils")
module.exports = function (content){
// 根据文件内容自动生成文件名
const interpolatedName = loaderUtils.interpolateName(
this,
"asset/[name].[hash].[ext]",
{
// 文件内容
content:content
}
)
// 将文件输出出去
this.emitFile(interpolatedName,content)
// 返回 module.export = "文件路径/文件名"
return `module.exports = "${interpolatedName}"`
}
module.exports.raw = true
使用
rules:[
{
test:/\.(png|jpg)$/,
loader: "./loader/file-loader.js",
type:"javascript/auto" // 阻止默认的asset打包配置
}
]
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
源码中可以看到,他们是:
// https://github.com/webpack/tapable/blob/master/lib/index.js
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 方法
class TestPlugin{
constructor() {
console.log("TestPlugin constructor")
}
apply(){
console.log("TestPlugin apply")
}
}
module.exports = TestPlugin
使用
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
class TestPlugin{
constructor() {
console.log("TestPlugin constructor")
}
apply(compiler){
console.log("TestPlugin apply")
// 从文档可知, compile hook 是 SyncHook, 也就是同步钩子, 只能用tap注册
compiler.hooks.compile.tap("TestPlugin", (compilationParams) => {
console.log("compiler.compile()");
});
// 从文档可知, make 是 AsyncParallelHook, 也就是异步并行钩子, 特点就是异步任务同时执行
// 可以使用 tap、tapAsync、tapPromise 注册。
// 如果使用tap注册的话,进行异步操作是不会等待异步操作执行完成的。
compiler.hooks.make.tap("TestPlugin", (compilation) => {
setTimeout(() => {
console.log("compiler.make() 111");
}, 2000);
});
// 使用tapAsync、tapPromise注册,进行异步操作会等异步操作做完再继续往下执行
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");
// 必须返回promise
return new Promise((resolve) => {
resolve();
});
});
// 从文档可知, emit 是 AsyncSeriesHook, 也就是异步串行钩子,特点就是异步任务顺序执行
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
添加调试启动命令
"scripts": {
"debug": "node --inspect-brk ./node_modules/webpack-cli/bin/cli.js"
},
运行
npm run debug
然后随便一个页面打开浏览器的控制台,点击 node 图标
点击后会跳转一个新页面,默认是在程序第一行打断点,我们点击下一步跳转到下一个断点即可跳转到我们打断点的位置
27.4 BannerWebpackPlugin
作用,给打包生成的文件添加作者信息
class BannerWebpackPlugin{
constructor(options) {
this.options = options;
}
apply(compile){
compile.hooks.emit.tapAsync("BannerWebpackPlugin",(compilation,callback)=>{
// 得到将要输出的文件,只获取JS和CSS
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
使用
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
class CleanWebpackPlugin{
apply(compiler) {
// 获取打包输出的目的地址
let outputPath = compiler.options.output.path;
// 获取操作文件的对象
let fs = compiler.outputFileSystem;
// 注册钩子,在文件输出前执行删除旧文件操作
compiler.hooks.emit.tap("CleanWebpackPlugin",(compilation)=>{
// 通过fs删除打包目录下的所有文件
this.removeFiles(fs,outputPath)
})
}
removeFiles(fs,filePath){
// 读取指定文件夹下的文件内容,得到的是一个数组
let files = fs.readdirSync(filePath);
// 遍历文件一个个删除
files.forEach(fileName=>{
// 得到文件全路径
let path = `${filePath}/${fileName}`
// 判断是否是文件夹
let stats = fs.statSync(path);
// isDirectory方法用来判断是否是文件夹
if(stats.isDirectory()){
this.removeFiles(fs,path)
}else{
fs.unlinkSync(path)
}
})
}
}
module.exports = CleanWebpackPlugin
使用
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 TestPlugin()
new BannerWebpackPlugin({
auther:"SongZX"
}),
// 使用自定义的删除文件插件
new CleanWebpackPlugin()
],
mode: "production"
}
27.6 AnalyzeWebpackPlugin
在打包时自动生成一个分析文件大小的 md 文件
新建 analyze-webpack-plugin.js
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
使用
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 TestPlugin()
new BannerWebpackPlugin({
auther:"SongZX"
}),
new CleanWebpackPlugin(),
new AnalyzeWebpackPlugin()
],
mode: "production"
}
效果文章来源:https://www.toymoban.com/news/detail-696249.html
文章来源地址https://www.toymoban.com/news/detail-696249.html
到了这里,关于Webpack5入门到原理的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!