webpack
一、基础
1.1 打包工具简介
1.1.1 为什么需要打包工具?
开发时,我们会使用框架(React、Vue),ES6 模块化语法,Less/Sass 等 css 预处理器等语法进行开发。
这样的代码要想在浏览器运行必须经过编译成浏览器能识别的 JS、Css 等语法,才能运行。
所以我们需要打包工具帮我们做完这些事。
除此之外,打包工具还能压缩代码、做兼容性处理、提升代码性能等。
1.1.2 有哪些打包工具?
- Grunt
- Gulp
- Parcel
- Webpack
- Rollup
- Vite
- ...
1.2 基本使用
1.2.1 webpack简介
Webpack 是一个静态资源打包工具。
它会以一个或多个文件作为打包的入口,将我们整个项目所有文件编译组合成一个或多个文件输出出去。
输出的文件就是编译好的文件,就可以在浏览器运行了。
我们将 Webpack 输出的文件叫做 bundle。
将浏览器不识别的语法(ES6模块化语法,Less/Sass.....)编译成浏览器识别的语法。
1.2.2 功能介绍
Webpack 本身功能是有限的:
- 开发模式:仅能编译 JS 中的
ES Module语法 - 生产模式:能编译 JS 中的
ES Module语法,还能压缩 JS 代码
1.2.3 资源目录
webpack_code # 项目根目录(所有指令必须在这个目录运行)
└── src # 项目源码目录
├── js # js文件目录
│ ├── count.js
│ └── sum.js
└── main.js # 项目主/入口文件1.2.4 创建文件
count.js
jsexport default function count(x, y) { return x - y; }sum.js
jsexport default function sum(...args) { return args.reduce((p, c) => p + c, 0); }main.js
jsimport count from "./js/count"; import sum from "./js/sum"; console.log(count(2, 1)); console.log(sum(1, 2, 3, 4));
在html文件内引入 main.js 会出现报错,原因:浏览器不识别ES6的模块化语法
1.2.5 下载依赖
打开终端,来到项目根目录。运行以下指令:
初始化
package.jsonnpm init -y此时会生成一个基础的
package.json文件需要注意的是
package.json中name字段不能叫做webpack, 否则下一步会报错下载依赖
npm i webpack webpack-cli -D
1.2.6 启用 Webpack
开发模式
npx webpack ./src/main.js --mode=development生产模式(会压缩代码)
npx webpack ./src/main.js --mode=production
npx webpack: 是用来运行本地安装 Webpack 包的。
./src/main.js: 指定 Webpack 从 main.js 文件开始打包,不仅会打包 main.js,还会将其依赖也一起打包进来。
1.2.7 观察输出文件
默认 Webpack 会将文件打包输出到 dist 目录下,我们查看 dist 目录下文件情况就好了
此时在html标签中引入编译打包后的 main.js 就可以正常运行了
1.2.8 小结
Webpack 本身功能比较少,只能处理 js 资源,一旦遇到 css 等其他资源就会报错。
所以我们学习 Webpack,就是主要学习如何处理其他资源。
1.3 基本配置
1.3.1 5大核心概念
- entry(入口)
指示 Webpack 从哪个文件开始打包
- output(输出)
指示 Webpack 打包完的文件输出到哪里去,如何命名等
- loader(加载器)
webpack 本身只能处理 js、json 等资源,其他资源需要借助 loader,Webpack 才能解析
- plugins(插件)
扩展 Webpack 的功能
- mode(模式)
主要由两种模式:
- 开发模式:development
- 生产模式:production
1.3.2 准备 Webpack 配置文件
在项目根目录下新建文件:webpack.config.js(固定写法)
module.exports = {
// 入口
entry: "",
// 输出
output: {},
// 加载器
module: {
rules: [],
},
// 插件
plugins: [],
// 模式
mode: "",
};Webpack 是基于 Node.js 运行的,所以采用 Common.js 模块化规范
1.3.3 修改配置文件
配置文件
js// nodejs模块用于处理路径问题 const path = require("path"); module.export = { // 入口 webpack打包的入口文件 entry: "./src/main.js", // 相对路径 // 输出 output: { // 文件的输出路径 // __dirname 当前文件的文件夹路径 path: path.resolve(__dirname, "dist"), // 绝对路径 // 文件的输出名称 filename: "main.js", }, // loader 加载器 module: { rules: [ // loader 配置 ], }, // plugins 插件 plugins: [ // plugins 配置 ], // mode 模式 mode: "development", };运行指令
npx webpack
此时功能和之前一样,也不能处理样式资源
1.3.4 小结
Webpack 将来都通过 webpack.config.js 文件进行配置,来增强 Webpack 的功能
我们后面会以两个模式来分别搭建 Webpack 的配置,先进行开发模式,再完成生产模式
1.4 开发模式介绍
开发模式顾名思义就是我们开发代码时使用的模式。
这个模式下我们主要做两件事:
- 编译代码,使浏览器能识别运行
开发时我们有样式资源、字体图标、图片资源、html 资源等,webpack 默认都不能处理这些资源,所以我们要加载配置来编译这些资源
- 代码质量检查,树立代码规范
提前检查代码的一些隐患,让代码运行时能更加健壮。
提前检查代码规范和格式,统一团队编码风格,让代码更优雅美观。
1.5 处理样式资源
使用 webpack 处理 Css、Less、Sass、Scss、Styl 样式资源
1.5.1 介绍
Webpack 本身是不能识别样式资源的,所以我们需要借助 Loader 来帮助 Webpack 解析样式资源
在 js 文件中导入样式文件将样式文件打包到 js 文件中
我们找 Loader 都应该去官方文档中找到对应的 Loader,然后使用
1.5.2 处理CSS资源
下载包
npm i css-loader style-loader -D注意:需要下载 webpack.config.js use 中的两个loader
功能介绍
- css-loader:负责将 css 文件编译成 Webpack 能识别的CommonJS模块
- style-loader:会动态创建一个 Style 标签,里面放置 Webpack 中 css 模块内容
此时引入打包后的js文件样式就会以 Style 标签的形式在页面上生效
配置
jsmodule: { rules: [ { // 用来匹配 .css 结尾的文件 test: /\.css$/, // use 数组里面 Loader 执行顺序是从右到左,从下到上 use: ["style-loader", "css-loader"], }, ], },添加css资源
在scr/css/index.css 添加css样式
css.box1 { width: 100px; height: 100px; background-color: pink; }在打包入口文件 src/main.js 引入css文件
jsimport count from "./js/count"; import sum from "./js/sum"; // 想要webpack打包资源,必须在入口文件引入该资源 import "./css/index.css";在html中引入打包后的js文件
html<!-- 引入使用webpack编译打包后的js文件 --> <script src="" data-missing="main.js"></script>
运行打包指令
npx webpack打开 index.html 页面查看效果
1.5.3 处理 Less 资源
下载包
npm i less less-loader -D功能介绍
less-loader:负责将 Less 文件编译成 css 文件less:less-loader依赖less进行编译
配置
js{ test: /\.less$/, use: [ "style-loader", "css-loader", "less-loader" ], },添加Less资源
在src/less/index.less添加less内容
css.box2 { width: 100px; height: 100px; background-color: deeppink; }在入口文件src/main.js 中引入less文件
jsimport "./less/index.less"在index.html中引入打包后的js文件
html<script src="" data-missing="main.js"></script>
运行指令
npx webpack打开 index.html 页面查看效果
1.5.4 处理 Sass 和 Scss 资源
下载包
npm i sass-loader sass -D下载 sass 和 sass-loader包
功能介绍
sass-loader: 负责将Sass文件编译成css文件sass:sass-loader依赖sass进行编译
配置
js{ test:/\.s[ac]ss$/, use:[ "style-loader",// 将JS字符串生成style标签 "css-loader",// 将CSS转化为 CommondJS 模块 "sass-loader",// 将Sass编译成CSS ] }添加 Sass 资源
scr/sass/index.sass
/* 可以省略大括号和分号 */ .box3 width: 100px height: 100px background-color: hotpinkscr/sass/index.scss
scss$body-color: red; .box4 { width: 300px; height: 200px; background-color: $body-color; }src/main.js
js// 引入要打包 sass 资源 import "./sass/index.sass" import "./sass/index.scss"public/index.html
<!-- 引入使用webpack编译打包后的js文件 --> <script src="" data-missing="main.js"></script>
执行指令
npx webpack
打开 index.html 页面查看效果
1.5.5 处理 Styl 资源
下载包
npm i stylus-loader -D功能介绍
stylus-loader: 负责将 styl 文件编译成 css 文件配置
js{ test:/\.styl$/, use:[ "style-loader", "css-loader", "stylus-loader" // 将stylus文件编译成css文件 ] }添加 Styl 资源
src/styl/index.styl
/* 可以省略大括号、分号、冒号 */ .box width 100px height 100px background-color pinksrc/main.js
import "./stylus/index.styl"public/index.html
运行指令
npx webpack
打开 index.html 页面查看效果
1.6 处理图片资源
过去在 Webpack4 时,我们处理图片资源通过 file-loader 和 url-loader 进行处理
现在 Webpack5 已经将两个 Loader 功能内置到 Webpack 里了,我们只需要简单配置即可处理图片资源
1.6.1 配置
{
test: /\.(png|jpe?g|gif|webp)$/,
type: "asset",
},1.6.2 添加图片资源
略
1.6.3 使用图片资源
略
1.6.4 运行指令
npx webpack1.6.5 输出资源情况
此时如果查看 dist 目录的话,会发现多了三张图片资源
因为 Webpack 会将所有打包好的资源输出到 dist 目录下
1.6.6 对图片资源进行优化
将小于某个大小的图片转化成 data URI 形式(Base64 格式)
{
test: /\.(png|jpe?g|gif|webp)$/,
type: "asset",
parser: {
dataUrlCondition: {
maxSize: 10 * 1024 // 小于10kb的图片会被base64处理
}
}
},- 优点:减少请求数量
- 缺点:体积变得更大
此时输出的图片文件就只有两张,有一张图片以 data URI 形式内置到 js 中了 (注意:需要将上次打包生成的文件清空,再重新打包才有效果)
1.7 修改输出资源的名称和路径
1.7.1 配置
const path = require("path");
module.exports = {
// 入口 webpack打包的入口文件
entry: "./src/main.js", // 相对路径
// 输出
output: {
// 所有文件的输出路径
// __dirname 当前文件的文件夹路径
path: path.resolve(__dirname, "dist"), // 绝对路径
// 入口文件的输出名称
filename: "static/js/main.js",
},
module: {
rules: [
// 图片
{
test: /\.(png|jpe?g|gif|webp)$/,
type: "asset",
parser: {
dataUrlCondition: {
maxSize: 10 * 1024, // 小于10kb的图片会被base64处理
},
},
generator: {
// 将图片文件输出到 static/imgs 目录中
// 将图片文件命名 [hash:8][ext][query]
// [hash:8]: hash值取8位
// [ext]: 使用之前的文件扩展名
// [query]: 添加之前的query参数
filename: "static/imgs/[hash:8][ext][query]",
},
],
},
plugins: [],
mode: "development",
};1.7.2 修改 index.html
<!-- 修改 js 资源路径 -->
<script src="" data-missing="main.js"></script>1.7.3 运行指令
npx webpack- 此时输出文件目录:
(注意:需要将上次打包生成的文件清空,再重新打包才有效果)
├── dist
└── static
├── imgs
│ └── 7003350e.png
└── js
└── main.js1.8 自动清空上次打包资源
1.8.1 配置
output: {
path: path.resolve(__dirname, "dist"),
filename: "static/js/main.js",
clean: true, // 自动将上次打包目录资源清空
},1.8.2 运行指令
npx webpack1.9 处理字体图标资源
1.9.1 下载字体图标文件
- 打开阿里巴巴矢量图标库
- 选择想要的图标添加到购物车,统一下载到本地
1.9.2 添加字体图标资源
- src/fonts/iconfont.ttf
- src/fonts/iconfont.woff
- src/fonts/iconfont.woff2
- src/css/iconfont.css
- 注意字体文件路径需要修改
- src/main.js
// 引入要打包 iconfont 资源
import "./css/iconfont.css"- public/index.html
<!-- 使用字体图标 -->
<span class="iconfont icon-aichegujiabeifen7"></span>1.9.3 配置
{
test: /\.(ttf|woff2?)$/,
type: "asset/resource",
generator: {
filename: "static/media/[hash:8][ext][query]",
},
},type: "asset/resource"和type: "asset"的区别:
type: "asset/resource"相当于file-loader, 将文件转化成 Webpack 能识别的资源,其他不做处理type: "asset"相当于url-loader, 将文件转化成 Webpack 能识别的资源,同时小于某个大小的资源会处理成 data URI(base64) 形式
1.9.4. 运行
npx webpack1.10 处理其他资源
开发中可能还存在一些其他资源,如音视频等,我们也一起处理了
1.10.1 配置
在处理字体配置的test中添加上对其它资源的正则匹配
{
test:/\.(ttf|woff|woff2|eot|mp3|mp4|ogg)$/,
type:"asset/resource",
generator:{
filename:"stastic/media/[hash:10][ext][query]"
},
},1.10.4. 运行
npx webpack1.11 处理js资源
Webpack 对 js 处理是有限的,只能编译 js 中 ES 模块化语法,不能编译其他语法,导致 js 不能在 IE 等浏览器运行,所以我们希望做一些兼容性处理。
其次开发中,团队对代码格式是有严格要求的,我们不能由肉眼去检测代码格式,需要使用专业的工具来检测。
- 针对 js 兼容性处理,我们使用 Babel 来完成
- 针对代码格式,我们使用 Eslint 来完成
我们先完成 Eslint,检测代码格式无误后,在由 Babel 做代码兼容性处理
1.11.1 Eslint
可组装的 JavaScript 和 JSX 检查工具。
这句话意思就是:它是用来检测 js 和 jsx 语法的工具,可以配置各项功能
我们使用 Eslint,关键是写 Eslint 配置文件,里面写上各种 rules 规则,将来运行 Eslint 时就会以写的规则对代码进行检查
1.配置文件
配置文件由很多种写法:
.eslintrc.*:新建文件,位于项目根目录
.eslintrc.eslintrc.js常用
.eslintrc.json- 区别在于配置格式不一样
package.json中eslintConfig:不需要创建文件,在原有文件基础上写
ESLint 会查找和自动读取它们,所以以上配置文件只需要存在一个即可
2. 具体配置
我们以 .eslintrc.js 配置文件为例:
module.exports = {
// 解析选项
parserOptions: {},
// 具体检查规则
rules: {},
// 继承其他规则
extends: [],
// ...
// 其他规则详见:https://eslint.bootcss.com/docs/user-guide/configuring
};parserOptions 解析选项
jsparserOptions: { ecmaVersion: 6, // ES 语法版本 sourceType: "module", // ES 模块化 ecmaFeatures: { // ES 其他特性 jsx: true // 如果是 React 项目,就需要开启 jsx 语法 } }rules 具体规则
"off"或0- 关闭规则"warn"或1- 开启规则,使用警告级别的错误:warn(不会导致程序退出)"error"或2- 开启规则,使用错误级别的错误:error(当被触发的时候,程序会退出)jsrules: { 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 官方的规则:
eslint:recommendedVue Cli 官方的规则:
plugin:vue/essentialReact Cli 官方的规则:
react-appjs// 例如在React项目中,我们可以这样写配置 module.exports = { extends: ["react-app"], rules: { // 我们的规则会覆盖掉react-app的规则 // 所以想要修改规则直接改就是了 eqeqeq: ["warn", "smart"], }, };
3. 在 Webpack 中使用
- 下载包
npm i eslint-webpack-plugin eslint -D配置
webpack.config.js
js// 引入插件 const ESLintPlugin = require('eslint-webpack-plugin'); plugins: [ new ESLintWebpackPlugin({ // 指定检查文件的根目录 context: path.resolve(__dirname, "src"), }), ],
定义 Eslint 配置文件
.eslintrc.js
jsmodule.exports = { // 继承 Eslint 规则 extends: ["eslint:recommended"], env: { node: true, // 启用node中全局变量 browser: true, // 启用浏览器中全局变量 }, parserOptions: { ecmaVersion: 6, sourceType: "module", }, rules: { "no-var": 2, // 不能使用 var 定义变量 自定义规则会覆盖继承的规则 }, };
运行
npx webpack
4.VSCode Eslint 插件
打开 VSCode,下载 Eslint 插件,即可不用编译就能看到错误,可以提前解决
但是此时就会对项目所有文件默认进行 Eslint 检查了,我们 dist 目录下的打包后文件就会报错。但是我们只需要检查 src 下面的文件,不需要检查 dist 下面的文件。
所以可以使用 Eslint 忽略文件解决。在项目根目录新建下面文件:
.eslintignore
# 忽略dist目录下所有文件 dist
1.11.2 Babel
JavaScript 编译器。
主要用于将 ES6 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中
1. 配置文件
配置文件有很多种写法:
babel.config.*:新建文件,位于项目根目录
babel.config.js
babel.config.json.babelrc.*:新建文件,位于项目根目录
.babelrc
.babelrc.js.babelrc.json
package.json中babel:不需要创建文件,在原有文件基础上写
Babel 会查找和自动读取它们,所以以上配置文件只需要存在一个即可
2. 具体配置
我们以 babel.config.js 配置文件为例:
module.exports = {
// 预设
presets: [],
};- presets 预设
简单理解:就是一组 Babel 插件, 扩展 Babel 功能
@babel/preset-env: 一个智能预设,允许您使用最新的 JavaScript。@babel/preset-react:一个用来编译 React jsx 语法的预设@babel/preset-typescript:一个用来编译 TypeScript 语法的预设
3. 在 Webpack 中使用
下载包
npm i babel-loader @babel/core @babel/preset-env -D配置
- webpack.config.js
js{ test: /\.js$/, // 匹配所有的js文件 exclude: /node_modules/, // 排除node_modules文件夹下的文件 loader: "babel-loader", /* 可以设置在全局的 babel.config.js 文件中 options: { presets: ['@babel/preset-env'], }, */ },- babel.config.js
jsmodule.exports = { // 智能预设,能编译ES6+的代码 presets: ["@babel/preset-env"], }运行指令
npx webpack打开打包后的 dist/static/js/main.js 文件查看,会发现箭头函数等 ES6 语法已经转换了
1.12 处理 Html 资源
自动在 html 文件中自动引入打包生成的文件
plugin插件使用步骤:下载 、引入 、new调用
1.12.1 下载包
npm i html-webpack-plugin -D1.12.2 配置
webpack.config.js 中引入
js// 引入HtmlWebpackPlugin插件 const HtmlWebpackPlugin = require("html-webpack-plugin");使用
jsplugins: [ // HtmlWebpackPlugin 配置 new HtmlWebpackPlugin({ // template: 以 public/index.html 为模板创建文件 // 新的html文件有两个特点:1.结构和源文件一致 2.自动引入打包生成的js等资源 template: path.resolve(__dirname, "public/index.html"), }), // 会在dist目录下生成一个index.html文件 ],
1.12.3 运行
npx webpack此时在dist文件夹下会输出一个 index.html 文件,并会自动引入static 文件夹下的 main.js
1.13 开发服务器&自动化
每次写完代码都需要手动输入指令才能编译代码,太麻烦了,我们希望一切自动化
1.13.1 下载包
npm i webpack-dev-server -Dwebpack-dev-server:在webpack开发环境中搭建一个开发服务器,会自动监视 src 目录下的文件,当文件发生变化时会自动打包
1.13.2 配置
webpack.config.js
jsplugins: [ ], // 开发服务器 devServer: { host: "localhost", // 启动服务器域名 port: "3000", // 启动服务器端口号 open: true, // 是否自动打开浏览器 }, mode: "development",
1.13.3 运行指令
npx webpack server注意运行指令发生了变化
并且当你使用开发服务器时,所有代码都会在内存中编译打包,并不会输出到 dist 目录下。
开发时我们只关心代码能运行,有效果即可,至于代码被编译成什么样子,我们并不需要知道。
1.14 生产模式介绍
生产模式是开发完成代码后,我们需要得到代码将来部署上线。
这个模式下我们主要对代码进行优化,让其运行性能更好。
优化主要从两个角度出发:
- 优化代码运行性能
- 优化代码打包速度
1.15 生产模式准备
我们分别准备两个配置文件来放开发模式和生产模式的配置
1.15.1 文件目录
├── webpack-test (项目根目录)
├── config (Webpack配置文件目录)
│ ├── webpack.dev.js(开发模式配置文件)
│ └── webpack.prod.js(生产模式配置文件)
├── node_modules (下载包存放目录)
├── src (项目源码目录,除了html其他都在src里面)
│ └── 略
├── public (项目html文件)
│ └── index.html
├── .eslintrc.js(Eslint配置文件)
├── babel.config.js(Babel配置文件)
└── package.json (包的依赖管理配置文件)1.15.2 修改 webpack.dev.js
因为文件目录变了,所以所有绝对路径需要回退一层目录才能找到对应的文件,相对路径不需要改变,相对路径相对的是项目运行的路径
module.exports = {
entry: "./src/main.js",
output: {
path: undefined, // 开发模式没有输出,不需要指定输出目录
filename: "static/js/main.js", // 打包后输出文件名 必须有
// clean: true, // 开发模式没有输出,不需要清空输出结果
},
....
plugins: [
new ESLintWebpackPlugin({
// 指定检查文件的根目录
context: path.resolve(__dirname, "../src"), // 返回上层路径
}),运行开发模式的指令:
npx webpack serve --config ./config/webpack.dev.js1.15.3 修改 webpack.prod.js
module.exports = {
entry: "./src/main.js",
output: {
path: path.resolve(__dirname, "../dist"), // 生产模式需要输出
filename: "static/js/main.js", // 将 js 文件输出到 static/js 目录中
clean: true, // 清空上次输出
},
.....
// devServer: {
// host: "localhost", // 启动服务器域名
// port: "3000", // 启动服务器端口号
// open: true, // 是否自动打开浏览器
// },
mode: "production",开发环境不需要输出,生产环境输出到指定的dist目录内
生产环境不需要使用自动更新服务
设置生产环境的 mode 为 production
运行生产模式的指令
npx webpack --config ./config/webpack.prod.js1.15.3 配置运行指令
为了方便运行不同模式的指令,我们将指令定义在 package.json 中 scripts 里面
"scripts": {
"start": "npm run dev",
"dev": "webpack server --config ./config/webpack.dev.js",
"build": "webpack --config ./config/webpack.prod.js"
},以后启动指令:
- 开发模式:
npm start或npm run dev - 生产模式:
npm run build
1.16 CSS处理
1.16.1 提取 Css 成单独文件
Css 文件目前被打包到 js 文件中,当 js 文件加载时,会创建一个 style 标签来生成样式
这样对于网站来说,会出现闪屏现象,用户体验不好
我们应该是单独的 Css 文件,通过 link 标签加载性能才好
下载包
npm i mini-css-extract-plugin -D配置
- webpack.prod.js
js// 1.引入MiniCssExtractPlugin插件 const MiniCssExtractPlugin = require("mini-css-extract-plugin"); .... // 2.修改所有的 "style-loader" 为 MiniCssExtractPlugin.loader module: { // loader 配置 rules: [ { test: /\.css$/, use: [ MiniCssExtractPlugin.loader, // 提取css成单独的文件 "css-loader", // 将CSS样式编译成commonjs的模块到js中 ], }, ] } // 3. 配置插件 plugins: [ // MiniCssExtractPlugin 配置 // 提取css成单独文件 new MiniCssExtractPlugin({ // 定义输出文件名和目录 filename: "static/css/main.css", }), ],运行
npm build
1.16.2 Css 兼容性处理
下载包
npm i postcss-loader postcss postcss-preset-env -D配置
- webpack.prod.js
js// 在 less/scss-loader前 css-loader后 添加以下代码 { test: /\.less$/, use: [ MiniCssExtractPlugin.loader, // 提取css成单独的文件 "css-loader", // css兼容处理 { loader: "postcss-loader", options: { postcssOptions: { plugins: [ "postcss-preset-env", // 能解决大多数样式兼容性问题 ], }, }, }, "less-loader", // 将less编译成css ], },控制兼容性
我们可以在
package.json文件中添加browserslist来控制样式的兼容性做到什么程度。json{ // 其他省略 "browserslist": ["ie >= 8"] }以上为了测试兼容性所以设置兼容浏览器 ie8 以上。
实际开发中我们一般不考虑旧版本浏览器了,所以我们可以这样设置:
json{ // 其他省略 "browserslist": [ "last 2 version", "> 1%", "not dead" ] }运行指令
npm run build
1.16.3 封装样式loader函数
不同的样式loader配置有大量相同的代码,将重复代码进行封装
webpack.prod.js
js// 用来获取处理样式的 loader function getStyleLoader(pre) { return [ // 执行顺序,从右到左(从下到上) MiniCssExtractPlugin.loader, // 提取css成单独的文件 "css-loader", // 将CSS样式编译成commonjs的模块到js中 // css兼容处理 { loader: "postcss-loader", options: { postcssOptions: { plugins: [ "postcss-preset-env", // 能解决大多数样式兼容性问题 ], }, }, }, pre, ].filter(Boolean); // 过滤掉未传参的pre } module.exports = { ..... module: { // loader 配置 rules: [ // CSS { test: /\.css$/, // 检测以.css结尾的文件 use: getStyleLoader(), }, // Less { test: /\.less$/, use: getStyleLoader("less-loader"), }, // Sass/Scss { test: /\.s[ac]ss$/, use: getStyleLoader("sass-loader"), }, // Stylus { test: /\.styl$/, use: getStyleLoader("stylus-loader"), }, }
1.16.4 CSS压缩
- 下载包
npm i css-minimizer-webpack-plugin -D- 配置
// 引入
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
// new调用
plugins: [
.....
new CssMinimizerPlugin(),
]运行
npm run build
1.16.5 html 压缩
默认生产模式已经开启了:html 压缩和 js 压缩
不需要额外进行配置
1.17 总结
本章节我们学会了 Webpack 基本使用,掌握了以下功能:
- 两种开发模式
- 开发模式:代码能编译自动化运行
- 生产模式:代码编译优化输出
- Webpack 基本功能
- 开发模式:可以编译 ES Module 语法
- 生产模式:可以编译 ES Module 语法,压缩 js 代码
- Webpack 配置文件
- 5 个核心概念
- entry
- output
- loader
- plugins
- mode
- devServer 配置
- Webpack 脚本指令用法
webpack直接打包输出webpack serve启动开发服务器,内存编译打包没有输出
二、高级优化
2.1 介绍
本章节主要介绍 Webpack 高级配置。
所谓高级配置其实就是进行 Webpack 优化,让我们代码在编译/运行时性能更好~
我们会从以下角度来进行优化:
- 提升开发体验
- 提升打包构建速度
- 减少代码体积
- 优化代码运行性能
2.2 提升开发体验
2.2.1 SourceMap
为什么
开发时我们运行的代码是经过 webpack 编译后的,例如下面这个样子:
/*
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/confi/ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../node_modules/css-loader/dist/runtime/api.js */ \"./node_modules/css-loadsourceURL=webpack://webpack5/./src/less/index.less?./node_modules/css-loader/dist/cjs.js!./node_modules/less-loader/dist/cjs.js");
/***/ }),
// 其他省略所有 css 和 js 合并成了一个文件,并且多了其他代码。此时如果代码运行出错那么提示代码错误位置我们是看不懂的。一旦将来开发代码文件很多,那么很难去发现错误出现在哪里。
所以我们需要更加准确的错误提示,来帮助我们更好的开发代码。
是什么
SourceMap(源代码映射)是一个用来生成源代码与构建后代码一一映射的文件的方案。
它会生成一个 xxx.map 文件,里面包含源代码和构建后代码每一行、每一列的映射关系。当构建后代码出错了,会通过 xxx.map 文件,从构建后代码出错位置找到映射后源代码出错位置,从而让浏览器提示源代码文件出错位置,帮助我们更快的找到错误根源
怎么用
通过查看Webpack DevTool 文档可知,SourceMap 的值有很多种情况.
但实际开发时我们只需要关注两种情况即可:
开发模式:
cheap-module-source-map- 优点:打包编译速度快,只包含行映射
- 缺点:没有列映射
jsmodule.exports = { // 其他省略 mode: "development", devtool: "cheap-module-source-map", };生产模式:
source-map- 优点:包含行/列映射
- 缺点:打包编译速度更慢
jsmodule.exports = { // 其他省略 mode: "production", devtool: "source-map", };
2.3 提升打包构建速度
2.3.1 HotModuleReplacement
为什么
开发时我们修改了其中一个模块代码,Webpack 默认会将所有模块全部重新打包编译,速度很慢。
所以我们需要做到修改某个模块代码,就只有这个模块代码需要重新打包编译,其他模块不变,这样打包速度就能很快。
是什么
HotModuleReplacement(HMR/热模块替换):在程序运行中,替换、添加或删除模块,而无需重新加载整个页面
怎么用
用于开发和生产环境中
- 基本配置
module.exports = {
// 其他省略
devServer: {
host: "localhost", // 启动服务器域名
port: "3000", // 启动服务器端口号
open: true, // 是否自动打开浏览器
hot: true, // 开启HMR功能(只能用于开发环境,生产环境不需要了)默认值自动开启
},
};此时 css 样式经过 style-loader 处理,已经具备 HMR 功能了。 但是 js 还不行。
- JS 配置
if (module.hot) {
// 判断是否支持热模块替换功能
// 如果支持,则监听指定模块的变化
module.hot.accept("./js/count.js");
module.hot.accept("./js/sum.js");
}上面这样写会很麻烦,所以实际开发我们会使用其他 loader 来解决。
比如:vue-loader, react-hot-loaderopen。
2.3.2 OneOf
为什么
打包时每个文件都会经过所有 loader 处理,每一个 test 正则都要过一遍。比较慢
是什么
目标文件只匹配上一个 loader, 剩下的就不匹配了。
怎么用
用于开发和生产环境中
rules: [
{
// 每个文件只能被其中一个loader配置处理
oneOf: [
// CSS
{
test: /\.css$/, // 检测以.css结尾的文件
use: [
// 执行顺序,从右到左(从下到上)
"style-loader", // 将js中css的通过创建style标签添加到html文件中生效
"css-loader", // 将CSS样式编译成commonjs的模块到js中
],
},
.....
]
}
]2.3.3 Include/Exclude
为什么
开发时我们需要使用第三方的库或插件,所有文件都下载到 node_modules 中了。而这些文件是不需要编译可以直接使用的。
所以我们在对 js 文件处理时,要排除 node_modules 下面的文件。
是什么
- include
包含,只处理 xxx 文件
- exclude
排除,除了 xxx 文件以外其他文件都处理
怎么用
用于开发和生产环境中
// babel
{
test: /\.js$/,
exclude: /node_modules/, // 排除node_modules文件夹下的文件 默认值
include:path.resolve(__dirname,"../src"),// 只处理src下的文件
loader: "babel-loader",
},2.3.4 Cache
为什么
每次打包时 js 文件都要经过 Eslint 检查 和 Babel 编译,而 js 文件在项目中的占比最大,速度比较慢。
我们可以缓存之前的 Eslint 检查 和 Babel 编译结果,只对更改的部分进行检查和编译,这样第二次打包时速度就会更快了。
是什么
对 Eslint 检查 和 Babel 编译结果进行缓存。
在第二次打包时,不打包所有js文件只针对改变更改的文件进行eslint检查和babel编译即可
怎么用
用于开发和生产环境中
// 1.开启babel缓存
{
test: /\.js$/,
exclude: /node_modules/,
// 排除node_modules文件夹下的文件
loader: "babel-loader",
options: {
// 可以设置在全局的 babel.config.js 文件中
// presets: ['@babel/preset-env'],
cacheDirectory:true, // 开启babel缓存
cacheCompression:false, // 关闭缓存文件压缩
},
},
// 2.开启eslint缓存
plugins: [
// ESLintPlugin 配置
new ESLintPlugin({
// 检测哪些文件
context: path.resolve(__dirname, "../src"),
exclude:"node_modules",
cache:true, // 开启缓存
// 缓存文件路径
cacheLocation:path.resolve(__dirname,"../node_modules/.cache/eslintcache")
}),
]2.3.5 Thread
为什么
当项目越来越庞大时,打包速度越来越慢,甚至于需要一个下午才能打包出来代码。这个速度是比较慢的。
我们想要继续提升打包速度,其实就是要提升 js 的打包速度,因为其他文件都比较少。
而对 js 文件处理主要就是 eslint 、babel、Terser 三个工具,所以我们要提升它们的运行速度。
我们可以开启多进程同时处理 js 文件,这样速度就比之前的单进程打包更快了
是什么
多进程打包:开启电脑的多个进程同时干一件事,速度更快。
需要注意:请仅在特别耗时的操作中使用,因为每个进程启动就有大约为 600ms 左右开销。
怎么用
我们启动进程的数量就是我们 CPU 的核数。
如何获取 CPU 的核数,因为每个电脑都不一样。
js// nodejs核心模块,直接使用 const os = require("os"); // cpu核数 const threads = os.cpus().length;下载包
npm i thread-loader -D使用
js// 1.开启babel编译多线程 { test: /\.js$/, exclude: /node_modules/, // 排除node_modules文件夹下的文件 use: [ { loader: "thread-loader", // 开启多线程编译 options: { works: threads, // 进程数量 }, }, { loader: "babel-loader", options: { // 可以设置在全局的 babel.config.js 文件中 // presets: ['@babel/preset-env'], cacheDirectory: true, // 开启babel缓存 cacheCompression: false, // 关闭缓存文件压缩 }, }, ], } // 2. 开启eslint检查多线程 plugins: [ // ESLintPlugin 配置 new ESLintPlugin({ // 检测哪些文件 context: path.resolve(__dirname, "../src"), exclude: "node_modules", // 开启缓存 cache: true, // 缓存文件路径 cacheLocation: path.resolve( __dirname, "../node_modules/.cache/eslintcache" ), threads, // 开启多线程编译 }), //3. 对打包生成的css和js代码进行压缩 optimization: { // 压缩操作 minimizer: [ new CssMinimizerPlugin(), // 压缩css new TerserWebpackPlugin({ // 压缩js parallel: threads, // 开启多线程压缩 }), ], },我们目前打包的内容都很少,所以因为启动进程开销原因,使用多进程打包实际上会显著的让我们打包时间变得很长。
2.4 减少代码体积
2.4.1 Tree Shaking
为什么
开发时我们定义了一些工具函数库,或者引用第三方工具函数库或组件库。
如果没有特殊处理的话我们打包时会引入整个库,但是实际上可能我们可能只用上极小部分的功能。
这样将整个库都打包进来,体积就太大了。
是什么
Tree Shaking 是一个术语,通常用于描述移除 JavaScript 中的没有使用上的代码。
注意:它依赖
ES Module只针对ES6模块化有效,不能用于CommondJS
怎么用
Webpack 已经默认开启了这个功能,无需其他配置
2.4.2 Babel
为什么
Babel 为编译的每个文件都插入了辅助代码,使代码体积过大!
Babel 对一些公共方法使用了非常小的辅助代码,比如 _extend。默认情况下会被添加到每一个需要它的文件中。
你可以将这些辅助代码作为一个独立模块,来避免重复引入。
是什么
@babel/plugin-transform-runtime: 禁用了 Babel 自动对每个文件的 runtime 注入,而是引入 @babel/plugin-transform-runtime 并且使所有辅助代码从这里引用。
怎么用
下载包
npm i @babel/plugin-transform-runtime -D配置
可以在开发和生产环境中使用
js// 在babel-loader 的option中添加以下代码 plugins: ["@babel/plugin-transform-runtime"], // 减少代码体积 // babel { test: /\.js$/, exclude: /node_modules/, // 排除node_modules文件夹下的文件 use: [ { loader: "thread-loader", // 开启多线程编译 options: { works: threads, // 进程数量 }, }, { loader: "babel-loader", options: { // 可以设置在全局的 babel.config.js 文件中 // presets: ['@babel/preset-env'], cacheDirectory: true, // 开启babel缓存 cacheCompression: false, // 关闭缓存文件压缩 plugins: ["@babel/plugin-transform-runtime"], // 减少代码体积 }, }, ], },
2.4.3 Image Minimizer
为什么
开发如果项目中引用了较多图片,那么图片体积会比较大,将来请求速度比较慢。
我们可以对图片进行压缩,减少图片体积。
注意:如果项目中图片都是在线链接,那么就不需要了。本地项目静态图片才需要进行压缩
是什么
image-minimizer-webpack-plugin: 用来压缩图片的插件
怎么用
下载包
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配置
我们以无损压缩配置为例:
jsconst ImageMinimizerPlugin = require("image-minimizer-webpack-plugin"); ... optimization: { // 压缩操作 minimizer: [ new CssMinimizerPlugin(), // 压缩css new TerserWebpackPlugin({ // 压缩js parallel: threads, // 开启多线程压缩 }), // 压缩图片 new ImageMinimizerPlugin({ minimizer: { implementation: ImageMinimizerPlugin.imageminGenerate, options: { plugins: [ ["gifsicle", { interlaced: true }], ["jpegtran", { progressive: true }], ["optipng", { optimizationLevel: 5 }], [ "svgo", { plugins: [ "preset-default", "prefixIds", { name: "sortAttrs", params: { xmlnsOrder: "alphabetical", }, }, ], }, ], ], }, }, }), ], },打包时会出现报错:
Error: Error with 'src\images\1.jpeg': '"C:\Users\86176\Desktop\webpack\webpack_code\node_modules\jpegtran-bin\vendor\jpegtran.exe"' Error with 'src\images\3.gif': spawn C:\Users\86176\Desktop\webpack\webpack_code\node_modules\optipng-bin\vendor\optipng.exe ENOENT我们需要安装两个文件到 node_modules 中才能解决, 文件可以从课件中找到:
jpegtran.exe
需要复制到
node_modules\jpegtran-bin\vendor下面optipng.exe
需要复制到
node_modules\optipng-bin\vendor下面
2.5 优化代码运行性能
2.5.1 Code Split
为什么
打包代码时会将所有 js 文件打包到一个文件中,体积太大了。我们如果只要渲染首页,就应该只加载首页的 js 文件,其他文件不应该加载。
所以我们需要将打包生成的文件进行代码分割,生成多个 js 文件,渲染哪个页面就只加载某个 js 文件,这样加载的资源就少,速度就更快。
是什么
代码分割(Code Split)主要做了两件事:
- 分割文件:将打包生成的文件进行分割,生成多个 js 文件。
- 按需加载:需要哪个文件就加载哪个文件
怎么用
代码分割实现方式有不同的方式,为了更加方便体现它们之间的差异,我们会分别创建新的文件来演示
1.多入口
文件目录
├── public ├── src | ├── app.js | └── main.js ├── package.json └── webpack.config.js下载包
npm i webpack webpack-cli html-webpack-plugin -D新建文件
内容无关紧要,主要观察打包输出的结果
- app.js
javascriptconsole.log("hello app");- main.js
javascriptconsole.log("hello main");配置
jsconst path = require("path"); const HtmlWebpackPlugin = require("html-webpack-plugin"); module.exports = { // entry:"./src/main.js" // 只有一个入口文件,单入口 entry: { // 有多个入口文件,多入口 为每个入口文件设置名字 app: "./src/app.js", main: "./src/main.js", }, output: { path: path.resolve(__dirname ,"dist"), filename: "[name].js", // webpack命名方式,[name]为入口文件对象名 }, plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname, "public/index.html"), }), ], mode:"production" };运行指令
npx webpack
此时在 dist 目录我们能看到输出了两个 js 文件
总结:配置了几个入口,至少输出几个 js 文件
2. 提取重复代码
如果多入口文件中都引用了同一份代码,我们不希望这份代码被打包到两个文件中,导致代码重复,体积更大。
我们需要提取多入口的重复代码,只打包生成一个 js 文件,其他文件引用它就好
- 修改文件
- app.js
import { sum } from "./math";
console.log("hello app");
console.log(sum(1, 2, 3, 4));- main.js
import { sum } from "./math";
console.log("hello main");
console.log(sum(1, 2, 3, 4, 5));- math.js(公共文件)
export const sum = (...args) => {
return args.reduce((p, c) => p + c, 0);
};修改配置文件
jsmode: "production", 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, }, }, }, },运行指令
npx webpack
此时在dist目录下会多出一个 456.js 文件 为提取的公共模块,在其它使用该公共模块的js代码中会引用该模块
3.按需加载,动态导入
想要实现按需加载,动态导入模块,在需要使用时再加载模块
还需要额外配置:
- 修改文件
main.js
jsdocument.getElementById("btn").onclick = function () { // import 动态导入,会将动态导入的文件代码分割(拆分成单独模块)在需要使用的时候自动加载 import('./count') // 返回值为promise .then(({default:sum})=>{ console.log("模块加载成功",sum(2,1)); }) .catch((err)=>{ console.log("模块加载失败",err); }) };count.js
jsexport default function count(x,y){ return x-y; }index.html
html<button id="btn">按钮</button>
运行指令
npx webpack
dist 目录下多出一个 655.js 文件为动态导入的模块,在浏览器的网络中查看,当点击了按钮时才会加载该模块
我们可以发现,一旦通过 import 动态导入语法导入模块,模块就被代码分割,同时也能按需加载了。
4.单入口
开发时我们可能是单页面应用(SPA),只有一个入口(单入口)。那么我们需要这样配置:
optimization: {
// 代码分割配置
splitChunks: {
chunks: "all", // 对所有模块都进行分割
}
},可以将node_modules 代码提取打包成一个单独的文件,使用import 动态导入会将动态导入的模块单独打包为一个js文件
5.给动态导入文件取名称
- 修改文件
main.js
jsdocument.getElementById("btn").onclick = function(){ // 动态加载模块 eslint不识别动态导入,需要在.eslintrc文件中进行配置 // /* webpackChunkName:"math" */ webpack魔法命名 为动态导入的模块命名 import(/* webpackChunkName:"math" */'./js/math').then(({add})=>{ console.log(add(1,2)); }).catch((err)=>{ console.log("错误"); }) }webpack.prod.js
jsoutput: { // 所有文件的输出路径 // __dirname 当前文件的文件夹路径 path: path.resolve(__dirname, "../dist"), // 绝对路径 // 入口文件的输出名称 filename: "static/js/main.js", // 给打包输出的其它文件 chunkFilename:"static/js/[name].js", // 自动清空上次打包资源 原理:在打包前将path整个目录进行清空 clean: true, },
- 统一命名配置
webpack.prod.js
jsmodule.exports = { entry: "./src/main.js", output: { path: path.resolve(__dirname, "../dist"), // 生产模式需要输出 filename: "static/js/[name].js", // 入口文件打包输出资源命名方式 chunkFilename: "static/js/[name].chunk.js", // 动态导入输出资源命名方式 assetModuleFilename: "static/media/[name].[hash][ext]", // 图片、字体等资源命名方式(注意用hash) clean: true, }, ..... // 提取css成单独文件 new MiniCssExtractPlugin({ // 定义输出文件名和目录 filename: "static/css/[name].css", chunkFilename: "static/css/[name].chunk.css", }),
2.5.2 Preload / Prefetch
为什么
我们前面已经做了代码分割,同时会使用 import 动态导入语法来进行代码按需加载(我们也叫懒加载,比如路由懒加载就是这样实现的)
但是加载速度还不够好,比如:是用户点击按钮时才加载这个资源的,如果资源体积很大,那么用户会感觉到明显卡顿效果
我们想在浏览器空闲时间,加载后续需要使用的资源。我们就需要用上 Preload 或 Prefetch 技术
是什么
Preload:告诉浏览器立即加载资源。Prefetch:告诉浏览器在空闲时才开始加载资源。
它们共同点:
- 都只会加载资源,并不执行。
- 都有缓存。
它们区别:
Preload加载优先级高,Prefetch加载优先级低。Preload只能加载当前页面需要使用的资源,Prefetch可以加载当前页面资源,也可以加载下一个页面需要使用的资源。
总结:
- 当前页面优先级高的资源用
Preload加载。 - 下一个页面需要使用的资源用
Prefetch加载。
它们的问题:兼容性较差。
- 我们可以去 Can I Use 网站查询 API 的兼容性问题。
Preload相对于Prefetch兼容性好一点。
怎么用
下载包
npm i @vue/preload-webpack-plugin -D配置 webpack.prod.js
jsconst PreloadWebpackPlugin = require('@vue/preload-webpack-plugin'); ..... new PreloadWebpackPlugin({ rel: 'preload', as: 'script', // rel:"prefetch" }),
2.5.3 Network Cache
为什么
将来开发时我们对静态资源会使用缓存来优化,这样浏览器第二次请求资源就能读取缓存了,速度很快。
但是这样的话就会有一个问题, 因为前后输出的文件名是一样的,都叫 main.js,一旦将来发布新版本,因为文件名没有变化导致浏览器会直接读取缓存,不会加载新资源,项目也就没法更新了。
所以我们从文件名入手,确保更新前后文件名不一样,这样就可以做缓存了
是什么
它们都会生成一个唯一的 hash 值。
- fullhash(webpack4 是 hash)
每次修改任何一个文件,所有文件名的 hash 至都将改变。所以一旦修改了任何一个文件,整个项目的文件缓存都将失效。
- chunkhash
根据不同的入口文件(Entry)进行依赖文件解析、构建对应的 chunk,生成对应的哈希值。我们 js 和 css 是同一个引入,会共享一个 hash 值。
- contenthash
根据文件内容生成 hash 值,只有文件内容变化了,hash 值才会变化。所有文件 hash 值是独享且不同的
怎么用
// [contenthash:8]使用contenthash,取8位长度
filename: "static/js/[name].[contenthash:8].js", // 入口文件打包输出资源命名方式
chunkFilename: "static/js/[name].[contenthash:8].chunk.js", // 动态导入输出资源命名方式
new MiniCssExtractPlugin({
// 定义输出文件名和目录
filename: "static/css/[name].[contenthash:8].css",
chunkFilename: "static/css/[name].[contenthash:8].chunk.css",
})- 问题:
当我们修改 math.js 文件再重新打包的时候,因为 contenthash 原因,math.js 文件 hash 值发生了变化(这是正常的)。
但是 main.js 文件的 hash 值也发生了变化,这会导致 main.js 的缓存失效。明明我们只修改 math.js, 为什么 main.js 也会变身变化呢?
- 原因:
- 更新前:math.xxx.js, main.js 引用的 math.xxx.js
- 更新后:math.yyy.js, main.js 引用的 math.yyy.js, 文件名发生了变化,间接导致 main.js 也发生了变化
- 解决:
将 hash 值单独保管在一个 runtime 文件中。
我们最终输出三个文件:main、math、runtime。当 math 文件发送变化,变化的是 math 和 runtime 文件,main 不变。
runtime 文件只保存文件的 hash 值和它们与文件关系,整个文件体积就比较小,所以变化重新请求的代价也小。
optimization: {
.....
runtimeChunk: {
name: (entrypoint) => `runtime-${entrypoint.name}.js`,
},
},2.5.4 Core-js
为什么
过去我们使用 babel 对 js 代码进行了兼容性处理,其中使用@babel/preset-env 智能预设来处理兼容性问题。
它能将 ES6 的一些语法进行编译转换,比如箭头函数、点点点运算符等。但是如果是 async 函数、promise 对象、数组的一些方法(includes)等,它没办法处理。
所以此时我们 js 代码仍然存在兼容性问题,一旦遇到低版本浏览器会直接报错。所以我们想要将 js 兼容性问题彻底解决
是什么
core-js 是专门用来做 ES6 以及以上 API 的 polyfill。
polyfill翻译过来叫做垫片/补丁。就是用社区上提供的一段代码,让我们在不兼容某些新特性的浏览器上,使用该新特性。
怎么用
- 使用core-js
下载包
npm i core-js手动全部引入
main.js
jsimport "core-js";这样引入会将所有兼容性代码全部引入,体积太大了。我们只想引入 promise 的
polyfill。手动按需引入
main.js
jsimport "core-js/es/promise";只引入打包 promise 的
polyfill,打包体积更小。但是将来如果还想使用其他语法,我需要手动引入库很麻烦自动按需引入
babel.config.js
jsmodule.exports = { // 智能预设:能够编译ES6语法 presets: [ [ "@babel/preset-env", // 按需加载core-js的polyfill { useBuiltIns: "usage", corejs: 3, }, ], ], };
2.5.5 PWA
为什么
开发 Web App 项目,项目一旦处于网络离线情况,就没法访问了。
我们希望给项目提供离线体验
是什么
渐进式网络应用程序(progressive web application - PWA):是一种可以提供类似于 native app(原生应用程序) 体验的 Web App 的技术。
其中最重要的是,在 离线(offline) 时应用程序能够继续运行功能。
内部通过 Service Workers 技术实现的。
怎么用
- 下载包
npm i workbox-webpack-plugin -D- 修改配置文件 webpack.config.js
// 引入workbox-webpack-plugin插件: PWA
const WorkboxPlugin = require("workbox-webpack-plugin");
...
// new调用
plugins: [
new WorkboxPlugin.GenerateSW({
// 这些选项帮助快速启用 ServiceWorkers
// 不允许遗留任何“旧的” ServiceWorkers
clientsClaim: true,
skipWaiting: true,
}),
],修改 main.js
在main.js中添加以下代码
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
navigator.serviceWorker
.register("/service-worker.js")
.then((registration) => {
console.log("SW registered: ", registration);
})
.catch((registrationError) => {
console.log("SW registration failed: ", registrationError);
});
});
}- 运行指令
npm run build此时如果直接通过 VSCode 访问打包后页面,在浏览器控制台会发现 SW registration failed。
因为我们打开的访问路径是:http://127.0.0.1:5500/dist/index.html。此时页面会去请求 service-worker.js 文件,请求路径是:http://127.0.0.1:5500/service-worker.js,这样找不到会 404。
实际 service-worker.js 文件路径是:http://127.0.0.1:5500/dist/service-worker.js。
- 决路径问题
下载包
npm i serve -gserve 也是用来启动开发服务器来部署代码查看效果的。
运行指令
serve dist此时通过 serve 启动的服务器我们 service-worker 就能注册成功了。
浏览器处于离线状态也能访问页面
2.6 总结
我们从 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能让代码离线也能访问,从而提升用户体验。