webpack5(上)

本想做一次 webpack5 的分享,准备过程中发现内容很多,所以分了上篇/下篇,上篇主要讲解 Webpack 重要的基本概念、以及开发中遇到的 webpack 配置。

When & What

是何时出现的 解决了什么问题

首先,webpack 是为什么出现的呢?一般来说,新技术都是由当下的问题而催生出来的。

自从前端引入了模块化,很好的解决了这些历史问题:

  • 避免命名冲突
  • 更好的分离代码,按需加载
  • 高复用性
  • 高可维护性

但是,与此同时,它又造成了新的问题:

  • 浏览器环境兼容问题
  • 模块化划分出的文件会比较多,零散的模块文件将导致浏览器频繁发出请求
  • 不仅 Javascript 代码需要模块化,HTML/CSS 这些资源文件同样需要

这时候,我们需要一种工具可以让我们在开发阶段,享受模块化所带来的便利,同时也不用担心以上举例出的种种问题。 于是,很多模块打包工具应运而生,其中就有我们所熟知的webpack

基础概念/配置

如果准备搭建一个基础的 React 开发环境,看看我们需要哪些基础配置:

  • 加载资源文件
  • HtmlWebpackPlugin 文件模版
  • HMR 模块热替换
  • 代码分割 性能
  • SourceMap 定位报错
  • webpack-dev-server 开发环境
  • babel 解析 ES6/JSX

这是已完成基础配置的 config 文件: webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
const path = require("path")
const HtmlWebpackPlugin = require("html-webpack-plugin")

module.exports = {
  mode: "development",
  entry: {
    index: "./src/index.js",
  },
  devtool: "inline-source-map",
  devServer: {
    contentBase: "./dist",
    hot: true,
  },
  resolveLoader: {
    modules: ["node_modules", "./loaders/"],
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: "Development",
      template: "src/index.html",
    }),
  ],
  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/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-env", ["@babel/preset-react"]],
          },
        },
      },
      {
        test: /\.js$/i,
        include: path.resolve(__dirname, "src"),
        use: {
          loader: "i-loader",
          options: {
            lang: "CN",
          },
        },
      },
    ],
  },
  output: {
    clean: true,
    filename: "[name].[contenthash].js",
    path: path.resolve(__dirname, "dist"),
  },
  optimization: {
    usedExports: true,
    runtimeChunk: "single",
  },
}

下面根据配置文件,说说 webpack 最主要的几个基础概念:

  • Entry 告诉 Webpack 从哪儿开始构建内部关系图
  • Output 告诉 Webpack 在哪儿输出打包好的文件
  • Mode 让 Webpack 可以根据相应的环境做内置优化
  • Loader 让 Webpack 能够解析非 JS 类型文件,将它们转化为应用中的有效模块。
  • Plugin 让 Webpack 可以执行更广泛的任务,如打包优化/静态资源管理/环境变量注入

运行流程

  1. 初始化————启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler

  2. 编译————从 Entry 出发,针对每个 Module 串行调用对应的 Loader 去翻译文件内容,再找到该 Module 依赖的 Module,递归地进行编译处理。

  3. 输出————对编译后的 Module 组合成 Chunk,把 Chunk 转换成文件,输出。

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件可以监听事件执行逻辑,改变 Webpack 的运行结果。

自定义 Loader/Plugin

Loader 基础

Loader 就像是一个翻译员,能转化源文件然后输出新的结果,并且一个文件还可以链式的经过多个 loader 翻译。

下面以处理 SCSS 文件为例: .scss –> sass-loader –> css –> css-loader –> 找出 css 中依赖的资源并压缩 css –> style-loader –> 通过脚本加载的 JS 代码

可以看出以上的处理过程需要有顺序的链式执行,先 sass-loader 再 css-loader 再 style-loader。

配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const path = require("path")

module.exports = {
  entry: "./src/index.js",
  output: {
    filename: "bundle.js",
    path: path.resolve(__dirname, "dist"),
  },
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: ["style-loader", "css-loader"],
      },
    ],
  },
}

加载顺序: 由右到左 ⬅️,由下到上 ⬆️ 由上面的例子可以看出:每个 Loader 的职责是单一的,只需要完成一种转换。

如果一个源文件需要经历多步转换才能使用,就使用多个 Loader 去转换。

所以,在开发自定义 Loader 时,请保持其职责的单一性,你只需关心输入和输出。

自定义 Loader

一个 Loader 其实就是一个 Node.js 模块,这个模块需要导出一个函数。 这个函数的工作就是获得处理前的内容,返回处理后的内容。

一个最简单的 Loader 的源码如下:

1
2
3
4
5
module.exports = function (source) {
  // source 为 compiler 传递给 Loader 的一个文件的原内容
  // 该函数需要返回处理后的内容,这里简单起见,直接把原内容返回了,相当于该 Loader 没有做任何转换
  return source
}

Webpack 还提供一些 API 供 Loader 调用:

获得 Loader 的 options: loader-utils 返回 SourceMap: this.callback(err:Error | null, content: string | Buffer, souceMap?: SourceMap, abstractSyntaxTree?: AST) 异步返回: const callback = this.async() 处理二进制文件: module.exports.raw = true 关闭缓存功能: this.cacheable(false)

这是实现的一个自定义 Loader-国际化 Loader:

1
2
3
4
5
6
7
8
9
10
11
12
13
const loaderUtils = require("loader-utils")

module.exports = function (source) {
  const options = loaderUtils.getOptions(this)
  let str = ""
  if (options.lang === "CN") {
    str = "中文标题"
  } else {
    str = "English title"
  }
  let res = source.replace("$title", str)
  return res
}

config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    ...
    module: {
        rules: [
            {
                test: /\.js$/i,
                include: path.resolve(__dirname, 'src'),
                use: {
                    loader: "i-loader",
                    options: {
                        lang: 'CN'
                    }
                }
            },
        ],
    }
    ...
};

html

1
<div>hello {"$title"}</div>

Plugin 基础

Webpack 源码中 80%的代码都是基于 Plugin 机制编写的,随着 Pulgin 越来越多,webpack 能做的也越来越多。可以说 Plugin 是 webpack 的灵魂。对于 Pulgin 的实现机制,是我们熟知的一个设计模式——事件驱动。—— 《深入浅出 Webpack》

Compiler & Compilation 它们是 Plugin 和 Webpack 之间的桥梁。 Compiler 代表了 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译。

Compiler: Compiler 模块是通过 CLI 或 Node API 传递的所有配置项创建的编译实例,包含了 options、loaders、plugins 这些信息。 它继承了 Tapable 类从而可以注册和调用插件。

Compilation: Compilation 实例可以访问所有模块及其依赖项。当 Webpack 以开发模式运行时,每当监测到一个文件变化,一次新的 Compilation 将被创建。 它继承了 Tapable 类从而可以注册和调用插件。

自定义 Plugin

Webpack 通过 Plugin 机制让其更加灵活,以适应各种应用场景。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

一个最基础的 Plugin 的代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
class BasicPlugin {
  // 在构造函数中获取用户给该插件传入的配置
  constructor(options) {}

  // Webpack 会调用 BasicPlugin 实例的 apply 方法给插件实例传入 compiler 对象
  apply(compiler) {
    compiler.plugin("compilation", function (compilation) {})
  }
}

// 导出 Plugin
module.exports = BasicPlugin

在使用这个 Plugin 时,相关配置代码如下:

1
2
3
4
const BasicPlugin = require("./BasicPlugin.js")
module.export = {
  plugins: [new BasicPlugin(options)],
}

当为 webpack 开发插件时,需要知道每个 hook 在什么阶段被调用,具体需查阅文档

Webpack5 新特性介绍

  • 持久性缓存 尝试用持久性缓存来提高构建性能

  • 改进长期缓存 尝试用更好的算法和默认值来改进长期缓存

  • Tree Shaking 尝试用更好的 Tree Shaking 和代码生成来改善包大小

  • 平台兼容性 尝试改善与网络平台的兼容性