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", // 网站titlehash: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", // 网站titlehash: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", // 网站titlehash: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", // 网站titlehash:true, // 添加哈希值}),// 将CSS处理成单独文件new MiniCssExtractPlugin({filename:"state/css/main.css"})],// 模式mode: "production",
}
13.4 压缩CSS
下载依赖
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
plugins:[// 压缩CSSnew 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", // 网站titlehash:true, // 添加哈希值}),// 将CSS处理成单独文件new MiniCssExtractPlugin({filename:"state/css/main.css"}),// 压缩CSSnew 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"), // 也可以设置只包含某个文件夹下的JSuse: {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", // 网站titlehash: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", // 网站titlehash:true, // 添加哈希值}),// 将CSS处理成单独文件new MiniCssExtractPlugin({filename:"state/css/main.css",}),],optimization: {minimize: true,minimizer: [// 压缩CSSnew 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"), // 也可以设置只包含某个文件夹下的JSuse: {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", // 网站titlehash:true, // 添加哈希值}),// 将CSS处理成单独文件new MiniCssExtractPlugin({filename:"state/css/[name].css",}),],optimization: {minimize: true,minimizer: [// 压缩CSSnew 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文件打包的命名:文件名.哈希值.jsfilename: "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: [// Eslintnew ESLintWebpackPlugin({context: path.resolve(__dirname, "../src"),exclude: "node_modules",cache: true,cacheLocation: path.resolve(__dirname,"../node_modules/.cache/.eslintcache"),}),// 自动引入jsnew 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",}),// 处理vuenew VueLoaderPlugin(),// 处理Vue的警告new DefinePlugin({__VUE_OPTIONS_API__: "true",__VUE_PROD_DEVTOOLS__: "false",}),],optimization: {// 压缩的操作minimizer: [// 压缩CSSnew 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文件打包的命名:文件名.哈希值.jsfilename: 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: [// Eslintnew ESLintWebpackPlugin({context: path.resolve(__dirname, "../src"),exclude: "node_modules",cache: true,cacheLocation: path.resolve(__dirname,"../node_modules/.cache/.eslintcache"),}),// 自动引入jsnew 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",}),// 处理vuenew VueLoaderPlugin(),// 处理Vue的警告new DefinePlugin({__VUE_OPTIONS_API__: "true",__VUE_PROD_DEVTOOLS__: "false",}),].filter(Boolean),optimization: {minimize: isProduction,// 压缩的操作minimizer: [// 压缩CSSnew 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就是normaltest: /\.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.jsconst 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.jsmodule.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.jsmodule.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.jsmodule.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.jsmodule.exports = function (content){console.log("pitch-normal-1",content)return content
}module.exports.pitch = function (){console.log("pitch1")
}
定义 pitch loader2
// loader/pitch/pitch1.jsmodule.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的类型是一个objecttype:"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);// 使用异步loaderconst 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");// 必须返回promisereturn 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和CSSconst 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)})debuggerassetPaths.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"
}
效果