求求了 学一点webpack吧

64 阅读23分钟

webpack 基础

常见的问题

什么是模块化?

模块化是指解决一个复杂问题时自顶向下逐层把系统划分成若干模块的过程。(百度百科)

前端模块化一般指得是 JavaScript 的模块,最常见的是 Nodejs 的 NPM 包,图片,css 文件,每个模块可能是最小甚至是最优的代码组合,也可能是解决某些问题有很多特定模块组成的大模块。

多模块化的规范:CommonJS、AMD 和 ES6 Module 规范(另外还有 CMD、UMD 等)。

什么是 Webpack?

Webpack 是一个现代 JavaScript 应用程序的静态模块打包器(static module bundler)

10.png

Webpack 的工作流程是怎么样的?Webpack 可以做什么?

Webpack 的工作流程通常分为三个主要步骤:加载(Load)、转换(Transform)、打包(Bundle)。
首先,Webpack 从入口文件开始,递归地解析文件依赖关系,然后使用加载器(Loaders)对不同类型的文件进行加载和转换,将它们转换为 Webpack 可以识别和处理的模块。接下来,Webpack 根据依赖关系,将所有模块打包成一个或多个 bundle,最终生成用于部署的静态资源文件。

Webpack 还可以解决传统构建工具解决的问题

  • 模块化打包:Webpack 以模块为中心,可以将项目中的所有资源(包括 JS、CSS、图片等)都视为模块,并通过模块依赖关系进行打包管理,提高了代码的可维护性和可复用性。;
  • 语法糖转换Webpack 支持使用各种加载器(Loaders)对代码进行转换,包括将新版本的 JavaScript 或 TypeScript 转换为向后兼容的 JavaScript 代码,使开发者可以更自由地使用最新的语言特性
  • 预处理器编译:Webpack 可以集成各种预处理器,如 Less、Sass 等,使开发者可以使用更高级的语法和功能来编写样式代码,并将其转换为浏览器可识别的 CSS 代码
  • 项目优化:Webpack 提供了丰富的优化功能,包括代码压缩、资源合并、图片压缩等,帮助开发者优化项目性能,提高用户体验。
  • 解决方案封装:通过强大的 Loader 和插件机制,可以完成解决方案的封装

Webpack 中的概念有哪些?

五大核心概念入口、输出、插件、模块转化器、模式

  • entry: 指定 Webpack 构建的入口文件或入口文件集合,Webpack 将从这些入口文件开始构建应用程序的依赖树。
  • output: 定义 Webpack 构建完成后输出的文件的配置,包括输出路径、输出文件名等
  • loader: 模块转换器,用于将不同类型的文件转换为 webpack 或者 浏览器 能够处理的模块,以便打包到最终的输出文件中。加载器可以处理各种类型的文件,例如将 ES6 代码转换为 ES5、将 Sass 文件转换为 CSS 等。
  • 插件(plugins): 扩展插件,在 webpack 构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要做的事情
  • 模式(mode):一种配置选项,用于指定构建时的环境和行为。

还有一些其他的概念:

  • module:开发中每一个文件都可以看做 module,模块不局限于 js,也包含 css、图片等
  • chunk:代码块,一个 chunk 可以由多个模块组成
  • bundle:最终打包完成的文件,bundle 就是对 chunk 进行压缩打包等处理后的产出

HelloWorld Webpack

使用 npm init -y 进行初始化包配置文件。

要使用 webpack,那么需要安装 webpackwebpack-cli:

npm install webpack@5 webpack-cli@4 -D

创建目录 src,其结构如下

1.png

wepack V4.0.0 开始, webpack 是开箱即用的,在不引入任何配置文件的情况下就可以使用。

创建 hello-world.js 文件

module.exports = 'hello world';

创建 index.js 文件

const sayHello = require("./hello-world");
console.log(sayHello);

下面尝试一下 webpack 打包

// 方法一
npx webpack

// 方法二 scripts中添加 "start": "webpack"
npm run start

打包结果

2.png

9.png

执行成功,index.js 文件被打包到了 dist 文件夹下了,同时提示我们默认使用了 production mode,即生产环境,打开 dist/main.js,里面的代码的确是被压缩的,说明是生产环境打包

CLI 进阶

一般的 CLI 的命令都会有一个 help 命令:

npx webpack help

Usage: webpack [entries...] [options]
Alternative usage to run commands: webpack [command] [options]

The build tool for modern web applications.

Options:
  -c, --config <pathToConfigFile...>     Provide path to one or more webpack configuration files to process, e.g.
                                         "./webpack.config.js".
  --config-name <name...>                Name(s) of particular configuration(s) to use if configuration file exports an
                                         array of multiple configurations.
  -m, --merge                            Merge two or more configurations using 'webpack-merge'.
  --disable-interpret                    Disable interpret for loading the config file.
  --env <value...>                       Environment variables passed to the configuration when it is a function, e.g.
                                         "myvar" or "myvar=myval".
  --node-env <value>                     Sets process.env.NODE_ENV to the specified value.
  --analyze                              It invokes webpack-bundle-analyzer plugin to get bundle information.
  --progress [value]                     Print compilation progress during build.
  -j, --json [pathToJsonFile]            Prints result as JSON or store it in a file.
  --fail-on-warnings                     Stop webpack-cli process with non-zero exit code on warnings from webpack.
  -e, --extends <value...>               Path to the configuration to be extended (only works when using webpack-cli).
  -d, --devtool <value>                  A developer tool to enhance debugging (false | eval |
                                         [inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map).
  --no-devtool                           Negative 'devtool' option.
  --entry <value...>                     A module that is loaded upon startup. Only the last one is exported.
  --extends <value...>                   Path to the configuration to be extended (only works when using webpack-cli).
  --mode <value>                         Enable production optimizations or development hints.
  --name <value>                         Name of the configuration. Used when loading multiple configurations.
  -o, --output-path <value>              The output directory as **absolute path** (required).
  --stats [value]                        Stats options object or preset name.
  --no-stats                             Negative 'stats' option.
  -t, --target <value...>                Environment to build for. Environment to build for. An array of environments to
                                         build for all of them when possible.
  --no-target                            Negative 'target' option.
  -w, --watch                            Enter watch mode, which rebuilds on file change.
  --no-watch                             Negative 'watch' option.
  --watch-options-stdin                  Stop watching when stdin stream has ended.
  --no-watch-options-stdin               Negative 'watch-options-stdin' option.

Global options:
  --color                                Enable colors on console.
  --no-color                             Disable colors on console.
  -v, --version                          Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server'
                                         and commands.
  -h, --help [verbose]                   Display help for commands and options.

Commands:
  build|bundle|b [entries...] [options]  Run webpack (default command, can be omitted).
  configtest|t [config-path]             Validate a webpack configuration.
  help|h [command] [option]              Display help for commands and options.
  info|i [options]                       Outputs information about your system.
  serve|server|s [entries...]            Run the webpack dev server and watch for source file changes while serving. To
                                         see all available options you need to install 'webpack', 'webpack-dev-server'.
  version|v [options]                    Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server'
                                         and commands.
  watch|w [entries...] [options]         Run webpack and watch for files changes.

To see list of all supported commands and options run 'webpack --help=verbose'.

Webpack documentation: https://webpack.js.org/.
CLI documentation: https://webpack.js.org/api/cli/.
Made withby the webpack team.

说明:

  • --config,-c:指定一个 Webpack 配置文件的路径
  • --mode:指定打包环境的 mode,取值为 development | production |none 对应着 开发环境|生产环境|不适用默认配置;
  • --progress:显示 Webpack 打包进度
  • --watch, -w:watch 模式打包,监控文件变化之后重新开始打包

配置文件

Webpack 是可配置的模块打包工具,我们可以通过修改 Webpack 的配置文件(webpack.config.js)来对 Webpack 进行配置

简单的 webpack.config.js 示例

const path = require("path");

module.exports = {
  mode: "development",
  entry: "./src/index.js",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "server.bundle.js",
  },
};

默认情况下,Webpack 会查找执行目录下面的 webpack.config.js 作为配置,如果需要指定某个配置文件,可以使用下面的命令:

webpack -c webpack.config.js

除了配置文件的语法多样之外,对于配置的类型也是多样的,最常见的是直接作为一个对象来使用,除了使用对象,Webpack 还支持函数、Promise 和多配置数组

常见配置

mode 模式

Webpack4.0 开始引入了 mode 配置。

通过配置 mode=development 或者 mode=production 来制定是开发环境打包,还是生产环境打包,比如生产环境代码需要压缩,图片需要优化,Webpack 默认 mode 是生产环境,即 mode=production。

除了在配置文件中设置 mode:

module.exports = {
  mode: "development",
};

还可以在命令行中设置 mode:

npx webpack -c webpack.config.js --mode development

entry 入口

Webpack 的 entry 支持多种类型,包括字符串、对象、数组。从作用上来说,包括了单文件入口和多文件入口两种方式。

单文件入口 单文件的用法如下:

module.exports = {
  entry: {
    main: "path/to/my/entry/file.js",
  },
};
// 简写
module.exports = {
  entry: "path/to/my/entry/file.js",
};

entry 还可以传入包含文件路径的数组,当 entry 为数组的时候也会合并输出,例如下面的配置:

module.exports = {
  mode: "development",
  // 无论是字符串还是字符串数组的 entry,实际上都是只有一个入口
  entry: ["./src/app.js", "./src/home.js"],
  output: {
    filename: "array.js",
  },
};

多文件入口 多文件入口如下:

多文件入口是使用对象语法来通过支持多个 entry,多文件入口的对象语法相对于单文件入口,具有较高的灵活性,例如多页应用、页面模块分离优化

module.exports = {
  entry: {
    home: "path/to/my/entry/home.js",
    search: "path/to/my/entry/search.js",
    list: "path/to/my/entry/list.js",
  },
};

Tips:对于一个 HTML 页面,推荐只有一个 entry ,通过统一的入口,解析出来的依赖关系更方便管理和维护。

多入口的应用场景是:旧的 JSP 或者 PHP 项目,前端需要工程化工具提供 css 预编译、JS 压缩。

output 输出

webpack 的 output 是指定了 entry 对应文件编译打包后的输出 bundle

output 的常用属性是:

  • path:制定了输出的 bundle 存放的路径,比如 dist、output 等
  • filename:这个是 bundle 的名称
  • publicPath:指定了一个在浏览器中被引用的 URL 地址( CDN_URL)

一个 webpack 的配置,可以包含多个 entry,但是只能有一个 output

对于不同的 entry 可以通过 output.filename 占位符语法来区分,比如:

module.exports = {
  entry: {
    home: "path/to/my/entry/home.js",
    search: "path/to/my/entry/search.js",
    list: "path/to/my/entry/list.js",
  },
  output: {
    filename: "[name].js",
    path: __dirname + "/dist",
  },
};

其中[name]就是占位符,它对应的是 entry 的 key(home、search、list),所以最终输出结果是:

path/to/my/entry/`home`.js   → dist/`home`.js
path/to/my/entry/`search`.js → dist/`search`.js
path/to/my/entry/`list`.js   → dist/`list`.js

output.library

如果打包的目的是生成一个供别人使用的库,那么可以使用 output.library 来指定库的名称

module.exports = {
  output: {
    library: "myLib", // '[name]'
  },
};
import myLib from "your-lib-name";

output.libraryTarget

使用 output.library 确定了库的名称之后,还可以使用 output.libraryTarget 指定库打包出来的规范,

tips:output.library 不能与 output.libraryTarget 一起使用

target

在项目开发中,不仅仅是开发 web 应用,还可能开发的是 Node.js 服务应用、或者 electron 这类跨平台桌面应用,这时候因为对应的宿主环境不同,所以在构建的时候需要特殊处理。webpack 中可以通过设置 target 来指定构建的目标(target)。

module.exports = {
  // 默认是 web,可以省略
  target: "web",
};

devtool

devtool 是来控制怎么显示 sourcemap,通过 sourcemap 可以快速还原代码的错误位置,从而方便调试和定位错误

但是由于 sourcemap 包含的数据量较大,而且生成算法需要计算量支持,所以 sourcemap 的生成会消耗打包的时间,下面的表格整理了不同的 devtool 值对应不同的 sourcemap 类型对应打包速度和特点。

devtool构建速度重建生产环境品质+建议
留空,none++++yes打包后的代码(默认)
eval+++no生成后的代码(开发+高性能)
eval-cheap-source-mapo+no转换过的代码(仅限行)
eval-cheap-module-source-map+no原始源代码(仅限行)
eval-source-map––ono原始源代码(开发+sourcemap)
cheap-source-mapono转换过的代码(仅限行)
cheap-module-source-mapno原始源代码(仅限行)
source-map––––yes原始源代码(生产+sourcemap)
inline-cheap-source-mapono转换过的代码(仅限行)
inline-cheap-module-source-mapno原始源代码(仅限行)
inline-source-map––––no原始源代码
eval-nosources-cheap-source-mapo+no转换过的代码(不包括源代码)
eval-nosources-source-map––ono原始源代码
hidden-source-mapyes原始源代码
nosources-source-mapyes无源代码内容

++ 快速, + 比较快, o 中等, - 比较慢, -- 慢
上表中,未完全展示

// development
devtool: "eval-cheap-module-source-map";
//  production
devtool: "cheap-module-source-map";

resolve

resolve 配置是帮助 Webpack 查找依赖模块的,通过 resolve 的配置,可以帮助 Webpack 快速查找依赖,也可以替换对应的依赖

resolve.extensions

resolve.extensions 是帮助 Webpack 解析扩展名的配置,默认值:['.wasm', '.mjs', '.js', '.json'],所以引入 js 和 json 文件,可以不写它们的扩展名,通常可以加上 .css、.less 等

module.exports = {
  resolve: {
    extensions: [".js", ".json", ".css"],
  },
};

因此,当你导入./utils/index 时,Webpack 会尝试按照以下顺序查找文件: "./utils/index.js"
"./utils/index.json"
"./utils/index.css"
都没有的话打包就直接报错了

resolve.alias

resolve.alias 是最常用的配置,通过设置 alias 可以帮助 webpack 更快查找模块依赖,而且也能使编写代码更加方便。例如,在实际开发中经常会把源码都放到 src 文件夹,目录结构如下:


src
├── lib
│ └── utils.js
└── pages
└── demo
└── index.js

在 src/pages/demo/index.js 中如果要引用 src/lib/utils.js 那么可以通过:import utils from '../../lib/utils'; ,如果目录更深一些,会越来越难看,这是可以通过设置 alias 来缩短这种写法,例如:

module.exports = {
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "src"),
      src: path.resolve(__dirname, "src"),
      "@lib": path.resolve(__dirname, "src/lib"),
    },
  },
};

经过设置了 alias,可以在任意文件中,不用理会目录结构,直接使用 require('@lib/utils')或者 require('src/lib/utils')来帮助 Webpack 定位模块。

module 模块

module 配置决定了如何处理项目中的不同类型的模块。

何为 webpack 模块 下面是一些示例

  • import 语句
  • CommonJS require() 语句
  • AMD define 和 require 语句
  • css/sass/less 文件中的 @import 语句。
  • stylesheet url(...) 或者 HTML <img src=...> 文件中的图片链接。

module.noParse

module.noParse 用于配置哪些模块文件不需要解析。这对于一些第三方库或者大型的库文件来说,可以提高构建性能,因为 webpack 在构建过程中不需要解析这些文件的依赖关系。(不用去分析依赖的依赖)
例如,如果你知道一个第三方库不会引用其他模块,你可以在 webpack 配置中添加 noParse 来跳过对该库的解析,加快构建速度。

module.exports = {
    module: {
        noParse: /jquery|lodash/
        // 使用函数,从 Webpack 3.0.0 开始支持
        noParse: (content) => {
            // content 代表一个模块的文件路径
            // 返回 true or false
            return /jquery|lodash/.test(content);
        }
    }
}

Tips:这里一定要确定被排除出去的模块代码中不能包含 import、require、define 等内容,以保证 webpack 的打包包含了所有的模块,不然会导致打包出来的 js 因为缺少模块而报错。

module.rules

module.rules 是在处理模块时,将符合规则条件的模块,提交给对应的处理器来处理,通常用来配置 loader,其类型是一个数组,数组里每一项都描述了如何去处理部分文件。每一项 rule 大致可以由以下三部分组成:

  • 条件匹配:通过 test、include、exclude 等配置来命中可以应用规则的模块文件;
  • 应用规则:对匹配条件通过后的模块,使用 use 配置项来应用 loader,可以应用一个 loader 或者按照从后往前的顺序应用一组 loader,当然还可以分别给对应 loader 传入不同参数;
  • 重置顺序:一组 loader 的执行顺序默认是**从后到前(或者从右到左)**执行,通过 enforce 选项可以让其中一个 loader 的执行顺序放到最前(pre)或者是最后(post)。
{
  test: /\.(j|t)sx?$/,
  include: [
    path.resolve(__dirname, 'src'),
  ],
  exclude: [
    path.resolve(__dirname, 'node_modules')
  ]
}

Loader 解析处理器

loader 是解析处理器,通过 loader 可以将 ES6 语法的 js 转化成 ES5 的语法,可以将图片转成 base64 的 dataURL

在使用对应的 loader 之前,需要先安装它,例如,要在 JavaScript 中引入 less,则需要安装 less-loader:

npm i -D less-loader

loader 有两种配置方式

  • 使用 webpack.config.js 的配置方式:
module.exports = {
  module:{
    rules:[
      test: /\.less$/, use:'less-loader'
    ]
  }
}
  • 在 JavaScript 文件内使用内联配置方式
const html = require("html-loader!./loader.html");
// or
import html from "html-loader!./loader.html";

Loader 的参数

给 loader 传参的方式有两种:

  • 通过 options 传入,以及通过 query 的方式传入
// inline内联写法,通过 query 传入
const html = require("html-loader?title=helloWebpack!./file.html");
// config内写法,通过 options 传入
module: {
  rules: [
    {
      test: /\.html$/,
      use: [
        {
          loader: "html-loader",
          options: {
            minimize: true,
            removeComments: false,
            collapseWhitespace: false,
          },
        },
      ],
    },
  ];
}
// config内写法,通过 query 传入
module: {
  rules: [
    {
      test: /\.html$/,
      use: [
        {
          loader:
            "html-loader?minimize=true&removeComments=false&collapseWhitespace=false",
        },
      ],
    },
  ];
}

Loader 的解析顺序

对于一些类型的模块,简单配置一个 loader 是不能够满足需求的,例如 less 模块类型的文件,只配置了 less-loader 仅仅是将 Less 语法转换成了 CSS 语法,但是 JS 还是不能直接使用,所以还需要添加 css-loader 来处理,这时候就需要注意 Loader 的解析顺序了。前面已经提到了,Webpack 的 Loader 解析顺序是从右到左(从后到前)的,即:

// query 写法从右到左,使用!隔开
const styles = require("css-loader!less-loader!./src/index.less");
// 数组写法,从后到前
module.exports = {
  module: {
    rules: [
      {
        test: /\.less$/,
        use: [
          {
            loader: "style-loader",
          },
          {
            loader: "css-loader",
          },
          {
            loader: "less-loader",
          },
        ],
      },
    ],
  },
};

如果需要调整 Loader 的执行顺序,可以使用 enforce,enforce 取值是 pre|post,pre 表示把放到最前,post 是放到最后:

use: [
  {
    loader: "babel-loader",
    // enforce:'post' 的含义是把该 loader 的执行顺序放到最后
    // enforce 的值还可以是 pre,代表把 loader 的执行顺序放到最前
    enforce: "post",
  },
];

plugin 插件

plugin 是 Webpack 的重要组成部分,通过 plugin 可以解决 loader 解决不了的问题。

Webpack 本身就是有很多插件组成的,所以内置了很多插件,可以直接通过 webpack 对象的属性来直接使用,例如:

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

除了内置的插件,也可以通过 NPM 包的方式来使用插件,例如:html-webpack-plugin

//首先引入插件
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
  //...
  plugins: [
    //数组 放着所有的webpack插件
    new HtmlWebpackPlugin({
      title: "webpack-html-template",
      template: "./public/index.html",
      filename: "public/index.html",
    }),
  ],
};

Tips:loader 面向的是解决某个或者某类模块的问题,而 plugin 面向的是项目整体,解决的是 loader 解决不了的问题

babel 处理 js 资源

原因是 Webpack 对 js 处理是有限的,只能编译 js 中 ES 模块化语法,不能编译其他语法,导致 js 不能在低版本浏览器运行,所以我们希望做一些兼容性处理

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

  1. 配置文件 配置文件由很多种写法:
  • babel.config.js
  • babel.config.json
  • .babelrc
  • .babelrc.js
  • .babelrc.json
  • package.json 中 babel:不需要创建文件,在原有文件基础上写

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

具体配置

presets 预设:简单理解就是一组 Babel 插件, 扩展 Babel 功能

  • @babel/preset-env: 一个智能预设,允许您使用最新的 JavaScript。
  • @babel/preset-react:一个用来编译 React jsx 语法的预设
  • @babel/preset-typescript:一个用来编译 TypeScript 语法的预设
npm i babel-loader @babel/core @babel/preset-env -D
// babel.config.js
module.exports = {
  presets: ["@babel/preset-env"],
};
// webpack.config.js
{
        test: /\.js$/,
        exclude: /node_modules/, // 排除node_modules代码不编译
        loader: "babel-loader",
      },

tips : 这里只对 js 的语法进行转换 比如 箭头函数 ...rest(剩余参数) 这种语法进行转换

Core-js 对 js 词法进行转换

npm i core-js
  • 手动全部引入
// main.js
import "core-js";

这样引入会将所有兼容性代码全部引入,体积太大了。我们只想引入 promise 的 polyfill。

  • 手动按需引入
import "core-js/es/promise";

只引入打包 promise 的 polyfill,打包体积更小。但是将来如果还想使用其他语法,我需要手动引入库很麻烦。

  • 自动按需引入
module.exports = {
  // 智能预设:能够编译ES6语法
  presets: [
    [
      "@babel/preset-env",
      // 按需加载core-js的polyfill
      { useBuiltIns: "usage", corejs: { version: "3", proposals: true } },
    ],
  ],
};

此时就会自动根据我们代码中使用的语法,来按需加载相应的 polyfill 了

CSS 处理

webpack 不能直接处理 css,需要借助 loader。如果是 .css,我们需要的 loader 通常有: style-loader、css-loader,考虑到兼容性问题,还需要 postcss-loader,而如果是 less 或者是 sass 的话,还需要 less-loader 和 sass-loader,这里配置一下 less 和 css 文件(sass 的话,使用 sass-loader 即可):

Webpack 处理 css,需要借助:

  • css-loader:让 webpack 认识 css 文件;
  • style-loader:把 webpack 处理完的 css 加载到 html 中,相当于是<style>引入
  • Less-loader,scss-loader 等:把 scss,less 这些语言转化为 css;
rules: [
  {
    test: /\.(sa|sc|c)ss$/,
    use: [
      // 不能同时使用 官方推荐
      // 开发 >> style-loader
      // 生产 >> MiniCssExtractPlugin(<link>的方式通过 URL 的方式引入进来)
      {
        loader: !isProduction ? "style-loader" : MiniCssExtractPlugin.loader,
      },
      {
        loader: "css-loader",
        options: {
          // css里面引入css文件的话 会跳过下面的sass-loader 直接运行css-loader,需要向下跑2个loader
          importLoaders: 2,
          // modules: true,
        },
      },
      "postcss-loader",
      "sass-loader",
    ],
    exclude: /node_modules/,
  },
];
// postcss.config.js

module.exports = {
  // 还需要再package文件中配置 browserslist
  plugins: [require("autoprefixer")],
};
// package.json

{
  "browserslist": [
    "defaults",
    "ie > 11",
    "last 2 versions",
    "> 1%",
    "iOS 7",
    "last 3 iOS versions"
  ]
}

资源处理

在 webpack 5 之前,通常使用

  • raw-loader 将文件导入为字符串
  • file-loader 能够根据配置项复制使用到的资源(不局限于图片)到构建之后的文件夹,并且能够更改对应的链接;
  • url-loader 包含 file-loader 的全部功能,并且能够根据配置将符合配置的文件转换成 Base64 方式引入,将小体积的图片 Base64 引入项目可以减少 http 请求,也是一个前端常用的优化方式。

下面以 url-loader 为例说明下 Webpack 中使用方法。

{
  test: /\.(jpg|png|gif)$/,
  use: {
    loader: "url-loader",
    options: {
      name: "[name]_[hash:6].[ext]",
      outputPath: "images/",
      limit: 10 * 1024, // 10kb
    },
  },
};

其他资源处理

对于字体、富媒体等静态资源,可以直接使用 url-loader 或者 file-loader 进行配置即可,不需要额外的操作,具体配置内容如下:

{
    // 文件解析
    test: /\.(eot|woff|ttf|woff2|appcache|mp4|pdf)(\?|$)/,
    loader: 'file-loader',
    query: {
        // 这么多文件,ext不同,所以需要使用[ext]
        name: 'assets/[name].[hash:7].[ext]'
    }
},

Tips:如果不需要 Base64,那么可以直接使用 file-loader,需要的话就是用 url-loader,还需要注意,如果将正则(test)放在一起,那么需要使用[ext]配置输出的文件名。

配置 CDN 域名

一般静态资源上线的时候都会放到 CDN,假设的 CDN 域名和路径为:http://xxx/img/,这时候只需要修改 output.publicPath 即可:

module.exports = {
  output: {
    publicPath: "http://xxx.com/img/",
  },
};

修改后执行 webpack 打包后的结果如下:

<img
  src="http://xxx.com/img/ad19429dc9b3ac2745c760bb1f460892.png"
  alt="背景图"
/>

在 webpack 5 之后

webpack 5 中,文件类型被视为 asset,则它将使用 asset 模块来处理,而不是传统的 loader,这可能会导致与旧的 assets loader(如 url-loader)冲突。通过将类型设置为 'javascript/auto',我们可以避免这种冲突,确保旧的 assets loader 能够正常工作。

module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif)$/i,
        use: [
          {
            loader: "url-loader",
            options: {
              limit: 10 * 1024,
            },
          },
        ],
        type: "javascript/auto",
      },
    ],
  },
};

使用 webpack5 asset 模块处理资源

[
  // 文件资源 静态资源
  {
    test: /\.(png|jpg|jpeg|gif|svg)$/i,
    type: "asset/resource",
    generator: {
      filename: "images/[name].[hash][ext]",
    },
  },
  // 文件资源 base64
  {
    test: /\.svg/,
    type: "asset/inline",
    generator: {
      dataUrl: (content) => {
        content = content.toString();
        return svgToMiniDataURI(content);
      },
    },
  },
  // 文件资源
  {
    test: /\.(text|png)$/i,
    type: "asset",
    generator: {},
    parser: {
      dataUrlCondition: {
        maxSize: 400 * 1024,
      },
    },
  },
];

进阶

静态资源拷贝

我们需要使用已有的 JS 文件、CSS 文件(本地文件),但是不需要 webpack 编译。例如,我们在 public/index.html 中引入了 public 目录下的 js 或 css 文件。这个时候,如果直接打包,那么在构建出来之后,肯定是找不到对应的 js / css 了。

webpack 提供了好用的插件 CopyWebpackPlugin,它的作用就是将单个文件或整个目录复制到构建目录。

npm install copy-webpack-plugin -D
new CopyWebpackPlugin({
        patterns: [
          {
            from: "public",
            // to: path.resolve(__dirname, "./dist/public"),
            to: path.resolve(__dirname, "./dist"),
            globOptions: {
              ignore: ["**/index.html"], // 忽略 public 文件夹下的 index.html 文件
            },
          },
          //还可以继续配置其它要拷贝的文件
        ],
      }),

将抽离出来的 css 文件进行压缩

使用 mini-css-extract-plugin,CSS 文件默认不会被压缩,如果想要压缩,需要配置 optimization,首先安装 optimize-css-assets-webpack-plugin | css-minimizer-webpack-plugin

npm install optimize-css-assets-webpack-plugin -D
npm install css-minimizer-webpack-plugin -D
//webpack.config.js
const OptimizeCssPlugin = require("optimize-css-assets-webpack-plugin");
module.exports = {
  entry: "./src/index.js",
  //....
  plugins: [new OptimizeCssPlugin()],
};
{optimization: {
      minimizer: [
      // css压缩,
      new CssMinimizerPlugin(),
      new TerserPlugin(),
    ],
    minimize: true,
  }, }

tips:配置 minimizer 之后,生产模式下自带的 js 压缩以及 jsjs 摇树功能丢失了,我们需要使用另外的插件进行 js 的压缩以及摇树 TerserPlugin

按需加载

很多时候我们不希望一次性加载所有的 JS 文件,而希望在需要的时候按需加载。webpack 内置了强大的分割代码的功能可以实现按需加载。

比如,我们在点击了某个按钮之后,才需要使用使用对应的 JS 文件中的代码,需要使用 import() 语法:

document.querySelector(".webpack").onclick = function () {
  import("./utils/index").then(({ sum }) => console.log(sum(1, 2, 3)));
};

3.png

当代码执行到 import 所在的语句时,才会加载该 Chunk 所对应的文件

热更新

  • 首先配置 devServer 的 hot 为 true
  • 并且在 plugins 中增加 new webpack.HotModuleReplacementPlugin() (这个配置 webpack 会给你自动加上,无需手动添加)
//webpack.config.js
const webpack = require("webpack");
module.exports = {
  //....
  devServer: {
    hot: true,
  },
  plugins: [
    //热更新插件
    new webpack.HotModuleReplacementPlugin(),
  ],
};
  • 在入口文件中新增:
if (module.hot) {
  module.hot.accept("./utils/utils.js", function () {
    console.log("Accepting the updated utils module!");
    // 在此处可以执行一些针对 utils.js 模块的特定操作
  });
}

HMR 加载样式 :借助 style-loader 后,使用模块热替换加载 CSS 实际更简单。此 loader 在幕后使用了 module.hot.accept,在 CSS 依赖模块更新之后,会将其修补到 <style>标签中。

定义全局变量

使用 webpack 内置插件 DefinePlugin 来定义全局变量。

// webpack.config.js
 {
  plugins: [
     ...[],
      // 设置全局变量
      new webpack.DefinePlugin({
        //字符串
        MODE: '"development"',
        // FLAG 是个布尔类型
        FLAG: "true",
      }),
    ].filter(Boolean),
 }
// main.js
console.log("globalConfig - DefinePlugin", {
  MODE, // ---->> 'development'
  FLAG, // ---->> true
});

利用 webpack 解决跨域问题

配置代理

//webpack.config.js
module.exports = {
  //...
  devServer: {
    proxy: {
      "/api": {
        target: "http://localhost:3000",
        pathRewrite: {
          "/api": "",
        },
      },
    },
  },
};

前端模拟简单数据

fetch("/api/user")
  .then((response) => response.json())
  .then((data) => console.log("get --> /api/user", data));
module.exports = {
  devServer: {
    setupMiddlewares: (middlewares, devServer) => {
      devServer.app.get("/api/user", function (req, res) {
        res.json({ name: "coder" });
      });
      return middlewares;
    },
  },
};

Code Split

打包代码时会将所有 js 文件打包到一个文件中,体积太大了。我们如果只要渲染首页,就应该只加载首页的 js 文件,其他文件不应该加载。

document.querySelector(".block").onclick = function () {
  import("./utils/index").then(({ sum }) => console.log(sum(1, 2)));

用户第二次进入网站的时候,希望只重新加载业务代码,而项目中所需要的依赖如果没有变更就走浏览器自身的缓存机制

[
  optimization: {
    // 代码分割配置
    splitChunks: {
      chunks: "all", // 对所有模块都进行分割
      // 以下是默认值
      // minSize: 20 * 1024, // 分割代码最小的大小 20kb
      // 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,
      //   },
      // },
    },
  },
]

预获取/预加载模块

在声明 import 时,使用下面这些内置指令,可以让 webpack 输出“resource hint”,来告知浏览器:

prefetch(预获取):告诉浏览器在空闲时才开始加载资源
preload(预加载):告诉浏览器立即加载资源

tips:都只会加载资源,并不执行,都有缓存。Preload 加载优先级高,Prefetch 加载优先级低。

import(/* webpackPrefetch: true */ "./path/to/LoginModal.js");

这会生成 并追加到页面头部,指示浏览器在闲置时间预取 login-modal-chunk.js 文件。

import(/* webpackPreload: true */ "ChartingLibrary");

Cache

将来开发时我们对静态资源会使用缓存来优化,这样浏览器第二次请求资源就能读取缓存了,速度很快。

但是这样的话就会有一个问题, 因为前后输出的文件名是一样的,都叫 main.js,一旦将来发布新版本,因为文件名没有变化导致浏览器会直接读取缓存,不会加载新资源,项目也就没法更新了。

所以我们从文件名入手,确保更新前后文件名不一样,这样就可以做缓存了。

tips: 字符占位符
contenthash:根据文件内容生成 hash 值,只有文件内容变化了,hash 值才会变化。所有文件 hash 值是独享且不同的

 {
 output: {
      path: path.resolve(__dirname, "./dist"), // 生产模式需要输出
      filename: "static/js/[name].[contenthash:8].js", // 入口文件打包输出资源命名方式
      chunkFilename: "static/js/[name].[contenthash:8].chunk.js", // 动态导入输出资源命名方式
      assetModuleFilename: "static/media/[name].[contenthash][ext]",
    },
 }

问题: 当我们修改 utils.js 文件再重新打包的时候,因为 contenthash utils.js 文件 hash 值发生了变化(这是正常的)。
但是 main.js 文件的 hash 值也发生了变化,原因是 main 里面会去 import(变化后的 contenthash.utils.js),所以我们不希望 main 的 contenthash 也发送变化 需要在中间设置一个映射关系,使 utils 变化时候只有 utils 里面的文件以及那个映射文件发送变化就行了

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

Loader 原理

loader 概念 loader link

帮助 webpack 将不同类型的文件转换为 webpack 可识别的模块

loader 执行顺序

loader 分类

  • pre: 前置 loader
  • normal: 普通 loader
  • inline: 内联 loader import Styles from 'style-loader!css-loader?modules!./styles.css';
  • post: 后置 loader

执行顺序

  • 4 类 loader 的执行优级为:pre > normal > inline > post 。
  • 相同优先级的 loader 执行顺序为:从右到左,从下到上。

例如:

// 此时loader执行顺序:loader3 - loader2 - loader1
module: {
  rules: [
    {
      test: /\.js$/,
      loader: "loader1",
    },
    {
      test: /\.js$/,
      loader: "loader2",
    },
    {
      test: /\.js$/,
      loader: "loader3",
    },
  ],
},
// 此时loader执行顺序:loader1 - loader2 - loader3
module: {
  rules: [
    {
      enforce: "pre",
      test: /\.js$/,
      loader: "loader1",
    },
    {
      // 没有enforce就是normal
      test: /\.js$/,
      loader: "loader2",
    },
    {
      enforce: "post",
      test: /\.js$/,
      loader: "loader3",
    },
  ],
},

loader 接受的参数

  • content 源文件的内容
  • map SourceMap 数据
  • meta 数据,可以是任何内容

loader 分类

  • 同步 loader
module.exports = function (content, map, meta) {
  return content;
};

this.callback 方法则更灵活,因为它允许传递多个参数,而不仅仅是 content。

module.exports = function (content, map, meta) {
  // 传递map,让source-map不中断
  // 传递meta,让下一个loader接收到其他参数
  this.callback(null, content, map, meta);
  return; // 当调用 callback() 函数时,总是返回 undefined
};
  • 异步 loader
module.exports = function (content, map, meta) {
  const callback = this.async();
  // 进行异步操作
  setTimeout(() => {
    callback(null, result, map, meta);
  }, 1000);
};
  • Raw Loader
module.exports = function (content) {
  // content是一个Buffer数据
  return content;
};
module.exports.raw = true; // 开启 Raw Loader

默认情况下,资源文件会被转化为 UTF-8 字符串,然后传给 loader。通过设置 raw 为 true,loader 可以接收原始的 Buffer。

  • Pitching Loader
module.exports = function (content) {
  return content;
};
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
  console.log("do somethings");
};

webpack 会先从左到右执行 loader 链中的每个 loader 上的 pitch 方法(如果有),然后再从右到左执行 loader 链中的每个 loader 上的普通 loader 方法。

4.png

在这个过程中如果任何 pitch 有返回值,则 loader 链被阻断。webpack 会跳过后面所有的的 pitch 和 loader,直接进入上一个 loader 。

5.png

loader 示例 clean-log-loader

// loaders/clean-log-loader.js
module.exports = function cleanLogLoader(content) {
  // 将console.log替换为空
  return content.replace(/console\.log\(.*\);?/g, "");
};

Plugin 原理

Plugin 的作用

通过插件我们可以扩展 webpack,加入自定义的构建行为,使 webpack 可以执行更广泛的任务,拥有更强的构建能力。

Plugin 工作原理

webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。 webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。

站在代码逻辑的角度就是:webpack 在编译代码过程中,会触发一系列钩子事件,插件所做的,就是找到相应的钩子,往上面注册事件,这样,当 webpack 构建的时候,插件注册的事件就会随着钩子的触发而执行了。

Webpack 钩子

  • tap:可以注册同步钩子和异步钩子。
  • tapAsync:回调方式注册异步钩子。
  • tapPromise:Promise 方式注册异步钩子

Plugin 构建对象

Compiler link

compiler 对象中保存着完整的 Webpack 环境配置,每次启动 webpack 构建时它都是一个独一无二,仅仅会创建一次的对象。

这个对象会在首次启动 Webpack 时创建,我们可以通过 compiler 对象上访问到 Webapck 的主环境配置,比如 loader 、 plugin 等等配置信息。

它有以下主要属性:

  • compiler.options 可以访问本次启动 webpack 时候所有的配置文件,包括但不限于 loaders 、 entry 、 output 、 plugin 等等完整配置信息。
  • compiler.inputFileSystem 和 compiler.outputFileSystem 可以进行文件操作,相当于 Nodejs 中 fs。
  • compiler.hooks 可以注册 tapable 的不同种类 Hook,从而可以在 compiler 生命周期中植入不同的逻辑。

Compilation link

compilation 对象代表一次资源的构建,compilation 实例能够访问所有的模块和它们的依赖。

一个 compilation 对象会对构建依赖图中所有模块,进行编译。 在编译阶段,模块会被加载(load)、封存(seal)、优化(optimize)、 分块(chunk)、哈希(hash)和重新创建(restore)。

它有以下主要属性:

  • compilation.modules 可以访问所有模块,打包的每一个文件都是一个模块。
  • compilation.chunks chunk 即是多个 modules 组成而来的一个代码块。入口文件引入的资源组成一个 chunk,通过代码分割的模块又是另外的 chunk。
  • compilation.assets 可以访问本次打包生成所有文件的结果。
  • compilation.hooks 可以注册 tapable 的不同种类 Hook,用于在 compilation 编译模块阶段进行逻辑添加以及修改。

生命周期简图

6.png

开发一个插件

注册 hook

class TestPlugin {
  constructor() {
    console.log("TestPlugin constructor()");
  }
  // 1. webpack读取配置时,new TestPlugin() ,会执行插件 constructor 方法
  // 2. webpack创建 compiler 对象
  // 3. 遍历所有插件,调用插件的 apply 方法
  apply(compiler) {
    console.log("TestPlugin apply()");

    // 从文档可知, compile hook 是 SyncHook, 也就是同步钩子, 只能用tap注册
    compiler.hooks.compile.tap("TestPlugin", (compilationParams) => {
      console.log("compiler.compile()");
    });

    // 从文档可知, make 是 AsyncParallelHook, 也就是`异步并行钩子`, 特点就是异步任务同时执行
    // 可以使用 tap、tapAsync、tapPromise 注册。
    // 如果使用tap注册的话,进行异步操作是不会等待异步操作执行完成的。
    compiler.hooks.make.tap("TestPlugin", (compilation) => {
      setTimeout(() => {
        console.log("compiler.make() 111");
      }, 2000);
    });

    // 使用tapAsync、tapPromise注册,进行异步操作会等异步操作做完再继续往下执行
    compiler.hooks.make.tapAsync("TestPlugin", (compilation, callback) => {
      setTimeout(() => {
        console.log("compiler.make() 222");
        // 必须调用
        callback();
      }, 1000);
    });

    compiler.hooks.make.tapPromise("TestPlugin", (compilation) => {
      console.log("compiler.make() 333");
      // 必须返回promise
      return new Promise((resolve) => {
        resolve();
      });
    });

    // 从文档可知, emit 是 AsyncSeriesHook, 也就是`异步串行钩子`,特点就是异步任务顺序执行
    compiler.hooks.emit.tapAsync("TestPlugin", (compilation, callback) => {
      setTimeout(() => {
        console.log("compiler.emit() 111");
        callback();
      }, 3000);
    });

    compiler.hooks.emit.tapAsync("TestPlugin", (compilation, callback) => {
      setTimeout(() => {
        console.log("compiler.emit() 222");
        callback();
      }, 2000);
    });

    compiler.hooks.emit.tapAsync("TestPlugin", (compilation, callback) => {
      setTimeout(() => {
        console.log("compiler.emit() 333");
        callback();
      }, 1000);
    });
  }
}
// TestPlugin constructor()
// TestPlugin apply()
// compiler.compile()

// compiler.make() 333
// compiler.make() 222
// compiler.make() 111

// compiler.emit() 111
// compiler.emit() 222
// compiler.emit() 333
module.exports = TestPlugin;

启动调试

通过调试查看 compiler 和 compilation 对象数据情况。

  1. package.json 配置指令
 "scripts": {
    "debug": "node --inspect-brk ./node_modules/webpack-cli/bin/cli.js"
  },
  1. 运行指令
npm run debug
  1. 打开 Chrome 浏览器,F12 打开浏览器调试控制台。 此时控制台会显示一个绿色的图标

8.png

  1. 点击绿色的图标进入调试模式

  2. 在需要调试代码处用 debugger 打断点,代码就会停止运行,从而调试查看数据情况

8.png

BannerWebpackPlugin

  1. 作用:给打包输出文件添加注释。
  2. 开发思路:

需要打包输出前添加注释:需要使用 compiler.hooks.emit 钩子, 它是打包输出前触发。
如何获取打包输出的资源?compilation.assets 可以获取所有即将输出的资源文件。

// plugins/banner-webpack-plugin.js
class BannerWebpackPlugin {
  constructor(options = {}) {
    this.options = options;
  }

  apply(compiler) {
    // 需要处理文件
    const extensions = ["js", "css"];

    // emit是异步串行钩子
    compiler.hooks.emit.tapAsync(
      "BannerWebpackPlugin",
      (compilation, callback) => {
        // compilation.assets包含所有即将输出的资源
        // 通过过滤只保留需要处理的文件
        const assetPaths = Object.keys(compilation.assets).filter((path) => {
          const splitted = path.split(".");
          return extensions.includes(splitted[splitted.length - 1]);
        });

        assetPaths.forEach((assetPath) => {
          const asset = compilation.assets[assetPath];

          const source = `/*
* Author: ${this.options.author}
*/\n${asset.source()}`;
          // 覆盖资源
          compilation.assets[assetPath] = {
            // 资源内容
            source() {
              return source;
            },
            // 资源大小
            size() {
              return source.length;
            },
          };
        });

        callback();
      }
    );
  }
}

module.exports = BannerWebpackPlugin;