likes
comments
collection
share

适合新手的 webpack 5 实战演练

作者站长头像
站长
· 阅读数 41

还有必要学习 webpack 吗?

学习 webpack,对后面理解 vite,turbpack 肯定会有帮助。

接下来,本文将通过我对 webpack 的理解,结合案例,希望让大家理解 webpack,它能干什么,怎么使用,能做出什么有趣的东西?

起步

注意事项:运行 webpack5 最低 nodejs 版本为 10.13.0

webpack 就是一个打包工具。根据你的 webpack 配置文件(默认是根目录下的 webpack.config.js),把你的代码进行一系列处理,变成服务器可访问的资源。

开发时和准备生产上线时的配置文件可能不一样,所以大多数时候,若干不同的环境会配置若干不同的配置文件。

但你学习的话可以暂时只使用一个配置文件。

创建项目

webpack-demo
├── package-lock.json
├── package.json
├── public
│   └── index.html
├── src
│   ├── main.js
│   └── utils
│       └── say.js
└── webpack.config.js

现在这个项目还没有下载任何依赖。接下来就开始下载依赖。

# webpack 和 webpack-cli 是必须要的两个包。

npm install webpack webpack-cli --save-dev

# 下载 lodash 包

npm install lodash --save

# 下载 html-webpack-plugin

npm install html-webpack-plugin --save-dev

html-webpack-plugin 作用:

假设只有一个入口文件,这个入口文件会被 webpack 分析它的依赖导入,最终生成一个 bundle 文件。而 html-webpack-plugin 会自动生成一个 html 文件,将其 bundle 文件以 script 引入。

除了自动生成 html 文件,html-webpack-plugin 还可以指定一个 html 文件作为模板。它会复制到打包后的目录中,并自动引入打包后的 bundle 文件。

接下来配置下 webpack.config.js 文件。

// nodejs 内置模块
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  // 相对 entry 的根目录
  context: path.resolve(__dirname, ''),
  // 指定入口文件
  entry: './src/main.js',
  // 指定打包后的输出目录
  output: {
    // 指定打包后的文件名。相对的是 output.path。只能是相对路径
    filename: './js/main.js',
    // 指定打包后的目录
    path: path.resolve(__dirname, 'dist'),
    // 打包时先清空输出目录下的文件
    clean: true,
  },
  plugins: [
    // 使用插件
    new HtmlWebpackPlugin({
      // 文档上说明,可以是相对路径或者绝对路径
      template: './public/index.html'
    }),
  ],
}

在 main.js、asy.js 里我们使用了 import/export 模块语法,如果在 .html 文件里使用,浏览器其实是不认识的。但通过 webpack 的处理,我们可以随意使用 ES6 的模块化语法。

看看 main.js 文件

import { sayHi } from "./utils/say";

sayHi()

看看 say.js 文件

import _ from 'lodash'

export const sayHi = () => {
  const msg = _.join(['hello', 'world'])
  console.log(msg);
}

最后配置一个打包脚本,并执行 npm run build

  "scripts": {
    "build": "webpack"
  },

可以看到打包成功。

控制台截图:

适合新手的 webpack 5 实战演练

打包目录截图:

适合新手的 webpack 5 实战演练

双击 dist/index.html 文件,或者用 node 起一个服务器访问 index.html,在控制台中看见输出:

适合新手的 webpack 5 实战演练

之后我们修改了文件,想看到修改后的结果,重新执行 build 脚本就可以了。

使用 webpack-dev-server

说说 webpack 的 watch 模式

如果每次修改文件,想看到修改后的结果,都的手动执行一下 build 脚本,是不是太麻烦了。

在介绍 webpack-dev-server 之前,先说说 webpack 的 watch 模式。只需要在 webpack 的脚本后面加上参数 watch 就可以了。之后每次更新文件,会自动执行 webpack 脚本。

  "scripts": {
    "build": "webpack",
    "auto-watch": "webpack --watch"
  },

然后修改 say.js

import _ from 'lodash'

export const sayHi = () => {
-  const msg = _.join(['hello', 'world'])
+  const msg = _.join(['hello', 'world','webpack 5!'])
  console.log(msg);
}

打开浏览器再刷新一下,发现打印已经变了。

适合新手的 webpack 5 实战演练

但我们也发现,访问这个文件的协议居然是 file 协议。

最终我们打包后的文件都是要部署到服务器上的,如果我们想模拟一下 http 环境,是不是还得用 node 起一个服务器环境,还可能要使用到 nodemon、live-server 等包,感觉有点麻烦呀。

webpack-dev-server

这时候,我们就可以使用 webpack-dev-server 这个 npm 包来解决以上痛点了。

它的作用是,当我们修改文件时,自动根据配置文件重新打包,并且生成一个本地服务器地址让打包后的文件可预览。

并且它的打包产物是放在缓存里的,我们看不见。所以使用 webpack-dev-server的时候,output 配置是失效的。

下载

npm install --save-dev webpack-dev-server

然后怎么使用它呢?只需要在 webpack 构建命令后面加上参数 serve 就可以了。

让我们加一个脚本并使用这个脚本。

  "scripts": {
    "build": "webpack",
    "auto-watch": "webpack --watch",
+   "auto-build": "webpack serve --open" // --open 表示自动打开浏览器
  },

适合新手的 webpack 5 实战演练

这时候我们修改文件,再切到浏览器,等几秒(可能比较慢),会发现自动更新了,并且协议也是 http 协议。

适合新手的 webpack 5 实战演练

除此之外,我们还可以在 webpack.config.js 里配置 devServer 选线来获得跟多功能支持。比如:

module.exports = {
    ...
+   devServer: {
+     static: './dist',
+   },
    ...
}

有同学可能会问,devServer 里配置 static 有什么用?

static 默认值为 'public'。

假设你使用 webpack-dev-server 启动了一个服务器地址 localhost:8080,然后你还在 public 目录下放了一张名为 test.png 的图片。

你就可以直接通过 localhost:8080/test.png 访问到这张图片。

处理 CSS、图片、字体

webpack 只能识别 js、json 文件,其它如 css 文件、图片资源、字体图片是识别不了的。

这种情况下只能借助 webpack loader 来处理。

处理 CSS

下载

npm install --save-dev style-loader css-loader

配置 loader

  module: {
    rules: [
      {
        test: /\.css$/i,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },

解释:webpack 把每一个文件都视为一个 module 模块。通过 test 去匹配 module,匹配上的 module 再使用 use 设置的 loader 进行一系列处理。

新增 src/assets/css/style.css 文件。

.hello {
  color: red;
}

在 main.js 文件中引入

+ import './assets/css/style.css'

+ function component() {
+   const box = document.createElement('div');

+   const elment = document.createElement('h1');
+   elment.innerHTML = "h1"
+   elment.classList.add('hello');
+   box.appendChild(elment)
  
+   return box;
+ }

+ document.body.appendChild(component());

重新执行脚本 auto-build,效果如下:

适合新手的 webpack 5 实战演练

处理图片

webpack 5 已经内置支持处理图片了,只需要配置一下。

   module: {
     rules: [
       {
         test: /\.css$/i,
         use: ['style-loader', 'css-loader'],
       },
+      {
+        test: /\.(png|svg|jpg|jpeg|gif)$/i,
+        type: 'asset/resource',
+      },
     ],
   },

其中 type 可选值为 asset/resource、asset/inline、asset/source、asset。

  1. asset/resource 原封不动的对文件进行输出
  2. asset/inline 将资源转换为 base64 格式
  3. asset/source 将文件转换为字符串。比如你导入一个 .vue 组件,你并不是想在模板上使用这个组件,而只是想获取到写在这个文件里的字符串
`
<template>
  <div>hello</div>
<template>
<script></script>
<style></style>
`
  1. asset 小于某个大小的文件会转换成 base64 格式,否则不转换

然后下载一张图片 src/assets/img/love.png,在 main.js 里新增代码:

+ import loveImg from './assets/img/love.png'

+   // 将图像添加到我们已经存在的 div 中。
+   const img = new Image();
+   img.src = loveImg;
+   box.appendChild(img);

重新执行脚本 auto-build,效果如下:

适合新手的 webpack 5 实战演练

然后在 css 里也是可以直接将图片作为背景使用的,效果就不演示啦。

.hello {
  color: red;
  background: url('../img/love.png');
}

处理字体

在线链接形式引入

我试了下,如果通过在线链接的形式引入 iconfont,可以不配置 loader。

新建 src/assets/css/iconfont.css

@font-face {
  font-family: "iconfont"; /* Project id 3478861 */
  src: url('https://at.alicdn.com/t/c/font_3478861_tsqc6eercy.woff2?t=1670778451523') format('woff2'),
       url('https://at.alicdn.com/t/c/font_3478861_tsqc6eercy.woff?t=1670778451523') format('woff'),
       url('https://at.alicdn.com/t/c/font_3478861_tsqc6eercy.ttf?t=1670778451523') format('truetype');
}

.iconfont {
  font-family: "iconfont" !important;
  font-size: 16px;
  font-style: normal;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.icon-dianhuatianchong:before {
  content: "\e678";
}

.icon-a-youjianchakanyoujianfasongyoujianshouyoujian-06:before {
  content: "\e918";
}

然后不配置 loader,直接在 main.js 里使用:

+ import './assets/css/iconfont.css'

+   const div = document.createElement('div');
+   div.classList.add('iconfont');
+   div.classList.add('icon-dianhuatianchong');
+   box.appendChild(div)

适合新手的 webpack 5 实战演练

下载到本地引入

需要配置loader:

   module: {
     rules: [
       {
         test: /\.css$/i,
         use: ['style-loader', 'css-loader'],
       },
       {
         test: /\.(png|svg|jpg|jpeg|gif)$/i,
         type: 'asset/resource',
       },
+      {
+        test: /\.(woff|woff2|eot|ttf|otf)$/i,
+        type: 'asset/resource',
+      },
     ],
   },

然后在阿里巴巴矢量图标库进行下载 iconfont 适合新手的 webpack 5 实战演练 下载完成之后将 iconfont.css 文件复制到 src/assets/css下

适合新手的 webpack 5 实战演练

并将下面三个文件复制到 src/assets/font 目录下

适合新手的 webpack 5 实战演练

然后修改 iconfont.css 中的引入 woff2、woff、ttf 文件的路径

修改完成之后在 main.js 中引入 css 样式就可以使用了。这里就不贴图啦😝。

处理 JS 资源

Webpack 对 js 处理是有限的,只能编译 js 中 ES 模块化语法,不能编译其他语法,而针对 js 不同的浏览器是可能纯在兼容性问题的。

针对代码格式,我们使用 Eslint 来完成。

针对 js 兼容性处理,我们使用 Babel 来完成。

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

ESlint 检查工具

安装使用

下载

npm install eslint-webpack-plugin eslint --save-dev

然后把插件添加到你的 webpack 配置。例如:

const ESLintPlugin = require('eslint-webpack-plugin');

module.exports = {
  // ...
  plugins: [new ESLintPlugin()],
  // ...
};

然后使用该命令安装并配置 ESLint:

npm init @eslint/config

接下来你可以再控制台进行选择: 适合新手的 webpack 5 实战演练

选择完后,在根目录下会自动创建 .eslintrc.js 文件:

module.exports = {
  env: {
    browser: true,
    es2021: true,
  },
  extends: 'airbnb-base',
  overrides: [
  ],
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module',
  },
  rules: {
  },
};

接下来重新执行 webpack 脚本,会发现控制台报错,如图。可以按照报错信息更改,直到不再报错。

适合新手的 webpack 5 实战演练

关闭 eslint 检查

但是在实际项目中,很多时候都会把 eslint 检查给关闭了。因为对于大部分人而言代码的写法都不符合 eslint 规则,但一个个慢慢改的话很拖延时间,对于需要快速交付的项目,要求就是能跑就行,所以为了节省时间就把 eslint 规则给关了。

关闭方法其实不止一种,在网上可以搜到很多。在这里直接注释掉继承的规则就可以了。

// .eslintrc.js
module.exports = {
  env: {
    browser: true,
    es2021: true,
  },
+  // extends: 'airbnb-base',
  overrides: [
  ],
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module',
  },
  rules: {
  },
};

继承规则

当你不想关闭 eslint 检查并且不想慢慢搞 eslint 配置的时候,可以使用到 eslint 的继承规则。

在 github 上搜索很多人用的继承规则,比如:(这个规则就没 airbnb-base 那么苛刻)

适合新手的 webpack 5 实战演练

按照它的文档要求,下载了这么多包:(文档上写的就是要下这么多包)

npm install --save-dev eslint-config-standard eslint-plugin-promise eslint-plugin-import eslint-plugin-n

然后设置继承的规则。

配置的规则会覆盖掉继承的规则(配置的规则优先级高于继承的规则)。

// 例如在Vue项目中,我们可以这样写配置
module.exports = {
+   extends: ["standard"],
  rules: {
    // 我们的规则会覆盖掉继承的规则
    // 所以想要修改规则直接改就是了
+    eqeqeq: ["warn", "smart"],
  },
};

配置继承规则的时候,我们去掉了前缀 eslint-config-,因为 Eslint 会帮我们自动推断。

vscode eslint 语法检查工具

如果我们安装了 ESLint 语法检查工具的话,就会发现在还没有进行打包的时候就已经标红报错了,这样就会更加友好一些!

适合新手的 webpack 5 实战演练

eslint 忽略检查

但是这个时候在打开dist目录时会发现dis/static/js/main.js标红报错。

适合新手的 webpack 5 实战演练

但是打包的文件是不需要进行语法检测的,这个时候就只需要在项目根目录新建一个 .eslintignore 文件,写上 dist,这个时候语法检测就会自动忽略掉 dist 目录下的文件。

适合新手的 webpack 5 实战演练

// .eslintignore
dist

Babel 检查工具

bable 预设就是为了方便使用,将一系列插件(plugin)集合在了一起。

@bable/preset-env 的 useBuiltIns 配置选项值有三个:false(默认),entry,usage。

配置了 .browserslistrc 后:

举个例子

首先安装这三个依赖:

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

然后在 webpack.config.js 里配置:

  module: {
    rules: [
      {
        test: /\.css$/i,
        use: ['style-loader', 'css-loader'],
      },
      {
        test: /\.(png|svg|jpg|jpeg|gif)$/i,
        type: 'asset/resource',
      },
+      {
+        test: /\.m?js$/,
+        exclude: /(node_modules|bower_components)/,
+        use: {
+          loader: 'babel-loader'
+        }
+      }
    ],
  },

接着在根目录创建 babel.config.js

module.exports = {
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage"
      }
    ]
  ]
}

根目录创建 .browserslistrc 文件:

> 1%
not dead
ie 11

重新打包,会发现在 ie 浏览器中能正常打开。箭头函数式被转换为了普通函数。

useBuiltIns 为 false

时只会转换基本的语法,比如将 const、let 转换为 var,将箭头函数转换为普通函数。其它如 Promise API、String.inclues 是不支持的。

useBuiltIns 为 entry

会转换基本的语法。但是需要我们在入口文件处手动引入全部 polyfill。

npm install core-js@3 --save

# or

npm install core-js@2 --save

在 main.js 中引入

import "core-js/stable"
// 这个包会自动下载
import "regenerator-runtime/runtime"

useBuiltIns 为 usage

会转换基本的语法。还会按需引入 polyfill。

比较

除了将 useBuiltIns 设为 entry、usage 来获得 polyfill,还可以通过运行时 @babel/runtime 获得。

可以看看这篇文章,对 babel 有更详细的解释。

entry、usage 都是在原型上改写方法,会污染全局方法。

entry 全量引入 polyfill。

usage 虽然是按需引入,但一般排除对 node_moduleds 的编译,可能会出现一些第三方包兼容的错误。

运行时 @babel/runtime 也是按需引入 polyfill,并且还不会污染全局方法,但也有缺点:不会根据我们的配置的目标浏览器 .browserslistrc 文件动态调整 polyfill,相当于失效了。

对于如何使用的选择,上面的给那篇文章链接也解释得十分清楚,推荐大家去瞅瞅。

最后的问题:兼容性

有时候 babel 能兼容,但 webpack 不一定能兼容。

适合新手的 webpack 5 实战演练

区分开发模式和生产模式

async defer 区别

让我们执行以下 build 命令

  "scripts": {
    "build": "webpack",
    "auto-watch": "webpack --watch",
    "auto-build": "webpack serve"
  },

适合新手的 webpack 5 实战演练

会发现打包后的 script 标签里有个 defer 属性。其实不止 defer,还可以是 async。下面就解释一它们的区别。

浏览器在解析 html 的过程中,如果遇到一个没有任何属性的 script 标签,就会在加载并执行完这个 script 标签后再往下执行。如果这个 script 标签内容很多,无疑会阻塞页面渲染。

当浏览器遇到带 async 属性的 script 标签时,它的加载就是异步的。异步加载的过程中不会阻塞 html 的解析。当 script 脚本加载完了,不管此时 html 是否解析完,会先去执行 srcript 脚本。

defer 表示延迟,当浏览器遇到带有 defer 的 script 标签时,获取这个 script 脚本的网络请求也是异步的。当网络请求响应成功,如果此时 html 还没有解析完成,浏览器并不会暂停解析去执行 js 代码,而是会等待 html 解析完毕之后再去执行 js 代码。

不同配置文件

在开发环境下,我们需要使用 webpack-dev-server。而在生产环境打包时,我们并不需要。

在开发和生产环境中的配置不会完全相同。在这里直接写两个配置文件,一个用于开发,一个用于生产。

改变后的 script 脚本

  "scripts": {
    "dev": "webpack serve --config ./webpack.config.dev.js",
    "build": "webpack --config ./webpack.config.prod.js"
  },

根目录下 webpack.config.dev.js

// nodejs 内置模块
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ESLintPlugin = require('eslint-webpack-plugin');

module.exports = {
  // 相对 entry 的根目录
  context: path.resolve(__dirname, ''),
  // 指定入口文件
  entry: './src/main.js',
  // 开发环境下不需要 output 配置,webpack-dev-server 会将文件打包到缓存中。
  // output: {},
  devServer: {
    static: './dist',
  },
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: ['style-loader', 'css-loader'],
      },
      {
        test: /\.(png|svg|jpg|jpeg|gif)$/i,
        type: 'asset/resource',
      },
      {
        test: /\.m?js$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'babel-loader'
        }
      }
    ],
  },
  plugins: [
    // 使用插件
    new HtmlWebpackPlugin({
      // 文档上说,可以是相对路径或者绝对路径
      template: './public/index.html'
    }),
    new ESLintPlugin({
      context: path.resolve(__dirname, ''),
      extensions: ['js', 'jsx'],
      exclude: ['node_modules', 'dist'],
      fix: true
    })
  ],
  // 将模式设置为 development
  mode: 'development'
}

webpack.config.prod.js

// nodejs 内置模块
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ESLintPlugin = require('eslint-webpack-plugin');

module.exports = {
  // 相对 entry 的根目录
  context: path.resolve(__dirname, ''),
  // 指定入口文件
  entry: './src/main.js',
  // 指定打包后的输出目录
  output: {
    // 指定打包后的文件名。相对的是 output.path。只能是相对路径
    filename: './js/main.js',
    // 指定打包后的目录
    path: path.resolve(__dirname, 'dist'),
    // 打包时先清空输出目录下的文件
    clean: true,
    publicPath: ''
  },
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: ['style-loader', 'css-loader'],
      },
      {
        test: /\.(png|svg|jpg|jpeg|gif)$/i,
        type: 'asset/resource',
      },
      {
        test: /\.m?js$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'babel-loader'
        }
      }
    ],
  },
  plugins: [
    // 使用插件
    new HtmlWebpackPlugin({
      // 文档上说,可以是相对路径或者绝对路径
      template: './public/index.html'
    }),
    new ESLintPlugin({
      context: path.resolve(__dirname, ''),
      extensions: ['js', 'jsx'],
      exclude: ['node_modules', 'dist'],
      fix: true
    })
  ],
  // 生产模式,开启压缩代码
  mode: 'production'
}

样式兼容

css 文件提取

为什么要提取 css 成单独文件呢?

说说 style 与 link css区别

  • 渲染方面:

style 样式是使用 HTML 进行解析是异步的, 不会阻塞浏览器渲染,不会阻塞 DOM 解析, 会一边解析一边渲染

link css 标签解析虽然不会阻塞 DOM 树的解析但是会阻塞 DOM 树渲染, 所以必须等待它加载完成后才会渲染页面

  • 加载方面:

因为 style 是写在 html 中的首次加载会更快, link 是外部链接加载相对慢一些但是浏览器可以对该文件进行缓存减少下一次访问时间

  • 两者使用

个人认为应该具体分析使用情况, 如果样式较少可以使用 style 进行更改提高加载速度, 如果样式较多可以抽离一个单独的 css 文件. 当然如果不是对页面极致的性能需求我觉得还是使用 link 分离样式会更好,代码整洁也便于维护.

主要是使用到这个插件:mini-css-extract-plugin

npm install --save-dev mini-css-extract-plugin

然后修改 webpack 的 css-loader 配置

  module: {
    rules: [
      {
        test: /\.css$/i,
-       use: ['style-loader', 'css-loader', 'postcss-loader'],
+       use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'],
      }
    ],
  },

打包后发现 main.css 已经被提取出来了。

适合新手的 webpack 5 实战演练 但此时发现 css 文件内并没有压缩。

适合新手的 webpack 5 实战演练

css 文件压缩

首先,你需要安装 css-minimizer-webpack-plugin:

npm install css-minimizer-webpack-plugin --save-dev

接着在 webpack 配置中加入该插件。示例:

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

module.exports = {
  module: {
    rules: [
      {
        test: /.s?css$/,
        use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
      },
    ],
  },
  optimization: {
    minimizer: [
      // 在 webpack@5 中,你可以使用 `...` 语法来扩展现有的 minimizer(即 `terser-webpack-plugin`),将下一行取消注释
      // `...`,
      new CssMinimizerPlugin(),
    ],
  },
  plugins: [new MiniCssExtractPlugin()],
};

看看打包后的 css,已经被压缩了。 适合新手的 webpack 5 实战演练

css 样式兼容

样式兼容需要想到 post-css。在 webpack 中使用需要下载如下三个包:

npm install --save-dev postcss-loader postcss postcss-preset-env

webpack.config.js

  module: {
    rules: [
      {
        test: /\.css$/i,
-       use: [MiniCssExtractPlugin.loader, 'css-loader'],
+       use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
      }
    ],
  },

然后还需在根目录下添加 postcss.config.js 文件,写入

module.exports = {
  plugins: [
    [
      'postcss-preset-env',
      {
        // 其他选项
      },
    ],
  ],
};

同时我们设置更目录下的 .browserslistrc 文件内容为

ie >= 8

打包,会发现已经自动加上兼容的前缀了。

适合新手的 webpack 5 实战演练

适合新手的 webpack 5 实战演练

在这里说个遇到的坑,当我设置 .browserslistrc 为 ie 8 时,打包出来需要兼容的 css 并没有加上前缀。也不知道问题出在哪里,如果有兴趣的朋友可以给 postcss 提个 issue😢。

ie 8

source-map

开发时我们运行的代码是经过 webpack 编译后的,所有 css 和 js 合并成了一个文件,并且多了其他代码。此时如果代码运行出错,那么提示代码错误位置我们是看不懂的。

一旦将来开发代码文件很多,那么很难去发现错误出现在哪里。所以我们就需要想办法去用加准确的错误提示,来帮助我们更好的开发代码,所以这个时候就需要用到 SourceMap

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

使用

实际开发只需要关注以下两种情况就可以了。

属性优点缺点模式
devtool: "cheap-module-source-map"打包编译速度快,只包含行映射没有列映射用于生产模式
devtool: "source-map"包含行/列映射打包编译速度更慢用于开发模式

了解了之后增加以下配置 :

webpack.config.dev.js

module.exports = {
  ......
  mode: "development",
  devtool: "cheap-module-source-map",
};

webpack.config.prod.js

module.exports = {
  ......
  mode: "development",
  devtool: "source-map",
};

测试可以发现,当我们配置了 SourceMap,无论是在开发环境还是在生产环境,只要代码出现错误,在控制台都可以准确的将错误信息给到我们,大大的提升我们开发以及解决错误问题的效率!

热模块替换(HotModuleReplacement)

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

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

使用

热替换只能用于开发环境,生产环境是不需要的,且开发环境中 hot 配置默认是开启的。

webpack.config.dev.js

......
  devServer: {
    host: "localhost", // 启动服务器域名
    port: "3000", // 启动服务器端口号
    open: true, // 是否自动打开浏览器
    hot: true, // 开启HMR功能
  },
......

在 hot: true 的情况下运行开发环境,如下更改 css 的时候可以看到页面并没有刷新,而只是对更改的 css 进行了解析,这就是热模块替换

适合新手的 webpack 5 实战演练

虽然 css 实现了热模块替换,但 JS 还没有实现,更改 JS 文件进行保存还是会刷新页面重新打包编译,那 JS 实现热模块替换还需要进行下面的操作。

比如在 main.js 里作如下修改,之后更改 utils/say 文件,浏览器将不会刷新,而是会热模块替换。

import { sayHi } from './utils/say'

+ if (module.hot) {
+   module.hot.accept('./utils/say')
+ }

适合新手的 webpack 5 实战演练 不过每个地方都这样写很麻烦,在实际开发中会使用其他的 loader 来解决这个问题,比如 vue-loader。

oneOf

在运行打包代码时每个文件都会经过所有 loader 处理,虽然因为 test 正则原因实际没有处理上,但是都要走,如果loader和文件很多,那就会大大拖慢打包文件的速度,那么就可以使用oneOf,也就是只能匹配上一个 loader, 只要匹配到了剩下的就不匹配了。

使用

我们只需要在生产环境和开发环境的配置中将所有的规则像下面这样包起来就可以了:

  module: {
    rules: [
      {
        oneOf: [
          {
            test: /\.css$/i,
            use: [MiniCssExtractPlugin.loader, 'css-loader'],
          },
          {
            test: /\.(png|svg|jpg|jpeg|gif)$/i,
            type: 'asset/resource',
          },
          {
            test: /\.m?js$/,
            exclude: /(node_modules|bower_components)/,
            use: {
              loader: 'babel-loader'
            }
          }
        ]
      }
    ]
  },

Include/Exclude

在我们开发的时候需要使用第三方的库或插件,比如 vue-router、Vuex,这些文件都会下载到 node_modules 中了。而这些文件都是已经编译好可以直接使用的。

所以我们在对 js 文件处理时,可以使用 Include/Exclude 排除 node_modules 下面的文件。

include:包含,只对 xxx 文件进行编译处理。

exclude:排除,除了 xxx 文件都进行编译处理。

使用

Include/Exclude 主要是针对 js 文件进行处理。

  module: {
    rules: [
      {
        oneOf: [
          {
            test: /\.m?js$/,
+           // exclude: /(node_modules|bower_components)/,
+           include: path.resolve(__dirname, "./src"), // 也可以用包含
            use: {
              loader: 'babel-loader'
            }
          }
        ]
      }
    ]
  },
  plugins: [
    new ESLintPlugin({
      context: path.resolve(__dirname, ''),
      extensions: ['js', 'jsx'],
+     exclude: ['node_modules', 'dist']
    }),
  ],

Include 和 Exclude 只需要写一个就可以了,如果两个都写会报错!生产环境和开发环境同样的配置,完成之后分别进行打包测试即可。

Cache(提高打包构建速度)

在我们每次打包时 js 文件都要经过 Eslint 检查 和 Babel 编译,速度比较慢,但是我们并不会每次打包都对所有 js 文件进行了更改。

所以我们可以缓存之前的 Eslint 检查 和 Babel 编译结果,这样第二次打包时就只会对更改过的 js 文件进行编译处理,这样的话运行的速度就会更快了。

Babel 编译缓存

对 Babel 编译结果进行缓存,需要在 options 中通过 cacheDirectory 来开启 babel 缓存,为了追求打包速度可以关闭对缓存的压缩 cacheCompression: false,生产模式和开发模式配置相同。

  module: {
    rules: [
      {
        oneOf: [
          {
            test: /\.m?js$/,
            exclude: /(node_modules|bower_components)/,
            use: {
              loader: 'babel-loader',
+             options: {
+               cacheDirectory: true,
+               cacheCompression: false
+             }
            }
          }
        ]
      }
    ]
  },

Eslint 检查缓存

对 Eslint 检查结果进行缓存,需要在 plugins 中设置 cache 属性为 true 来开启Eslint缓存。

还可以通过 cacheLocation 来设置缓存存储的位置,默认写在 .eslintcache 文件里,生产模式和开发模式配置相同。

plugins: [
    new ESLintPlugin({
      context: path.resolve(__dirname, '../src'), // 需要检查的文件路径
      exclude:'node_modules',
+     cache: true, // 开启缓存
+     cacheLocation: path.resolve(__dirname,"../node_modules/.cache/.eslintcache"), // 缓存目录
    })
]

减少打包代码体积

Tree Shaking

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

Webpack 已经默认开启了这个功能,无需其他配置。它依赖 ES Module,如果使用的是 CommonJS 的话是没法移除没有使用的代码了。

Image Minimizer 图片压缩

在开发中如果项目引用了较多图片,那么图片体积会比较大,将来请求速度比较慢。因此我们可以对图片进行压缩,减少图片体积。

如果项目中图片都是在线链接,那么就不需要了。本地项目静态图片才需要进行压缩。

使用到这个插件 ImageMinimizerWebpackPlugin。不过我在测试的时候安装包总会失败,感觉有点难用。

代码分割

生成多个 js 文件

首先我们不作代码分割时,打包出来的文件长这样: 适合新手的 webpack 5 实战演练

然后我们进行更改配置 splitChunks 选项。这里有很多默认配置。(很多默认配置我暂时并不知道什么意思,希望以后能知道)

  output: {
    filename: './js/main.js',
    path: path.resolve(__dirname, 'dist'),
    clean: true,
    publicPath: ''
  },
  optimization: {
    minimizer: [
      new CssMinimizerPlugin(),
    ],
    // 代码分割配置
    splitChunks: {
-     chunks: 'async',
+     chunks: 'all',
-     minSize: 20000,
+     minSize: 0, // 我们定义的文件体积太小了,所以要改打包的最小文件体积
      minRemainingSize: 0,
      minChunks: 1,
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      enforceSizeThreshold: 50000,
      cacheGroups: {
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          reuseExistingChunk: true,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },

此时我们打包,发现会报错。

[webpack-cli] Error: Conflict: Multiple chunks emit assets to the same filename ./js/main.js (chunks 179 and 976)

适合新手的 webpack 5 实战演练

这里我们把 output 的 filename 改一下就可以了。

  output: {
-   filename: './js/main.js',
+   filename: './js/[name].js',
    path: path.resolve(__dirname, 'dist'),
    clean: true,
    publicPath: ''
  },

再次打包,dist 目录下的 js 文件就被分为多个了。

适合新手的 webpack 5 实战演练

按需加载,动态导入

此时直接点开打包后的 index.html,查看网络请求,发现分割的两个 js 文件是一同被加载的。

适合新手的 webpack 5 实战演练

如何做到 js 文件按需加载,动态导入呢。

首先修改下 main.js 文件

      // 将图像添加到我们已经存在的 div 中。
      const img = new Image()
      img.src = loveImg
-     img.onclick = sayHi
+     import('./utils/say').then(({ sayHi }) => {
+       sayHi()
+     })
      box.appendChild(img)

此时打包后的文件也发生了变化:

适合新手的 webpack 5 实战演练

当我们没点击图片时 适合新手的 webpack 5 实战演练 当我们点击了图片后

适合新手的 webpack 5 实战演练

也就是说,使用 import() 语法可以做到按需加载,动态导入。

魔法命名(Magic Comments)

可以看到动态导入的 js 文件名是一个 id。如果想要改变动态导入 js 文件的名字,可以使用魔法命名。

修改 main.js 文件

      // 将图像添加到我们已经存在的 div 中。
      const img = new Image()
      img.src = loveImg
+     import(/* webpackChunkName:'sayHi' */'./utils/say').then(({ sayHi }) => {
        sayHi()
      })
      box.appendChild(img)

注意 /* webpackChunkName:'sayHi' */ 的:别写成中文的:。

此时可以看到动态引入的 js 文件名变为了我们设置的 sayHi 名。

适合新手的 webpack 5 实战演练

在 output 中还可以设置 assetModuleFilename 属性,更改图片、字体等资源模块处理后输出后的命名。

  output: {
    filename: './js/[name].js',
    path: path.resolve(__dirname, 'dist'),
    clean: true,
    publicPath: '',
    chunkFilename:'./js/[name].js',
+   assetModuleFilename: 'static/media/[name].[ext]'
  },

打包后

适合新手的 webpack 5 实战演练

网络缓存 Network Cache

假设用户用浏览器打开了我们的开发的应用,那么一些资源可能就会被浏览器缓存。下次用户再打开这个应用,就可以直接访问缓存,加快应用的加载速度了。

假设浏览器缓存了 index.html 引入的 js 文件,然后我们在服务器上改了文件内容了,但这时浏览器还是读的缓存的内容,这种情况就不对了。

所以我们可以将打包生成的文件名都加上 hash 值,这样每次打包只要文件内容发生了变化文件名就会发生变化。

但是这样就会发现进行缓存之后,A 文件改变,B 文件引用 A 文件,会因为 A 文件文件名发生变化(hash 值),导致在 B 文件也发生更改,从而 B 文件也需要重新进行打包,那如果很多文件都引用了 A 文件,就会导致很多文件缓存失效,所以为了更好地进行缓存我们就可以用到下面的方法!

为了解决这个问题我们可以将 hash 值单独保存在一个文件中,这个文件只保存文件的 hash 值和它们与文件关系,这样这个文件体积也不会太大,重新请求这个文件也不会有太大的负荷。实现这个功能只需要进行如下配置(参考官方文档)

webpack.config.js

module.exports = {
  //...
  optimization: {
    runtimeChunk: {
      name: (entrypoint) => `runtime~${entrypoint.name}`,
    },
  },
};

完成如上配置,再次进行打包会发现生成一个 runtime.js 的文件,存放的就是 hash 值和文件的关系,这样 A 文件更改就不会影响到 B 文件,从而让浏览器能够更好的进行缓存,提高用户的使用体验!

渐进式网络应用程序(PWA)

参考官方文档,配置很简单。

添加包

npm install workbox-webpack-plugin --save-dev

调整 webpack.config.js

  const path = require('path');
  const HtmlWebpackPlugin = require('html-webpack-plugin');
+ const WorkboxPlugin = require('workbox-webpack-plugin');

  module.exports = {
    entry: {
      app: './src/index.js',
      print: './src/print.js',
    },
    plugins: [
+     new WorkboxPlugin.GenerateSW({
+       // 这些选项帮助快速启用 ServiceWorkers
+       // 不允许遗留任何“旧的” ServiceWorkers
+       clientsClaim: true,
+       skipWaiting: true,
+     }),
    ],
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist'),
      clean: true,
    },
  };

在入口文件添加判断

 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,用服务器访问 dist 目录下文件。再设置浏览为断网状态,此时刷新,控制台打印如下内容,就说明注册 PWA 成功了。

适合新手的 webpack 5 实战演练

后记

  1. 在多入口的时候设置 optimization.runtimeChunk: 'single'

有什么用?chunk 是什么?

多个模块构成一个 chunk,一般来讲,一个 chunk 对应一个 bundle。 什么时候不一般?比如我配置了 devtool: 'source-map',这时候虽然只有一个 chunk,但是却有两个 bundle(包含一个 source-map 文件)。

动态导入会单独生成一个 chunk

  1. 在设置 publicPath 为 / 时会导致直接打开打包后的 index.html 找不到图片、字体等资源。

但通过 live-server 打开可以找到对应资源。

  output: {
    path: path.resolve(__dirname, './dist'),
    filename: '[name].js',
    clean: true,
    // publicPath: '/', // 静态资源公共路径
  }
  1. 对于模块的理解 一个模块就像一个函数,一个文件,只会被执行一次。

所以导出一个对象,两个模块对对其属性修改,修改的是同一个对象。

总结

以上内容对于基本的理解 webpack 有一定帮助。欢迎评论交流。

如果有用,动手点个赞吧,谢谢。

转载自:https://juejin.cn/post/7181417236639252517
评论
请登录