Webpack一文搞懂:从入门到入土

84 阅读31分钟

长文警告!!! 虽说目前有了很多 Vite 即将取而代之的声音,但作为称霸打包界一时的Webpack大哥,觉得还是有必要学习学习,毕竟其生态尤为庞大,主要面试还会问~
本文正在参加「金石计划 . 瓜分6万现金大奖」

1、前言

1.1 为什么需要打包工具?

开发时,我们会使用框架(React、Vue),ES6 模块化语法,Less/Sass 等 css 预处理器等语法进行开发。 这样的代码要想在浏览器运行必须经过编译成浏览器能识别的 js、css 等语法,才能运行。
所以我们需要打包工具帮我们做完这些事。 除此之外,打包工具还能压缩代码、做兼容性处理、提升代码性能等

1.2 有哪些打包工具?

Grunt、Gulp、Parcel、Webpack、Rollup、Vite...
目前市面上最流量的是 Webpack ,所以我们主要以 Webpack 来介绍使用打包工具

2、基本使用

2.1 功能介绍

Webpack 是一个静态资源打包工具。
它会以一个或多个文件作为打包的入口,将我们整个项目所有文件编译组合成一个或多个文件输出出去。
输出的文件就是编译好的文件,就可以在浏览器段运行了。
我们将 Webpack 输出的文件叫做bundle
Webpack 本身功能是有限的:

  • 开发模式:仅能编译 JS 中的ES Module语法
  • 生产模式:能编译 JS 中的ES Module语法,还能压缩 JS 代码

2.2 基础使用案例

2.2.1 资源目录

webpack_code # 项目根目录(所有指令必须在这个目录运行)  
    └── public # 静态资源目录(图片、html文件等)
    └── src # 项目源码目录
          ├── js # js文件目录
          │   ├── count.js
          │   └── sum.js
          └── main.js # 项目主文件

2.2.2 创建文件

  • count.js
export default function count(x, y) {
  return x - y;
}
  • sum.js
export default function sum(...args) {
  return args.reduce((p, c) => p + c, 0);
}
  • main.js
import count from "./js/count";
import sum from "./js/sum";
console.log(count(2, 1));
console.log(sum(1, 2, 3, 4));

2.2.3 下载依赖

打开终端,来到项目根目录。运行以下指令:

  • 初始化 package.json

npm init -y

此时会生成一个基础的 package.json 文件,需要注意的是 package.json 中 name字段不能叫做 'webpack', 否则下一步会报错

  • 下载依赖
    • webpack是核心模块
    • webpack-cli则是命令行工具

npm i webpack webpack-cli -D

2.2.4 启用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,还会将其依赖也一起打包进来。
--mode=xxx:指定模式(环境)

2.2.5 观察输出文件

默认 Webpack 会将文件打包输出到 dist 目录下,我们查看 dist 目录下文件情况就好了

#小结

Webpack 本身功能比较少,只能处理js资源,一旦遇到css等其他资源就会报错。
所以我们学习 Webpack,就是主要学习如何处理其他资源。

3、基本配置

3.1 五大核心概念

  • entry(入口):指示 webpack 以哪个文件为入口起点开始打包,分析构建内部依赖图
  • output(输出):指示 webpack 打包后的资源 bundles 输出到哪里去,以及如何命名
  • loader(加载器):让 webpack 能够去处理那些非 JS 的文件,比如样式文件、图片文件(webpack 自身只理解JS)
  • plugins(插件):可以用于执行范围更广的任务。插件的范围包括:从打包优化和压缩,一直到重新定义环境中的变量等
  • mode(模式):主要由两种模式: 开发模式development, 生产模式production

3.2 Webpack配置文件webpack.config.js

在项目根目录下新建文件:webpack.config.js

// Node.js的核心模块,专门用来处理文件路径,引入path
const path = require("path");

module.exports = {
  // 入口
  // 相对路径和绝对路径都行
  entry: "./src/main.js",
  // 输出
  output: {
    // path: 文件输出目录,必须是绝对路径
    // path.resolve()方法返回一个绝对路径
    // __dirname 当前文件的文件夹绝对路径
    path: path.resolve(__dirname, "dist"),
    // filename: 输出文件名
    filename: "main.js",
  },
  // 加载器
  module: {//是一个对象
    rules: [],
  },
  // 插件
  plugins: [],//是一个数组
  // 模式
  mode: "development", // 开发模式
};

配置好配置文件后打包指令(此时功能和之前一样,也不能处理样式资源---)

npx webpack

4、开发模式介绍

开发模式顾名思义就是我们开发代码时使用的模式。
这个模式下我们主要做两件事:

  • 编译代码,使浏览器能识别运行
    开发时我们有样式资源、字体图标、图片资源、html 资源等,webpack 默认都不能处理这些资源,所以我们要加载配置来编译这些资源
  • 代码质量检查,树立代码规范 提前检查代码的一些隐患,让代码运行时能更加健壮。 提前检查代码规范和格式,统一团队编码风格,让代码更优雅美观。

5、常见配置

5.1 样式资源

5.1.1 介绍

本章节我们学习使用 Webpack 如何处理 Css、Less、Sass、Scss、Styl 样式资源。
Webpack 本身是不能识别样式资源的,所以我们需要借助 Loader 来帮助 Webpack 解析样式资源
我们找 Loader 都应该去官方文档中找到对应的 Loader,然后使用。
官方文档找不到的话,可以从社区 Github 中搜索查询
中文文档:www.webpackjs.com/loaders/

5.1.2 处理css资源

  • 下载包

npm i css-loader style-loader -D

注意:需要下载两个 loader

  • 功能介绍
    • css-loader:负责将 Css 文件编译成 Webpack 能识别的模块
    • style-loader:会动态创建一个 Style 标签,里面放置 Webpack 中 Css 模块内容
      此时样式就会以 Style 标签的形式在页面上生效
  • 配置(在前文webpack.config.js的module{rule[ ]中})
module: {
    rules: [
      {
        test: /\.css$/,// 用来匹配 .css 结尾的文件
//注意:这里用use而不是loader是因为use里可以写多个loader
        use: ["style-loader", "css-loader"],// use 数组里面 Loader 执行顺序是从右到左
      },
    ],
},
  • 添加 css 资源

src/css/index.css

.box1 {
  width: 100px;
  height: 100px;
  background-color: pink;
}

src/main.js

import count from "./js/count";
import sum from "./js/sum";
// 引入 Css 资源,Webpack才会对其打包
import "./css/index.css";
console.log(count(2, 1));
console.log(sum(1, 2, 3, 4));

public/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>webpack5</title>
  </head>
  <body>
    <h1>Hello Webpack5</h1>
    <!-- 准备一个使用样式的 DOM 容器 -->
    <div class="box1"></div>
    <!-- 引入打包后的js文件,才能看到效果 -->
    <script src="../dist/main.js"></script>
  </body>
</html>
  • 运行指令

npx webpack

打开 index.html 页面查看效果

5.1.3 处理 Less 资源

  • 下载包

npm i less-loader -D

  • 功能介绍
    • less-loader:负责将 Less 文件编译成 Css 文件
  • 配置(在前文webpack.config.js的module{rule[ ]中})
module: {
    rules: [
      {
        test: /\.css$/,// 用来匹配 .css 结尾的文件
        use: ["style-loader", "css-loader"],// use 数组里面 Loader 执行顺序是从右到左
      },
      {
        test: /\.less$/,
        use: ["style-loader", "css-loader", "less-loader"],
      },
    ],
},
  • 添加 Less 资源

src/less/index.less

.box2 {
  width: 100px;
  height: 100px;
  background-color: deeppink;
}

src/main.js

import count from "./js/count";
import sum from "./js/sum";
// 引入 Css 资源,Webpack才会对其打包
import "./css/index.css";
import "./less/index.less";
console.log(count(2, 1));
console.log(sum(1, 2, 3, 4));

public/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>webpack5</title>
  </head>
  <body>
    <h1>Hello Webpack5</h1>
    <!-- 准备一个使用样式的 DOM 容器 -->
    <div class="box1"></div>
    <div class="box2"></div>
    <!-- 引入打包后的js文件,才能看到效果 -->
    <script src="../dist/main.js"></script>
  </body>
</html>
  • 运行指令

npx webpack

打开 index.html 页面查看效果

5.1.4 处理 Sass 资源和Scss资源

  • 下载包

npm i sass-loader sass -D

注意:需要下载两个

  • 功能介绍
    • sass-loader:负责将 Sass 文件编译成 css 文件
    • sass:sass-loader 依赖,sass 进行编译
  • 配置(在前文webpack.config.js的module{rule[ ]中})
module: {
    rules: [
      {
        test: /\.css$/,// 用来匹配 .css 结尾的文件
        use: ["style-loader", "css-loader"],// use 数组里面 Loader 执行顺序是从右到左
      },
      {
        test: /\.less$/,
        use: ["style-loader", "css-loader", "less-loader"],
      },
      {
        test: /\.s[ac]ss$/,
        use: ["style-loader", "css-loader", "sass-loader"],
      },
    ],
},

  • 添加 Sass 资源

src/sass/index.sass

/* 可以省略大括号和分号 */
.box3
  width: 100px
  height: 100px
  background-color: hotpink

src/sass/index.scss

.box4 {
  width: 100px;
  height: 100px;
  background-color: lightpink;
}

src/main.js

import count from "./js/count";
import sum from "./js/sum";
// 引入 Css 资源,Webpack才会对其打包
import "./css/index.css";
import "./less/index.less";
import "./sass/index.sass";
import "./sass/index.scss";
console.log(count(2, 1));
console.log(sum(1, 2, 3, 4));

public/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>webpack5</title>
  </head>
  <body>
    <h1>Hello Webpack5</h1>
    <!-- 准备一个使用样式的 DOM 容器 -->
    <div class="box1"></div>
    <div class="box2"></div>
    <div class="box3"></div>
    <div class="box4"></div>
    <!-- 引入打包后的js文件,才能看到效果 -->
    <script src="../dist/main.js"></script>
  </body>
</html>
  • 运行指令

npx webpack

打开 index.html 页面查看效果

5.1.5 处理 stylus资源

  • 下载包

npm i stylus-loader -D

  • 功能介绍
    • stylus-loader:负责将 Styl 文件编译成 Css 文件
  • 配置(在前文webpack.config.js的module{rule[ ]中})
module: {
    rules: [
      {
        test: /\.css$/,// 用来匹配 .css 结尾的文件
        use: ["style-loader", "css-loader"],// use 数组里面 Loader 执行顺序是从右到左
      },
      {
        test: /\.less$/,
        use: ["style-loader", "css-loader", "less-loader"],
      },
      {
        test: /\.s[ac]ss$/,
        use: ["style-loader", "css-loader", "sass-loader"],
      },
      {
        test: /\.styl$/,
        use: ["style-loader", "css-loader", "stylus-loader"],
      },
    ],
  },
  • 添加 Styl 资源

src/styl/index.styl

/* 可以省略大括号、分号、冒号 */
.box 
  width 100px 
  height 100px 
  background-color pink

src/main.js

import count from "./js/count";
import sum from "./js/sum";
// 引入 Css 资源,Webpack才会对其打包
import "./css/index.css";
import "./less/index.less";
import "./sass/index.sass";
import "./sass/index.scss";
import "./styl/index.styl";
console.log(count(2, 1));
console.log(sum(1, 2, 3, 4));

public/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>webpack5</title>
  </head>
  <body>
    <h1>Hello Webpack5</h1>
    <!-- 准备一个使用样式的 DOM 容器 -->
    <div class="box1"></div>
    <div class="box2"></div>
    <div class="box3"></div>
    <div class="box4"></div>
    <div class="box5"></div>
    <!-- 引入打包后的js文件,才能看到效果 -->
    <script src="../dist/main.js"></script>
  </body>
</html>
  • 运行指令

npx webpack

打开 index.html 页面查看效果

5.2 处理图片资源

  • 配置

过去在 Webpack4 时,我们处理图片资源通过 file-loaderurl-loader 进行处理。现在 Webpack5 已经将两个 Loader 功能内置到 Webpack 里了,我们只需要简单配置即可处理图片资源.

module: {
    rules: [
      {//
        test: /\.(png|jpe?g|gif|webp)$/,
        type: "asset",
      },
    ],
},
  • 添加图片资源
    • src/images/1.jpeg
    • src/images/2.png
    • src/images/3.gif
  • 使用图片资源

src/less/index.less

.box2 {
  width: 100px;
  height: 100px;
  background-image: url("../images/1.jpeg");
  background-size: cover;
}

src/sass/index.sass

.box3
  width: 100px
  height: 100px
  background-image: url("../images/2.png")
  background-size: cover

src/styl/index.styl

.box5
  width 100px
  height 100px
  background-image url("../images/3.gif")
  background-size cover
  • 运行指令

npx webpack

  • 输出资源情况

此时如果查看 dist 目录的话,会发现多了三张图片资源。因为 Webpack 会将所有打包好的资源输出到 dist 目录下。
为什么样式资源没有呢?
因为经过 style-loader 的处理,样式资源打包到 main.js 里面去了,所以没有额外输出出来.

  • 对图片资源进行优化

将小于某个大小的图片转化成 data URI 形式(Base64 格式)

module: {
    rules: [
      {//
        test: /\.(png|jpe?g|gif|webp)$/,
        type: "asset",
        parser: {
          dataUrlCondition: {
            maxSize: 10 * 1024 // 小于10kb的图片会被base64处理
          }
        }
      },
    ],
},

优点:减少请求数量
缺点:体积变得更大
此时输出的图片文件就只有两张,有一张图片以 data URI 形式内置到 js 中了 (注意:需要将上次打包生成的文件清空,再重新打包才有效果)

5.3 两个优化

5.3.1 修改输出资源的名称和路径

我们想让打包后的文件不都堆积在dist目录下,而是有其相对应的文件夹管理,就需要进行一些配置:这里是对打包后的main.js和图片进行一些管理

module.exports = {
    output: {
        path: path.resolve(__dirname, "dist"),
        filename: "static/js/main.js", // 将 js 文件输出到 static/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]",
            },
          },
        ],
      },
};

运行指令:

npx webpack

  • 此时输出文件目录:(注意:需要将上次打包生成的文件清空,再重新打包才有效果)
├── dist
    └── static
         ├── imgs
         │    └── 7003350e.png
         └── js
              └── main.js

5.3.2 自动清空上次打包资源

我们每次新一次打包时,都需要将上一次dist文件夹删除,此时在output配置下写 clean:true 命令即可在每次打包时帮我们自动覆盖更新

  • 原理:在打包前,将path整个目录内容清空,再进行打包
module.exports = {
    output: {
        path: path.resolve(__dirname, "dist"),
        filename: "static/js/main.js",
        clean: true, // 自动将上次打包目录资源清空
    },
};

5.4 处理字体图标资源

5.4.1 使用字体图标文件

5.4.2 添加字体图标资源

  • 解压下载的压缩包,demo是使用演示,我们这里复制 iconfont.css 文件到src/css/iconfont.css
    • 注意字体文件路径需要修改
webpack_code # 项目根目录(所有指令必须在这个目录运行)
    └── dist
    └── node_modules
    └── public # 静态资源目录(图片、html文件等)
    └── src # 项目源码目录
        └── js # js文件目录
        ├── css
            └── index.css
            └── iconfont.css
        ├── fonts
            └── iconfont.ttf
            └── iconfont.woff
            └── iconfont.woff2
        └── main.js # 项目主文件
  • src/main.js
import "./css/iconfont.css";
  • public/index.html
<!-- 使用字体图标 -->
<body>
    <i class="iconfont icon-arrow-down"></i>
    <i class="iconfont icon-ashbin"></i>
    <i class="iconfont icon-browse"></i>
</body>

5.4.3 配置

module.exports = {
   module: {
      rules: [
          {
            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 形式

5.4.4 处理其他资源

我们在开发中可能还会遇到MP3、MP4等音视频资源,只需要在处理字体图标资源基础上增加其他文件类型,在test配置的正则表达式中统一标注就好

test: /\.(ttf|woff2?|map4|map3|avi)$/

5.5 处理js资源

有人可能会问,js 资源 Webpack 不能已经处理了吗,为什么我们还要处理呢?
原因是 Webpack 对 js 处理是有限的,只能编译 js 中 ES 模块化语法,不能编译其他语法,导致 js 不能在 IE 等浏览器运行,所以我们希望做一些兼容性处理。
其次开发中,团队对代码格式是有严格要求的,我们不能由肉眼去检测代码格式,需要使用专业的工具来检测。

  • 针对 js 兼容性处理,我们使用 Babel 来完成
  • 针对代码格式,我们使用 Eslint 来完成

我们先完成 Eslint,检测代码格式无误后,在由 Babel 做代码兼容性处理

5.5.1 Eslint(一个plugin)

可组装的 JavaScript 和 JSX 检查工具(用来检测 js 和 jsx 语法的工具,可以配置各项功能)
我们使用 Eslint,关键是写 Eslint 配置文件,里面写上各种 rules 规则, 将来运行 Eslint 时就会以写的规则对代码进行检查

a. 配置文件(配置文件由很多种写法):

  • .eslintrc.*:新建文件,位于项目根目录(与src、public同级)
    • .eslintrc
    • .eslintrc.js
    • .eslintrc.json
    • 区别在于配置格式不一样,由于.eslintrc.js可以用js语法编写,我们一般使用这个
  • package.jsoneslintConfig:不需要创建文件,在原有文件基础上写

ESLint 会查找和自动读取它们,所以以上配置文件只需要存在 一个 即可

b. 具体配置:我们以 .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 具体规则(每条规则采用key:value形式,value可以是数组,但是第一个值是该规则是否开启)
    • "off" 或 0 - 关闭规则
    • "warn" 或 1 - 开启规则,使用警告级别的错误:warn (不会导致程序退出)
    • "error" 或 2 - 开启规则,使用错误级别的错误:error (当被触发的时候,程序会退出)

更多rule规则:eslint.bootcss.com/docs/rules/

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 除了少数情况下不会有警告
  ],
}
// 例如在React项目中,我们可以这样写配置,当然除了官方规则,vue和react规则都需要去网致下载
module.exports = {
  extends: ["react-app"],
  rules: {
    // 我们的规则会覆盖掉react-app的规则
    // 所以想要修改规则直接改就是了
    eqeqeq: ["warn", "smart"],
  },
};

c. 在Webpack中使用

  • 下载包

npm i eslint-webpack-plugin eslint -D

  • 定义 Eslint 配置文件.eslintrc.js(根目录下)
module.exports = {
  // 继承 Eslint 规则
  extends: ["eslint:recommended"],//继承官方规则
  env: {
    node: true, // 启用node中全局变量
    browser: true, // 启用浏览器中全局变量
  },
  parserOptions: {
    ecmaVersion: 6,//采用ES6语法版本检查代码
    sourceType: "module",//ES 模块化
  },
  rules: {
    "no-var": 2, // 不能使用 var 定义变量
  },
};
  • webpack.config.js中配置
//plugins和loader不同,因为是插件所以需要先引入才能使用
const ESLintWebpackPlugin = require("eslint-webpack-plugin");
module.exports = {
   plugins: [
      new ESLintWebpackPlugin({
          // 检测哪些文件
          context: path.resolve(__dirname, "src"),
      }),
   ],
};

d. VSCode Eslint 插件

打开 VSCode,下载 Eslint 插件,即可不用打包编译就能看到错误,可以提前解决

截图18.png

但是此时就会对项目所有文件默认进行 Eslint 检查了,我们 dist 目录下的打包后文件就会报错(打包时把ES6语法都转为ES5了,按Eslint的rule检查规则有些语法就不可用)。但是我们只需要检查 src 下面的文件,不需要检查 dist 下面的文件。

所以可以使用 Eslint 忽略文件解决。在项目根目录新建下面文件:

  • .eslintignore
//忽略dist目录下所有文件
dist

5.5.2 Babel(一个loader)

JavaScript 编译器:主要用于将 ES6 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中,关键是写预设

a. 配置文件(配置文件由很多种写法):

  • babel.config.*:新建文件,位于项目根目录
    • babel.config.js
    • babel.config.json
  • .babelrc.*:新建文件,位于项目根目录
    • .babelrc
    • .babelrc.js
    • .babelrc.json
  • package.json 中 babel:不需要创建文件,在原有文件基础上写

Babel 会查找和自动读取它们,所以以上配置文件只需要存在一个即可

b. 具体配置(以 babel.config.js 配置文件为例)

module.exports = {
  // 预设
  presets: [],
};
  • presets 预设简单理解:就是一组 Babel 插件, 扩展 Babel 功能
    • @babel/preset-env: 一个智能预设,允许您使用最新的 JavaScript
    • @babel/preset-react:一个用来编译 React jsx 语法的预设
    • @babel/preset-typescript:一个用来编译 TypeScript 语法的预设

c. 在 Webpack 中使用

  • 下载包

npm i babel-loader @babel/core @babel/preset-env -D

  • 定义 Babel 配置文件babel.config.js
module.exports = {
  presets: ["@babel/preset-env"],//将最新语法转换为向后兼容
};
  • webpack.config.js中配置
module.exports = {
   module: {
      rules: [
        {
          test: /\.js$/,
          exclude: /node_modules/, // 排除node_modules代码不编译
          loader: "babel-loader",
        },
      ],
   },
};

此时运行命令(打开打包后的 dist/static/js/main.js 文件查看,会发现箭头函数等 ES6 语法已经转换了)

npx webpack

5.6 处理Html资源

有时候手动在html文件中引入我们打包好的js文件也并不是最佳选择,所有我们借用一个plugins来帮我们自动引入

  • 下载包

npm i html-webpack-plugin -D

  • 配置webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      // 以 public/index.html 为模板创建文件
      // 新的html文件有两个特点:1. 内容和源文件一致 2. 自动引入打包生成的js等资源
      template: path.resolve(__dirname, "public/index.html"),
    }),
  ],
  mode: "development",
};

6、开启服务器&自动化

每次写完代码都需要手动输入指令才能编译代码,太麻烦了,我们希望一切自动化

  • 下载包

npm i webpack-dev-server -D

  • 配置webpack.config.js
module.exports = {
  // 开发服务器
  devServer: {
    host: "localhost", // 启动服务器域名
    port: "3000", // 启动服务器端口号
    open: true, // 是否自动打开浏览器
  },
  mode: "development",
};
  • 运行指令

npx webpack serve

注意运行指令发生了变化
并且当你使用开发服务器时,所有代码都会在内存中编译打包,并不会输出到 dist 目录下。
开发时我们只关心代码能运行,有效果即可,至于代码被编译成什么样子,我们并不需要知道。

7、生产模式介绍

生产模式是开发完成代码后,我们需要得到代码将来部署上线。

这个模式下我们主要对代码进行优化,让其运行性能更好。

优化主要从两个角度出发:

  1. 优化代码运行性能
  2. 优化代码打包速度

7.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 (包的依赖管理配置文件)

7.2 修改 webpack.dev.js

因为文件目录变了,所以所有绝对路径需要回退一层目录才能找到对应的文件 (相对路径不需要调整因为命令行工具执行时还是会根据根目录下进行执行,只是我们绝对路径的__dirname限制了该行代码当前目录为config,需要../)

const path = require("path");
const ESLintWebpackPlugin = require("eslint-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  entry: "./src/main.js",
  output: {
    path: undefined, // 开发模式没有输出,不需要指定输出目录
    filename: "static/js/main.js", // 将 js 文件输出到 static/js 目录中
    // clean: true, // 开发模式没有输出,不需要清空输出结果
  },
  module: {
    rules: [
      {
        // 用来匹配 .css 结尾的文件
        test: /\.css$/,
        // use 数组里面 Loader 执行顺序是从右到左
        use: ["style-loader", "css-loader"],
      },
      {
        test: /\.less$/,
        use: ["style-loader", "css-loader", "less-loader"],
      },
      {
        test: /\.s[ac]ss$/,
        use: ["style-loader", "css-loader", "sass-loader"],
      },
      {
        test: /\.styl$/,
        use: ["style-loader", "css-loader", "stylus-loader"],
      },
      {
        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]",
        },
      },
      {
        test: /\.(ttf|woff2?)$/,
        type: "asset/resource",
        generator: {
          filename: "static/media/[hash:8][ext][query]",
        },
      },
      {
        test: /\.js$/,
        exclude: /node_modules/, // 排除node_modules代码不编译
        loader: "babel-loader",
      },
    ],
  },
  plugins: [
    new ESLintWebpackPlugin({
      // 指定检查文件的根目录
      context: path.resolve(__dirname, "../src"),
    }),
    new HtmlWebpackPlugin({
      // 以 public/index.html 为模板创建文件
      // 新的html文件有两个特点:1. 内容和源文件一致 2. 自动引入打包生成的js等资源
      template: path.resolve(__dirname, "../public/index.html"),
    }),
  ],
  // 其他省略
  devServer: {
    host: "localhost", // 启动服务器域名
    port: "3000", // 启动服务器端口号
    open: true, // 是否自动打开浏览器
  },
  mode: "development",
};
  • 运行开发模式的指令:

npx webpack serve --config ./config/webpack.dev.js

7.3 修改 webpack.prod.js

const path = require("path");
const ESLintWebpackPlugin = require("eslint-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  entry: "./src/main.js",
  output: {
    path: path.resolve(__dirname, "../dist"), // 生产模式需要输出
    filename: "static/js/main.js", // 将 js 文件输出到 static/js 目录中
    clean: true,
  },
  module: {
    rules: [
      {
        // 用来匹配 .css 结尾的文件
        test: /\.css$/,
        // use 数组里面 Loader 执行顺序是从右到左
        use: ["style-loader", "css-loader"],
      },
      {
        test: /\.less$/,
        use: ["style-loader", "css-loader", "less-loader"],
      },
      {
        test: /\.s[ac]ss$/,
        use: ["style-loader", "css-loader", "sass-loader"],
      },
      {
        test: /\.styl$/,
        use: ["style-loader", "css-loader", "stylus-loader"],
      },
      {
        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]",
        },
      },
      {
        test: /\.(ttf|woff2?)$/,
        type: "asset/resource",
        generator: {
          filename: "static/media/[hash:8][ext][query]",
        },
      },
      {
        test: /\.js$/,
        exclude: /node_modules/, // 排除node_modules代码不编译
        loader: "babel-loader",
      },
    ],
  },
  plugins: [
    new ESLintWebpackPlugin({
      // 指定检查文件的根目录
      context: path.resolve(__dirname, "../src"),
    }),
    new HtmlWebpackPlugin({
      // 以 public/index.html 为模板创建文件
      // 新的html文件有两个特点:1. 内容和源文件一致 2. 自动引入打包生成的js等资源
      template: path.resolve(__dirname, "../public/index.html"),
    }),
  ],
  // devServer: {
  //   host: "localhost", // 启动服务器域名
  //   port: "3000", // 启动服务器端口号
  //   open: true, // 是否自动打开浏览器
  // },
  mode: "production",
};
  • 运行生产模式的指令:

npx webpack --config ./config/webpack.prod.js

7.4 配置运行指令

为了方便运行不同模式的指令,我们将指令定义在 package.json 中 scripts 里面

// package.json
{
  // 其他省略
  "scripts": {
    "start": "npm run dev",
    "dev": "npx webpack serve --config ./config/webpack.dev.js",
    "build": "npx webpack --config ./config/webpack.prod.js"
  }
}

以后启动指令:

  • 开发模式:npm startnpm run dev
  • 生产模式:npm run build

8、额外处理

8.1 Css处理

8.1.1 提取css成单独文件

Css 文件目前被打包到 js 文件中,当 js 文件加载时,会创建一个 style 标签来生成样式,这样对于网站来说,会出现闪屏现象,用户体验不好,我们应该是单独的 Css 文件,通过 link 标签加载性能才好

  • 下载包

npm i mini-css-extract-plugin -D

  • 配置 webpack.prod.js

就是把之前用到style-loader的地方全换成MiniCssExtractPlugin.loader,再在plugins里实例化一下

const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
  module: {
    rules: [
      {
        // 用来匹配 .css 结尾的文件
        test: /\.css$/,
        // use 数组里面 Loader 执行顺序是从右到左
        //use: ["style-loader", "css-loader"]
        use: [MiniCssExtractPlugin.loader, "css-loader"],
      },
    ],
  },
  plugins: [
    // 提取css成单独文件
    new MiniCssExtractPlugin({
      // 定义输出文件名和目录
      filename: "static/css/main.css",
    }),
  ],
  mode: "production",
};;
  • 运行指令

npm run build

8.1.2 css兼容性处理

  • 下载包

npm i postcss-loader postcss postcss-preset-env -D

  • 配置webpack.prod.js
    • 注意:以下代码postcss-loader要写在css-loader下后面,其它样式loader前面。拿less举例
module: {
  rules: [
     {
       test: /\.less$/,
       use: [
           MiniCssExtractPlugin.loader,
           "css-loader",
           {//postcss-loader配置项
               loader: "postcss-loader",
               options: {
                  postcssOptions: {
                    plugins: [
                      "postcss-preset-env", // 能解决大多数样式兼容性问题
                    ],
                  },
                },
            },//postcss-loader配置项
            "less-loader",
      ],
}, 
  • 控制兼容性

我们可以在 package.json 文件中添加 browserslist 来控制样式的兼容性做到什么程度。

{
  // 其他省略
  "browserslist": ["ie >= 8"]
}

想要知道更多的 browserslist 配置,查看browserslist 文档
以上为了测试兼容性所以设置兼容浏览器 ie8 以上。 实际开发中我们一般不考虑旧版本浏览器了,所以我们可以这样设置:

{
  // 其他省略
  "browserslist": ["last 2 version", "> 1%", "not dead"]
}
  • 合并配置

我们将相同复用的loader封装成一个loader函数,并接受每个rule里use到的余下loader作为参数,封装函数里return的数组调用filter方法根据是否有参数判断是否返回一个新数组

const path = require("path");
const ESLintWebpackPlugin = require("eslint-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

// 获取处理样式的Loaders
const getStyleLoaders = (preProcessor) => {
  return [
    MiniCssExtractPlugin.loader,
    "css-loader",
    {
      loader: "postcss-loader",
      options: {
        postcssOptions: {
          plugins: [
            "postcss-preset-env", // 能解决大多数样式兼容性问题
          ],
        },
      },
    },
    preProcessor,
  ].filter(Boolean);
};

module.exports = {
  entry: "./src/main.js",
  output: {
    path: path.resolve(__dirname, "../dist"), // 生产模式需要输出
    filename: "static/js/main.js", // 将 js 文件输出到 static/js 目录中
    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: /\.styl$/,
        use: getStyleLoaders("stylus-loader"),
      },
      {
        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]",
        },
      },
      {
        test: /\.(ttf|woff2?)$/,
        type: "asset/resource",
        generator: {
          filename: "static/media/[hash:8][ext][query]",
        },
      },
      {
        test: /\.js$/,
        exclude: /node_modules/, // 排除node_modules代码不编译
        loader: "babel-loader",
      },
    ],
  },
  plugins: [
    new ESLintWebpackPlugin({
      // 指定检查文件的根目录
      context: path.resolve(__dirname, "../src"),
    }),
    new HtmlWebpackPlugin({
      // 以 public/index.html 为模板创建文件
      // 新的html文件有两个特点:1. 内容和源文件一致 2. 自动引入打包生成的js等资源
      template: path.resolve(__dirname, "../public/index.html"),
    }),
    // 提取css成单独文件
    new MiniCssExtractPlugin({
      // 定义输出文件名和目录
      filename: "static/css/main.css",
    }),
  ],
  // devServer: {
  //   host: "localhost", // 启动服务器域名
  //   port: "3000", // 启动服务器端口号
  //   open: true, // 是否自动打开浏览器
  // },
  mode: "production",
};

8.1.3 css压缩

  • 下载包

npm i css-minimizer-webpack-plugin -D

  • 配置webpack.prod.js
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
   plugins: [
       // css压缩
       new CssMinimizerPlugin(),
   ],
};

8.2 html和js压缩

默认生产模式已经开启了:html 压缩和 js 压缩 不需要额外进行配置

小结

  • 两种开发模式
    • 开发模式:代码能编译自动化运行
    • 生产模式:代码编译优化输出
  • Webpack 基本功能
    • 开发模式:可以编译 ES Module 语法
    • 生产模式:可以编译 ES Module 语法,压缩 js 代码
  • Webpack 配置文件
    • 5 个核心概念
      • entry
      • output
      • loader
      • plugins
      • mode
    • devServer配置开发服务器9.1 SourceMap技术(源码-包后代码映射关系,找到错码位置)
  • Webpack 脚本指令用法
    • webpack 直接打包输出
    • webpack serve 启动开发服务器,内存编译打包没有输出

往后为webpack优化方案

所谓Webpack高级配置其实就是进行 Webpack 优化,让我们代码在编译/运行时性能更好~

我们会从以下角度来进行优化:

  1. 提升开发体验
  2. 提升打包构建速度
  3. 减少代码体积
  4. 优化代码运行性能

9、提升开发体验

9.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/configuration/devtool/) * or disable the default devtool with "devtool: false". * If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/). */ /******/ (() => { // webpackBootstrap /******/ "use strict"; /******/ var __webpack_modules__ = ({ /***/ "./node_modules/css-loader/dist/cjs.js!./node_modules/less-loader/dist/cjs.js!./src/less/index.less": /*!**********************************************************************************************************!*\ !*** ./node_modules/css-loader/dist/cjs.js!./node_modules/less-loader/dist/cjs.js!./src/less/index.less ***! \**********************************************************************************************************/ /***/ ((module, __webpack_exports__, __webpack_require__) => { eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var _node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../node_modules/css-loader/dist/runtime/noSourceMaps.js */ \"./node_modules/css-loader/dist/runtime/noSourceMaps.js\");\n/* harmony import */ var _node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../node_modules/css-loader/dist/runtime/api.js */ \"./node_modules/css-loader/dist/runtime/api.js\");\n/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__);\n// Imports\n\n\nvar ___CSS_LOADER_EXPORT___ = _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default()((_node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default()));\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \".box2 {\\n width: 100px;\\n height: 100px;\\n background-color: deeppink;\\n}\\n\", \"\"]);\n// Exports\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (___CSS_LOADER_EXPORT___);\n\n\n//# sourceURL=webpack://webpack5/./src/less/index.less?./node_modules/css-loader/dist/cjs.js!./node_modules/less-loader/dist/cjs.js"); /***/ }), // 其他省略

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

  • 是什么

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

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

  • 怎么用

通过查看Webpack DevTool 文档可知,SourceMap 的值有很多种情况。但实际开发时我们只需要关注两种情况即可:

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

10、提升打包构建速度

10.1 HotModuleReplacement(HMR哪块代码重修改-局部编译打包)

  • 为什么

开发时我们修改了其中一个模块代码,Webpack 默认会将所有模块全部重新打包编译,速度很慢。所以我们需要做到修改某个模块代码,就只有这个模块代码需要重新打包编译,其他模块不变,这样打包速度就能很快

  • 是什么

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

  • 怎么用

基本配置(在开发模式webpack.dev.jsdevServer开发服务器下

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");
  module.hot.accept("./js/sum.js");
}

上面这样写会很麻烦,所以实际开发我们会使用其他 loader 来解决。 比如:vue-loader, react-hot-loader

10.2 OneOf(只挑一个loader)

  • 为什么

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

  • 是什么

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

  • 怎么用

拿开发模式webpack.dev.js举例,生产模式一样:只需要将rules下再那个oneOf包裹所有loader就可以了

module: {
    rules: [
      {
        oneOf: [
          {
            // 用来匹配 .css 结尾的文件
            test: /\.css$/,
            // use 数组里面 Loader 执行顺序是从右到左
            use: ["style-loader", "css-loader"],
          },
          //其余loader省略
          {
            test: /\.js$/,
            exclude: /node_modules/, // 排除node_modules代码不编译
            loader: "babel-loader",
          },
        ],
      },
    ],
  },

10.3 Include/Exclude(不打包三方库/插件)

  • 为什么

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

所以我们在对 js 文件处理时,要排除 node_modules 下面的文件(一般只针对js文件,因为css样式我们一般都自己写不会需要插件)

  • 是什么

    • include:包含,只处理 xxx 文件
    • exclude:排除,除了 xxx 文件以外其他文件都处理
  • 怎么用

我们只需要 对处理JS资源的babel编译和Eslint检查配置上indlude/exclude信息就可以

const ESLintWebpackPlugin = require("eslint-webpack-plugin");
module.exports = {
  module: {
    rules: [
      {
        oneOf: [
          {
            test: /\.js$/,
            // exclude: /node_modules/, // 排除node_modules代码不编译
            include: path.resolve(__dirname, "../src"), // 也可以用包含
            loader: "babel-loader",
          },
        ],
      },
    ],
  },
  plugins: [
    new ESLintWebpackPlugin({
      // 指定检查文件的根目录
      context: path.resolve(__dirname, "../src"),
      exclude: "node_modules", // 默认值
    }),
  ],
  // 开发服务器
  devServer: {
    host: "localhost", // 启动服务器域名
    port: "3000", // 启动服务器端口号
    open: true, // 是否自动打开浏览器
    hot: true, // 开启HMR功能
  },
  mode: "development",
  devtool: "cheap-module-source-map",
};

10.4 Cache(缓存之前Eslint检查、Babel编译的结果)

  • 为什么

每次打包时 js 文件都要经过 Eslint 检查 和 Babel 编译,速度比较慢。 我们可以缓存之前的 Eslint 检查 和 Babel 编译结果,这样第二次打包时速度就会更快了

  • 是什么

Eslint 检查Babel 编译 结果进行缓存

  • 怎么用

我们只需要配置 处理JS资源的babel编译(cacheDirectorycacheCompression)和Eslint检查(cacheLocation

{//webpack.dev.js的module.exports的module的rules下
    test: /\.js$/,
    // exclude: /node_modules/, // 排除node_modules代码不编译
    include: path.resolve(__dirname, "../src"), // 也可以用包含
    loader: "babel-loader",
    options: {
        cacheDirectory: true, // 开启babel编译缓存
        cacheCompression: false, // 缓存文件不要压缩
    },
//plugins下的ESLintWebpackPlugin配置信息
new ESLintWebpackPlugin({
      // 指定检查文件的根目录
      context: path.resolve(__dirname, "../src"),
      exclude: "node_modules", // 默认值
      cache: true, // 开启缓存
      // 缓存目录
      cacheLocation: path.resolve(__dirname,"../node_modules/.cache/.eslintcache"),
}),

10.5 Thread(开启多线程打包)

  • 为什么

当项目越来越庞大时,打包速度越来越慢,甚至于需要一个下午才能打包出来代码。这个速度是比较慢的。
我们想要继续提升打包速度,其实就是要提升 js 的打包速度,因为其他文件都比较少。
而对 js 文件处理主要就是 eslint检查 、babel编译、Terser压缩 三个工具,所以我们要提升它们的运行速度,我们可以开启多进程同时处理 js 文件,这样速度就比之前的单进程打包更快了

  • 是什么

多进程打包:开启电脑的多个进程同时干一件事,速度更快。
注意:请仅在特别耗时的操作中使用,因为每个进程启动就有大约为 600ms 左右开销

  • 怎么用 (我们启动进程的数量就是我们 CPU 的核数)

如何获取 CPU 的核数,因为每个电脑都不一样

// nodejs核心模块,直接使用
const os = require("os");
// cpu核数
const threads = os.cpus().length;

下载包

npm i thread-loader -D

使用 (在babel、eslint、terser分别开启多线程thead)

const os = require("os");
//terser是webpack自带,默认工作的,要处理它就得先把它调出来
const TerserPlugin = require("terser-webpack-plugin");
// cpu核数
const threads = os.cpus().length;
module.exports = {
  module: {
     rules: [
         {
           oneOf [
              //其余loader已省略,这里只呈现babel-loader
              {
                test: /\.js$/,
                // exclude: /node_modules/, // 排除node_modules代码不编译
                include: path.resolve(__dirname, "../src"), // 也可以用包含
                use: [
                  {
                    loader: "thread-loader", // 开启多进程
                    options: { workers: threads, // 开启进程数量=我们电脑核数},
                  },
                  {
                    loader: "babel-loader",
                    options: {
                      cacheDirectory: true, // 开启babel编译缓存
                    },},],},],},],
  },             
  plugins: [
    new ESLintWebpackPlugin({
      // 指定检查文件的根目录
      context: path.resolve(__dirname, "../src"),
      exclude: "node_modules", // 默认值
      cache: true, // 开启缓存
      // 缓存目录
      cacheLocation: path.resolve(
        __dirname,
        "../node_modules/.cache/.eslintcache"
      ),
      threads, // 开启多进程
    }),
  //Webpack5规定将压缩类插件都统一写到optimization配置项里,而不是plugins
  optimization: {
    minimize: true,
    minimizer: [
      // css压缩也可以写到optimization.minimizer里面,效果一样的
      new CssMinimizerPlugin(),
      // 当生产模式会默认开启TerserPlugin,但是我们需要进行其他配置,就要重新写了
      new TerserPlugin({
        parallel: threads // 开启多进程
      })
    ],
  },

11、减少代码体积

11.1 Tree Shaking(从库中只打包我们用到的文件/函数)

  • 为什么

开发时我们定义了一些工具函数库,或者引用第三方工具函数库或组件库。
如果没有特殊处理的话我们打包时会引入整个库,但是实际上可能我们可能只用上极小部分的功能。
这样将整个库都打包进来,体积就太大了

  • 是什么

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

  • 怎么用

Webpack 已经默认开启了这个功能,无需其他配置(yeah!)

11.2 Babel(减少babel编译时的打包体积)

  • 为什么

Babel 为编译的每个文件都插入了辅助代码,使代码体积过大!
Babel 对一些公共方法使用了非常小的辅助代码,比如 _extend。默认情况下会被添加到每一个需要它的文件中。
我们可以将这些辅助代码作为一个独立模块,来避免重复引入

  • 是什么

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

  • 怎么用

下载包

npm i @babel/plugin-transform-runtime -D

配置(只需要在babel-loaderoptions 配置项里引入:

loader: "babel-loader",
options: {
   cacheDirectory: true, // 开启babel编译缓存
   cacheCompression: false, // 缓存文件不要压缩
   plugins: ["@babel/plugin-transform-runtime"], // 减少代码体积

11.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

配置(以无损压缩为例):在optimization配置项的minimizer中配置信息

//引入插件
const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
//在optimization配置项的minimizer中复制以下代码即可// 压缩图片
      new ImageMinimizerPlugin({
        minimizer: {
          implementation: ImageMinimizerPlugin.imageminGenerate,
          options: {
            plugins: [
              ["gifsicle", { interlaced: true }],
              ["jpegtran", { progressive: true }],
              ["optipng", { optimizationLevel: 5 }],
              [
                "svgo",
                {
                  plugins: [
                    "preset-default",
                    "prefixIds",
                    {
                      name: "sortAttrs",
                      params: {
                        xmlnsOrder: "alphabetical",
                      },
                    },
                  ],
                },
              ],
            ],
          },
        },
      }),

如果打包时出现报错:

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 下面,👉下载地址

12、优化代码运行性能

12.1 Code Split(分割生成的js文件,按需加载,chunk)

  • 为什么

打包代码时会将所有 js 文件打包到一个文件中,体积太大了。我们如果只要渲染首页,就应该只加载首页的 js 文件,其他文件不应该加载。
所以我们需要 将打包生成的文件进行代码分割,生成多个 js 文件,渲染哪个页面就只加载某个 js 文件,这样加载的资源就少,速度就更快

  • 是什么

代码分割(Code Split)主要做了两件事:
:分割文件:将打包生成的文件进行分割,生成多个 js 文件。
:按需加载:需要哪个文件就加载哪个文件

  • 怎么用
    代码分割实现方式有不同的方式,为了更加方便体现它们之间的差异,我们会分别创建新的文件来演示

(1) 多入口方法(配置多个入口来控制输出多个js文件)

a. 基本配置
  • 文件目录
├── public
├── src
|   ├── app.js
|   └── main.js
├── package.json
└── webpack.config.js
  • 下载包

npm i webpack webpack-cli html-webpack-plugin -D

  • 新建文件
    • app.js:console.log("hello app");
    • main.js :console.log("hello main");
  • 配置webpack.config.js 深层次理解chunk
// webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  // 单入口
  // entry: './src/main.js',
  // 多入口
  entry: {
    main: "./src/main.js",
    app: "./src/app.js",
  },
  output: {
    path: path.resolve(__dirname, "./dist"),
    // [name]是webpack命名规则,使用chunk的name作为输出的文件名。
    // 什么是chunk?打包的资源就是chunk,输出出去叫bundle。
    // chunk的name是啥呢? 比如: entry中xxx: "./src/xxx.js", name就是xxx。注意是前面的xxx,和文件名无关。
    // 为什么需要这样命名呢?如果还是之前写法main.js,那么打包生成两个js文件都会叫做main.js会发生覆盖。(实际上会直接报错的)
    filename: "js/[name].js",
    clear: true,
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./public/index.html",
    }),
  ],
  mode: "production",
};
  • 运行指令:

npx webpack

此时在 dist 目录我们能看到输出了两个 js 文件。
总结:配置了几个入口,至少输出几个 js 文件。

b. 提取重复代码

如果多入口文件中都引用了同一份代码,我们不希望这份代码被打包到两个文件中,导致代码重复,体积更大。
我们需要 提取多入口的重复代码,只打包生成一个 js 文件,其他文件引用它 就好

  • 修改文件

app.js

import { sum } from "./math";

console.log("hello app");
console.log(sum(1, 2, 3, 4));

mian.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);
};
  • 修改配置文件 (在webpack.config.js的optimization配置项中添加一个splitChunks代码分割配置,前面的minizer是代码压缩配置)
optimization: {
    // 代码分割配置
    splitChunks: {
      chunks: "all", // 对所有模块都进行分割
      cacheGroups: {
        default: { 
          minSize: 0, // 我们定义的文件体积太小了,所以要改打包的最小文件体积
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
      // 以下是默认值
      // 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,
      //   },
      // },
    },
  • 运行指令:

npx webpack

此时我们会发现生成 3 个 js 文件,其中有一个就是提取的公共模块

c. 按需加载,动态导入

想要实现按需加载,动态导入模块。还需要额外配置:

  • 修改文件

main.js(这里import不能直接导入,得使用动态语法

console.log("hello main");
document.getElementById("btn").onclick = function () {
//点击按钮才使用math.js文件,import动态语法返回的是一个promise,可以使用then
  import("./math.js").then(({ sum }) => {
    alert(sum(1, 2, 3, 4, 5));
  });
};

app.js

console.log("hello app");

public/index.html

<button id="btn">计算</button>
  • 运行指令

npx webpack

我们可以发现,一旦通过 import 动态导入语法导入模块,模块就被代码分割,同时也能按需加载了

(2) 单入口处理

开发时我们可能是单页面应用(SPA),只有一个入口(单入口)。那么我们需要单入口+代码分割+动态导入方式来进行配置:(其实也就在optimization配置项中追加了splitChunks配置,且都使用默认值,因为只有一个入口文件

// webpack.prod.js
const os = require("os");
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 TerserPlugin = require("terser-webpack-plugin");
const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");

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

// 获取处理样式的Loaders
const getStyleLoaders = (preProcessor) => {
  return [
    MiniCssExtractPlugin.loader,
    "css-loader",
    {
      loader: "postcss-loader",
      options: {
        postcssOptions: {
          plugins: [
            "postcss-preset-env", // 能解决大多数样式兼容性问题
          ],
        },
      },
    },
    preProcessor,
  ].filter(Boolean);
};

module.exports = {
  entry: "./src/main.js",
  output: {
    path: path.resolve(__dirname, "../dist"), // 生产模式需要输出
    filename: "static/js/main.js", // 将 js 文件输出到 static/js 目录中
    clean: true,
  },
  module: {
    rules: [
      {
        oneOf: [
          {
            // 用来匹配 .css 结尾的文件
            test: /\.css$/,
            // use 数组里面 Loader 执行顺序是从右到左
            use: getStyleLoaders(),
          },
          {
            test: /\.less$/,
            use: getStyleLoaders("less-loader"),
          },
          {
            test: /\.s[ac]ss$/,
            use: getStyleLoaders("sass-loader"),
          },
          {
            test: /\.styl$/,
            use: getStyleLoaders("stylus-loader"),
          },
          {
            test: /\.(png|jpe?g|gif|svg)$/,
            type: "asset",
            parser: {
              dataUrlCondition: {
                maxSize: 10 * 1024, // 小于10kb的图片会被base64处理
              },
            },
            generator: {
              // 将图片文件输出到 static/imgs 目录中
              // 将图片文件命名 [hash:8][ext][query]
              // [hash:8]: hash值取8位
              // [ext]: 使用之前的文件扩展名
              // [query]: 添加之前的query参数
              filename: "static/imgs/[hash:8][ext][query]",
            },
          },
          {
            test: /\.(ttf|woff2?)$/,
            type: "asset/resource",
            generator: {
              filename: "static/media/[hash:8][ext][query]",
            },
          },
          {
            test: /\.js$/,
            // exclude: /node_modules/, // 排除node_modules代码不编译
            include: path.resolve(__dirname, "../src"), // 也可以用包含
            use: [
              {
                loader: "thread-loader", // 开启多进程
                options: {
                  workers: threads, // 数量
                },
              },
              {
                loader: "babel-loader",
                options: {
                  cacheDirectory: true, // 开启babel编译缓存
                  cacheCompression: false, // 缓存文件不要压缩
                  plugins: ["@babel/plugin-transform-runtime"], // 减少代码体积
                },
              },
            ],
          },
        ],
      },
    ],
  },
  plugins: [
    new ESLintWebpackPlugin({
      // 指定检查文件的根目录
      context: path.resolve(__dirname, "../src"),
      exclude: "node_modules", // 默认值
      cache: true, // 开启缓存
      // 缓存目录
      cacheLocation: path.resolve(
        __dirname,
        "../node_modules/.cache/.eslintcache"
      ),
      threads, // 开启多进程
    }),
    new HtmlWebpackPlugin({
      // 以 public/index.html 为模板创建文件
      // 新的html文件有两个特点:1. 内容和源文件一致 2. 自动引入打包生成的js等资源
      template: path.resolve(__dirname, "../public/index.html"),
    }),
    // 提取css成单独文件
    new MiniCssExtractPlugin({
      // 定义输出文件名和目录
      filename: "static/css/main.css",
    }),
    // css压缩
    // new CssMinimizerPlugin(),
  ],
  optimization: {
    minimizer: [
      // css压缩也可以写到optimization.minimizer里面,效果一样的
      new CssMinimizerPlugin(),
      // 当生产模式会默认开启TerserPlugin,但是我们需要进行其他配置,就要重新写了
      new TerserPlugin({
        parallel: threads, // 开启多进程
      }),
      // 压缩图片
      new ImageMinimizerPlugin({
        minimizer: {
          implementation: ImageMinimizerPlugin.imageminGenerate,
          options: {
            plugins: [
              ["gifsicle", { interlaced: true }],
              ["jpegtran", { progressive: true }],
              ["optipng", { optimizationLevel: 5 }],
              [
                "svgo",
                {
                  plugins: [
                    "preset-default",
                    "prefixIds",
                    {
                      name: "sortAttrs",
                      params: {
                        xmlnsOrder: "alphabetical",
                      },
                    },
                  ],
                },
              ],
            ],
          },
        },
      }),
    ],
    // 代码分割配置
    splitChunks: {
      chunks: "all", // 对所有模块都进行分割
      // 其他内容用默认配置即可
    },
  },
  // devServer: {
  //   host: "localhost", // 启动服务器域名
  //   port: "3000", // 启动服务器端口号
  //   open: true, // 是否自动打开浏览器
  // },
  mode: "production",
  devtool: "source-map",
};

(3) 给动态生成文件命名

我们发现前面无论多入口还单入口自动分割的文件名总是自动以数字命名,那么我们怎么自主给打包后动态分割的文件命名呢

a. 修改main.js(在import动态语法前添加webpackChunkName
document.getElementById("btn").onClick = function () {
  // eslint会对动态导入语法报错,需要修改eslint配置文件
  // webpackChunkName: "math":这是webpack动态导入模块命名的方式
  // "math"将来就会作为[name]的值显示。
  import(/* webpackChunkName: "math" */ "./js/math.js").then(({ count }) => {
    console.log(count(2, 1));
  });
};
b. eslint 配置.eslintrc.js(导入import插件让其识别import动态语法
  • 下载包:

npm i eslint-plugin-import -D

// .eslintrc.js
module.exports = {
  // 继承 Eslint 规则
  extends: ["eslint:recommended"],
  env: {
    node: true, // 启用node中全局变量
    browser: true, // 启用浏览器中全局变量
  },
  plugins: ["import"], // 解决动态导入import语法报错问题 --> 实际使用eslint-plugin-import的规则解决的
  parserOptions: {
    ecmaVersion: 6,
    sourceType: "module",
  },
  rules: {
    "no-var": 2, // 不能使用 var 定义变量
  },
};
c. webpack.config.js命名配置(既然此处统一了动态生成文件的命名,我们也把加载图片、字体、css文件的命名做一个统一
module.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,
  },
  module: {
    rules: [
      {
        oneOf: [
          {
            test: /\.(png|jpe?g|gif|svg)$/,
            type: "asset",
            parser: {
              dataUrlCondition: {
                maxSize: 10 * 1024, // 小于10kb的图片会被base64处理
              },
            },//将图片的命名在output统一
            // generator: {
            //   // 将图片文件输出到 static/imgs 目录中
            //   // 将图片文件命名 [hash:8][ext][query]
            //   // [hash:8]: hash值取8位
            //   // [ext]: 使用之前的文件扩展名
            //   // [query]: 添加之前的query参数
            //   filename: "static/imgs/[hash:8][ext][query]",
            // },
          },
          {////将字体的命名在output统一
            test: /\.(ttf|woff2?)$/,
            type: "asset/resource",
            // generator: {
            //   filename: "static/media/[hash:8][ext][query]",
            // },
          },
        ],
      },
    ],
  },
  plugins: [
    // 提取css成单独文件
    new MiniCssExtractPlugin({
      // 定义输出文件名和目录
      filename: "static/css/[name].css",
      chunkFilename: "static/css/[name].chunk.css",//统一css文件名
    }),
    // css压缩
    // new CssMinimizerPlugin(),
  ],
  mode: "production",
  devtool: "source-map",
};

12.2 Preload / Prefetch(空闲时间加载资源)

  • 为什么

我们前面已经做了代码分割,同时会使用 import 动态导入语法来进行代码按需加载(我们也叫懒加载,比如路由懒加载就是这样实现的)。
但是加载速度还不够好,比如:是用户点击按钮时才加载这个资源的,如果资源体积很大,那么用户会感觉到明显卡顿效果。
我们想在浏览器空闲时间,加载后续需要使用的资源。我们就需要用上 PreloadPrefetch 技术。

  • 是什么
    • Preload:告诉浏览器立即加载资源。
    • Prefetch:告诉浏览器在空闲时才开始加载资源。
    • 它们共同点:
      • 都只会加载资源,并不执行。
      • 都有缓存。
    • 它们区别:
      • Preload加载优先级高,Prefetch加载优先级低。
      • Preload只能加载当前页面需要使用的资源,Prefetch可以加载当前页面资源,也可以加载下一个页面需要使用的资源。
    • 总结:
      • 当前页面优先级高的资源用 Preload 加载
      • 下一个页面需要使用的资源用 Prefetch 加载
    • 它们的问题:兼容性较差。
      • 我们可以去 caniuse.com/ 网站查询 API 的兼容性问题
      • Preload 相对于 Prefetch 兼容性好一点。
  • 怎么用

下载包

npm i @vue/preload-webpack-plugin -D

配置 webpack.prod.js

const PreloadWebpackPlugin = require("@vue/preload-webpack-plugin");
    plugins: [
      new PreloadWebpackPlugin({
        rel: "preload", // preload兼容性更好
        as: "script",
        // rel: 'prefetch' // prefetch不用as
       }),
     ],

12.3 Network Cache(基于hash值 优化访问静态缓存)

  • 为什么

将来开发时我们对静态资源会使用缓存来优化,这样浏览器第二次请求资源就能读取缓存了,速度很快。
但是这样的话就会有一个问题, 因为前后输出的文件名是一样的,都叫 main.js,一旦将来发布新版本,因为文件名没有变化导致浏览器会直接读取缓存,不会加载新资源,项目也就没法更新了。
所以我们从文件名入手,确保更新前后文件名不一样,这样就可以做缓存了

  • 是什么

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

: fullhash(webpack4 是 hash)每次修改任何一个文件,所有文件名的 hash 至都将改变。所以一旦修改了任何一个文件,整个项目的文件缓存都将失效
: chunkhash 根据不同的入口文件(Entry)进行依赖文件解析、构建对应的 chunk,生成对应的哈希值。我们 js 和 css 是同一个引入,会共享一个 hash 值。
: contenthash(常用)根据文件内容生成 hash 值,只有文件内容变化了,hash 值才会变化。所有文件 hash 值是独享且不同的

  • 怎么用在需要静态缓存的地方将输出文件命名规则的hash改变
//webpack.config.js
module.exports = {
  entry: "./src/main.js",
  output: {
    path: path.resolve(__dirname, "../dist"), // 生产模式需要输出
    // [contenthash:8]使用contenthash,取8位长度
    filename: "static/js/[name].[contenthash:8].js", // 入口文件打包输出资源命名方式
    chunkFilename: "static/js/[name].[contenthash:8].chunk.js", // 动态导入输出资源命名方式
    assetModuleFilename: "static/media/[name].[hash][ext]", // 图片、字体等资源命名方式(注意用hash)
    clean: true,
  },
  plugins: [
    // 提取css成单独文件
    new MiniCssExtractPlugin({
      // 定义输出文件名和目录
      filename: "static/css/[name].[contenthash:8].css",
      chunkFilename: "static/css/[name].[contenthash:8].chunk.css",
    }),
    // css压缩
    // new CssMinimizerPlugin(),
  ]
  mode: "production",
  devtool: "source-map",
};
  • 问题:

当我们修改 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配置项中与minimizer、spliyChunks同级配置如下代码,打包后会自动生成一个runtime文件

// 提取runtime文件
    runtimeChunk: {
      name: (entrypoint) => `runtime~${entrypoint.name}`, // runtime文件命名规则
    },
  },

12.4 Core-js(处理babel处理不了的ES6以上语法)

  • 为什么

过去我们使用 babel 对 js 代码进行了兼容性处理,其中使用@babel/preset-env智能预设来处理兼容性问题。 它能将 ES6 的一些语法进行编译转换,比如箭头函数、...运算符等。但是如果是 async 函数、promise 对象、数组的一些方法(includes)等,它没办法处理。

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

  • 是什么

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

  • 怎么用(引入core-js包)

当我们在main.js里用到promise语法时,此时 Eslint 会对 Promise 报错

修改配置文件.eslintrc.js

  • 下载包

npm i @babel/eslint-parser -D

  • 修改配置文件.eslintrc.js
module.exports = {
  // 继承 Eslint 规则
  extends: ["eslint:recommended"],
  parser: "@babel/eslint-parser", // 支持最新的最终 ECMAScript 标准
  env: {
    node: true, // 启用node中全局变量
    browser: true, // 启用浏览器中全局变量
  },
  plugins: ["import"], // 解决动态导入import语法报错问题 --> 实际使用eslint-plugin-import的规则解决的
  parserOptions: {
    ecmaVersion: 6, // es6
    sourceType: "module", // es module
  },
  rules: {
    "no-var": 2, // 不能使用 var 定义变量
  },
};

运行指令

npm run build

此时不报错,但是观察打包输出的 js 文件,我们发现 Promise 语法并没有编译转换,所以我们需要使用 core-js 来进行 polyfill

使用 core-js

  • 下载包

npm i core-js

  • 使用core-js分三种情况
    • 手动全部引入(在用到promise语法的文件内,这里是main.js
import "core-js";
//这样引入会将所有兼容性代码全部引入,体积太大了。我们只想引入 promise 的 polyfill。
    • 手动按需引入(按node_moudle中的路径去引入
import "core-js/es/promise";
//只引入打包 promise 的 polyfill,打包体积更小。但是将来如果还想使用其他语法,我需要手动引入库很麻烦
    • 自动按需引入(常用,在babel.config.js中配置,只要babel处理不了的语言它就会自动去引入需要的core-js
module.exports = {
  // 智能预设:能够编译ES6语法
  presets: [
    [
      "@babel/preset-env",
      // 按需加载core-js的polyfill
      { useBuiltIns: "usage", corejs: { version: "3", proposals: true } },
    ],
  ],
};

12.5 PWA(离线缓存)

  • 为什么

开发 Web App 项目,项目一旦处于网络离线情况,就没法访问了。 我们希望给项目提供离线体验

  • 是什么

渐进式网络应用程序(progressive web application - PWA):是一种可以提供类似于 native app(原生应用程序) 体验的 Web App 的技术。其中最重要的是,在 离线(offline) 时应用程序能够继续运行功能

内部通过 Service Workers 技术实现的

  • 怎么用
    • 下载包

npm i workbox-webpack-plugin -D

    • 修改配置文件webpack.config.js
const WorkboxPlugin = require("workbox-webpack-plugin");
plugins: [
    new WorkboxPlugin.GenerateSW({
      // 这些选项帮助快速启用 ServiceWorkers
      // 不允许遗留任何“旧的” ServiceWorkers
      clientsClaim: true,
      skipWaiting: true,
    }),
],
    • 需要设置离线访问的文件中 配置如下代码(这里是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);
      });
  });
}
    • 运行指令(然后在dist目录下会生成一个service-worker.js文件)

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

    • 解决上述路径问题
      • 下载包(serve 也是用来启动开发服务器来部署代码查看效果的)

npm i serve -g

      • 运行指令

serve dist

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

总结

先来复习一下webpack基础配置

  • 两种开发模式
    • 开发模式:代码能编译自动化运行
    • 生产模式:代码编译优化输出
  • Webpack 基本功能
    • 开发模式:可以编译 ES Module 语法
    • 生产模式:可以编译 ES Module 语法,压缩 js 代码
  • Webpack 配置文件
    • 5 个核心概念
      • entry
      • output
      • loader
      • plugins
      • mode
    • devServer配置开发服务器9.1 SourceMap技术(源码-包后代码映射关系,找到错码位置)
  • Webpack 脚本指令用法
    • webpack 直接打包输出
    • webpack serve 启动开发服务器,内存编译打包没有输出

然后是优化配置

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

1. 提升开发体验

  • 使用 Source Map 让开发或上线时代码报错能有更加准确的错误提示。

2. 提升 webpack 提升打包构建速度

  • 使用 HotModuleReplacement 让开发时只重新编译打包更新变化了的代码,不变的代码使用缓存,从而使更新速度更快。
  • 使用 OneOf 让资源文件一旦被某个 loader 处理了,就不会继续遍历了,打包速度更快。
  • 使用 Include/Exclude 排除或只检测某些文件,处理的文件更少,速度更快。
  • 使用 Cache 对 eslint 和 babel 处理的结果进行缓存,让第二次打包速度更快。
  • 使用 Thead 多进程处理 eslint 和 babel 任务,速度更快。(需要注意的是,进程启动通信都有开销的,要在比较多代码处理时使用才有效果)

3. 减少代码体积

  • 使用 Tree Shaking 剔除了没有使用的多余代码,让代码体积更小。
  • 使用 @babel/plugin-transform-runtime 插件对 babel 进行处理,让辅助代码从中引入,而不是每个文件都生成辅助代码,从而体积更小。
  • 使用 Image Minimizer 对项目中图片进行压缩,体积更小,请求速度更快。(需要注意的是,如果项目中图片都是在线链接,那么就不需要了。本地项目静态图片才需要进行压缩。)

4. 优化代码运行性能

  • 使用 Code Split 对代码进行分割成多个 js 文件,从而使单个文件体积更小,并行加载 js 速度更快。并通过 import 动态导入语法进行按需加载,从而达到需要使用时才加载该资源,不用时不加载资源。
  • 使用 Preload / Prefetch 对代码进行提前加载,等未来需要使用时就能直接使用,从而用户体验更好。
  • 使用Network Cache能对输出资源文件进行更好的命名,将来好做缓存,从而用户体验更好。
  • 使用 Core-js 对 js 进行兼容性处理,让我们代码能运行在低版本浏览器。
  • 使用 PWA 能让代码离线也能访问,从而提升用户体验。

好了以上就是常见的webpack打包配置以及优化方案,目前可能更多的项目打包是基于搭建 React-Cli 和 Vue-Cli,而并非配置原生webpack,但了解基本配置也有助于我们继续往下学习~

最后,卷叭各位,该上号了 image.png