章节简介

上一章基于 Webpack4 介绍了基本配置、常用 Loader 与 Plugin 和在前端工程化中如何使用,本章将进一步深入了解 Webpack 并学习其常用的进阶配置,主要分为以下几个部分:

  1. Webpack 基础原理
  2. Webpack 插件机制原理,如何编写一个 Plugin
  3. Webpack Loader 机制,如何编写一个 Loader
  4. Webpack4 进阶配置

首先基础原理部分将从这类构建工具的基本需求出发讲解 Webpack 基础原理和运行流程,接下来我们一起深入 Webpack 两个重要的部分 —— 插件机制与 Loader 机制,了解 Webpack 的生命周期流程是如何建立在插件机制上和 Loader 的模块翻译器工作是如何实现的,并学习编写 Plugin 和 Loader,最后将以几个实际应用场景中常见的方案为例,讲解基于 Webpack4 的进阶配置。

Webpack 基础原理

经过阅读上一个章节,相信大家已经掌握了 Webpack 的基本配置,可以独立搭建出一套前端项目构建体系,在本小节我们首先回顾一下 Webpack 这类前端构建工具的存在究竟是为了解决什么问题呢?了解它的目标,会更有利于理解它的运行流程,总的来说构建工具一般需要实现以下几个小目标:

  • 依赖管理,能够识别依赖并且梳理依赖关系

  • 资源加载管理,能根据依赖关系和输入配置,处理文件的加载顺序和优化文件加载数量(合并、拆分等)

  • 效率与优化,能提升开发效率、优化结果以达到最终页面加载速度提升的效果

那么 Webpack 是如何实现这几个小目标的呢?Webpack 的运行过程可以简单概括为以下几个步骤:

配置解析 -> 内置插件&配置插件注册 -> 确认入口获取依赖资源 -> 使用Loader翻译资源 -> 识别资源加载语句并递归的遍历所有资源 -> 封装依赖资源输出结果

基本概念

首先我们简单的解释说明 Webpack 官方文档中经常出现的几个概念 —— Module 、Chunk、Bundle :

module(模块)

从编程角度,模块化编程是一种将程序划分为独立的功能块,组合使用的编程模式,通常被应用于 Javascirpt 代码中(比如 Node.js 的模块化系统)。
Webpack 扩充了这个概念:在 Webpack 的世界中,万物皆 module ,它将 module 的概念应用于任何输入资源:Javscript 文件、CSS 文件、图片资源文件、字体资源等,识别 module 的关键语句包括但不限于:

  • ES2015 import 语句,如 import ('foo') CommanJS require() 语句, 如 require('bar') css/sass/less文件中的 @import 语句,如 @import('common') 样式表的 url(‘…’) 语句或 html 深入了解 Webpack - 图1 中包含的图片地址url

chunk(块)

chunk 是 Webpack 的一个特定术语,我们可以将 module 理解为输入资源, chunk 就是在编译构建过程中对输入 module 和其依赖的管理者,它管理依赖图谱中的 module 如何封装成文件并输出。

bundle(包)

bundle 可以对应理解为 Webpack 流程输出的最终结果文件——输入资源经过配置解析、编译构建过程后最终输出js文件,提供给浏览器进行加载。bundle 由 chunk 组成,通常情况下它们之间是一一对应的关系,但是有一些配置会产生不同的情况。

在阅读 Webpack 文档时,可能会对 chunk 和 bundle 之间的关系感到迷惑,尽管术语解释文档对两个名词概念都做了解释,甚至有几个 issue 里讨论它们的区别。

运行流程

配置解析

Webpack 命令运行后首先读取配置文件(或命令行参数)并进行解析和默认配置合并,初始化本次构建流程的配置参数,并注册内置插件和配置文件中注册的插件。

编译&构建

入口(entry point)是 Webpack 开启构建旅程的起点,通常在配置文件 entry 属性中进行定义,入口可以指定一个或多个,当指定多个时,将生成互相独立的依赖图谱。Webpack 从入口指定的文件开始读取,通过识别关键语句分析出需要加载的模块资源,使用配置的对应 Loader 翻译模块文件内容,接着找到每个已加载模块依赖的模块,再递归地进行读取、翻译处理。

这个过程可以获取从入口模块开始直接或间接依赖的所有模块,和这些模块之间的依赖关系。

模块封装&输出

上一步编译&构建流程,所有依赖的模块都被读取和翻译,并且它们之间的依赖关系也被保存,接着 Webpack 将开始封装模块过程:

  • 在未配置代码分割(splitChunk)且没有依赖异步加载模块的情况下,每个入口(entry point)将会生成一个独立 bundle —— 所有记录在该入口依赖里的模块将封装到一个 bundle 文件中;

  • 在配置了代码分割策略或存在异步模块的情况下 ,根据配置策略可能会拆分多个chunk块,最终输出多个结果文件。

小结

Webpack 插件机制

简介

PlugIn (插件) 系统是 Webpack 世界中最重要又最难以理解的概念,理解 Plugin 原理将有助于更灵活地使用 Webpack:

webpack 插件机制是整个 webpack 工具的骨架,而 webpack 本身也是利用这套插件机制构建出来的

首先我们思考一下 Plugin 在 Webpack中的作用,上一章中学习了几款常用 Plugin 的使用,它们的功能都各不相同且看起来也并没有联系,也就是说 Plugin 在 Webpack 中并没有局限的一种作用 —— 它可以做任何 Loader 做不了的事情。

原理解析

基础 —— Tapable

在运行流程一章我们了解到 Webpack 已经定义好了所有的运行流程——默认情况下这个流程下的各个子任务不能由用户进行拼接或者插入自定义任务,大部分时候用户都是在和配置打交道,而插件 Plugin 就是介入 Webpack 运行流程黑盒的媒介:开发者可以通过编写 Plugin 在 Webpack 对外暴露的事件钩子中,获取当前编译信息或完成一些操作以达到扩展 Webpack 功能的目的,所有的事件钩子都基于 Webpack 的运行流程生命周期。

它的核心框架是 Webpack 的内部库 Tapable ,Tapable 提供了基于发布订阅模式(观察者模式或事件流)的架构,源码细节这里不会展开,但我们需要理解它提供了什么功能:

  • 注册事件监听,类似于使用 on 注册监听事件,Tapable 中使用 tap 注册,每个事件点支持插入监听回调;
  • 触发指定事件,类似于使用 emit 触发事件,Tapable 中使用 call 触发;
  • 支持多种事件类型,大类上按同步、异步串行、异步并行划分,每个大类下根据回调事件执行方式还有进一步细分。
  • 支持多种监听事件处理逻辑分类:
    • 普通模式,事件点上注册的所有监听回调按注册顺序根据事件类型依次调用,相互独立;
    • 瀑布模式,上一个监听回调执行完成后的返回值将注入下一个监听回调;
    • 熔断模式,监听回调返回非 null 值将中断剩余回调的调用。

注意:上文提到的 tap 和 call 只是举例,实际不止这两个api

Webpack 中负责编译流程的很多对象都继承自或混入了 Tapable ,也就是说它们借助 Tapable ,拥有了获取自定义事件订阅、触发自定义事件的能力。

核心实例

我们再理一理插件系统的流程:

  • Webpack 的配置文件中所有依赖的插件通过 new XXXPlugin() 的方式填写在 plugin 配置项下,这些 Plugin 中注册了特定事件并提供了回调。
  • 在 Webpack 初始化配置阶段将遍历 plugin 配置项并将每个 Plugin 都注册
  • 接下来在Webpack 主流程运行时,每个关键生命周期点通过 call 方式触发特定事件,注册了特定事件的 Plugin 回调被调用,回调方法中被注入编译对象,可以获取到特定事件触发时编译对象的状态(即当前编译信息)并完成一些操作达到扩展目的。

实际上不仅仅是用户配置的 Plugin,在 Webpack 源码中很多的流程操作也是基于 Plugin 的方式实现的,所以可以说 Webpack 就是一个插件合集。

上文我们了解了 Webpack 的 Plugin 系统是基于事件发布订阅模式搭建的,如果要编写一个插件,还必须要了解: Webpack 到底对外暴露了哪些事件钩子?是什么类型的事件钩子?它们在编译流程的哪一个生命周期节点?会提供怎样的信息?

Webpack 运行流程这一节粗略介绍了整体运行流程,而这一节我们将深入到流程的具体实现中—— Webpack 的两个核心对象 Compiler 与 Compilation。

Compiler & Compilation

TODO 重写,介绍 Compiler | Compilation | Resolver | Module Factory | Parser | Templates

Compiler对象

Compiler 是 Webpack 的编译器对象,它主要负责主流程运作,在 Webpack 启动时通过 new 实例化,接收所有配置文件中的配置项(entry,output,module,plugin等),并实例化 Compilation 对象开启编译流程,它的生命周期就是 Webpack 整个运行时期。Compiler 并不会涉及某个具体文件的编译细节,因此它对外暴露的事件钩子粒度比较粗。

Compilation对象

Compilation 对象同样继承自 Tapable , 它负责每一次版本的编译构建和资源生成流程中的细节,在 Compiler 对象的生命周期内(即 Webpack 运行时)可能有多次编译流程,比如常用的开发环境下,文件内容变更会引起重新编译。它被 Compiler 对象实例化,通过Compilation实例可以访问到依赖图谱中的所有模块和他们的依赖:

在编译过程中,模块将被 加载、封装、优化、分开、哈希和存储。

完整的事件钩子说明参考官网文档,如果需要对编译的结果要做细粒度的处理的时候,就得使用 Compilation 对象上定义的事件钩子了。

Resolver & Module Factory

Parser & Templates

常用事件钩子(Hooks)

Webpack4优化事件钩子相关代码编写,继承自tapable的class入口注册了所有hook…

完整的事件钩子使用说明可以参考官网文档,如果想要进一步了解具体某个钩子在什么时候触发,可以在 Webpack 源码中搜索 hooks.<hook name>.call

TODO: 事件钩子类型说明 & 几个常用关键流程事件钩子简单说明

如何编写Plugin

Plugin 基础

经过前文的介绍,我们对插件机制有了基本认识,而实际上完成一个类似 html-webpack-plugin 插件的开发需要对 Webpack 源码细节有深入的了解,这一小节我们只介绍基本编写 Plugin 需要遵循的规则和一些简单的例子,希望对今后面对实际开发插件需求时有一定的帮助。

注意:Webpack4 之后插件开发API有较大变更,本节例子都依据新版本API编写

参考官网教程 Writing a Plugin ,一个插件由几部分组成

  1. 一个 JavaScript 类函数 (即可以通过 new 调用)
  2. 在函数原型(prototype)中定义一个 apply 方法,该方法调用时将会注入 compiler 实例。
  3. apply 方法中注册要监听的事件钩子名称、插件名称、回调函数。
  4. 在回调函数中通过注入的参数,读取或操纵修改 Webpack 内部实例数据。
  5. 针对异步类型的事件钩子:插件流程完成后调用 callback 方法;或返回一个 Promise 实例。

如果我们需要注册 compiler 的事件钩子,可以在第3点提到的 apply 方法中使用 compiler 实例注册;而如果需要注册 compilation 的事件钩子, 需要先使用 compiler 实例注册一个会注入 compilation 实例的事件钩子,并在该事件钩子回调中使用注入的 compilation 实例注册 compilation 暴露的事件钩子:

  1. // 一个标准JavaScript类函数
  2. class MyPluginDemo {
  3. // apply方法,第一个参数为注入的compiler实例
  4. apply(compiler) {
  5. // 注册compiler的compilation钩子,这是一个同步钩子,回调中会注入compilation实例
  6. compiler.hooks.compilation.tap('MyWebpackPluginDemo', compilation => {
  7. // 注册compilation的optimizeChunkAssets钩子,这是一个异步钩子
  8. compilation.hooks.optimizeChunkAssets.tapAsync(
  9. 'MyWebpackPluginDemo',
  10. (chunks, callback) => {
  11. // 遍历所有chunk文件输出其内容,仅为举例,没有实际意义
  12. chunks.forEach(chunk => {
  13. chunk.files.forEach(file => {
  14. console.log(compilation.assets[file].source())
  15. });
  16. });
  17. // 异步钩子操作完成后,调用callback方法
  18. callback();
  19. }
  20. );
  21. });
  22. }
  23. }

Tap API

Tap API 是挂载回调到事件钩子上必须要掌握的API,它基本使用模式如下

<instance>.hooks.<hook name>.<tap API>('<plugin name>', callback )

  • instance 即为 compilercompilation 对象的实例引用
  • hook name 是需要挂载钩子的名称,通过查阅 官网文档 获取所有对外暴露的事件钩子名称、触发时机、类型、注入参数
  • tap API 有三种:
    • tap 用于挂载一个同步回调,适合任何事件钩子类型
    • tapAsync 用于挂载一个异步回调,不能对同步类型钩子使用,回调中会注入 callback 供插件处理完操作后调用,如果不调用 callback 流程将无法继续进行
    • tapPromisetapAsync 的作用和限制类似,不同在于要求返回一个 Promise 实例,并且这个 Promise 一定会被决议(无论 resolve 或 reject )

进阶用法

本小节将通过几个简单的例子说明插件系统的强大之处,但不会涉及编译的细节和复杂的流程处理 —— 读者不需要了解比如 compilation 对象的组成细节或如何操作它。希望能通过这些简单的例子扩展使用插件的思路,当遇到实际类似场景时,能提供一些帮助。

使用 Loader 机制

TODO 补充例子

扩展插件自定义事件

在插件代码中,我们有途径能访问到 compiler实例和 compilation 实例,并且已知这两个对象继承自 Tapable —— 那么我们也就可以在插件代码中通过这两个实例去增加新的自定义事件钩子,并在合适时机去触发它们!

html-webpack-plugin 就是这么做的,从它的官方文档可以看到为了提供给其他插件配合修改HTML文件的能力,它对外提供了几种 Tapable Hook 。自定义事件钩子的使用和 Webpack 本身提供的事件钩子并没有什么不同,只要插件配置在 html-webpack-plugin 插件之后就都可以使用该插件扩展的自定义事件,下边通过一个简单的例子说明:

  1. // Foo插件
  2. class Foo {
  3. apply(compiler) {
  4. // 普通的同步钩子
  5. const SyncHook = require('tapable').SyncHook;
  6. // 在compiler.hooks上创建自定义钩子
  7. // 传入的是一个参数名数组,名字只是为了可读性
  8. // 重点需要注意数组长度:规定了触发时可以注入几个参数
  9. compiler.hooks.Plugin1Hook = new SyncHook(['paramName']);
  10. compiler.hooks.done.tap('Foo', stats => {
  11. console.log('Compilation has completed.');
  12. // 在编译完成后触发,注入一个字符串参数
  13. compiler.hooks.Plugin1Hook.call('done!');
  14. })
  15. }
  16. }
  17. // Bar插件
  18. class Bar {
  19. apply(compiler) {
  20. // 监听Foo插件的自定义事件
  21. compiler.hooks.Plugin1Hook.tap('Bar', params => {
  22. // 简单地输出注入的参数
  23. console.log(params);
  24. })
  25. }
  26. }

使用时需要注意 Bar 插件必须在 Foo 插件之后插入 plugins 配置数组:

  1. // 注意顺序
  2. {
  3. ... ...
  4. plugins: [new Foo(), new Bar()]
  5. ... ...
  6. }

小结

Webpack Loader 机制

Loader 综述

职责与特点

Loader 是 Webpack 中的一名重要成员,它是 Webpack 可以识别除了 JavaScript 之外各类资源的关键:如使用 TypeScript , 又如在 JavaScript 文件中引入 css\scss\less 文件,我们可以把它理解为模块的翻译器或预处理器。在上一章中,我们学习了如何在 module.rules 中配置几款必备的 Loader,本小节将深入了解 Loader 的特性,并且尝试学习动手开发 Loader。

Loader 的本质是导出一个函数的 Node 模块,它所导出的函数将在读取到目标类型(通常在 module.rules 中配置)源文件时调用,源文件字符串将作为参数传入这个函数,并且函数中可以通过 this 关键字访问到上下文信息和使用 Loader API 中指定的属性和方法,最终同步异步地返回处理后的结果:

  • 类似于函数的链式调用,Loader 也支持链式调用:每一个 Loader 将接收上一个返回的结果,最后一个调用的 Loader 需要返回一个 JavaScirpt 模块。以下边这段配置为例,链式调用的顺序和配置的顺序相反,less-loader 将会被第一个调用,最后是 style-loader


  1. ... ...
  2. {
  3. test: /\.less$/,
  4. use: [
  5. {
  6. loader: 'style-loader'
  7. },
  8. {
  9. loader: 'css-loader'
  10. },
  11. {
  12. loader: 'postcss-loader'
  13. },
  14. {
  15. loader: 'less-loader'
  16. }
  17. ]
  18. }
  19. ... ...
  • Loader的结果返回支持同步或异步的方式。
  • Loader 能接收 options 对象作为配置(不推荐 query 参数的配置的方式)
  • 由于 Loader 运行在 Node 环境中,所以它能做的事情不仅仅限于翻译资源文件而已,你可以根据业务的需求发挥自己的想象力做到更多的事情:如 url-loader 能将文件资源导出到 output 指定的文件目录下,eslint-loader 能对代码进行格式检查等。
  • Loader 能配合 Plugin 使用发挥更大的作用。

调用过程

在 Loader 函数被调用之前,Webpack会执行一系列前序操作,这里仅简单列出和 Loader 相关的流程:

Loader配置解析 => 解析 Loader 和资源文件的路径(resolve)=> 生成 RuleSet 规则集(匹配模块使用的 Loader )

接下来负责调用 Loader 链并返回结果的是一个独立的库 loader-runner,Webpack中利用了它提供的 runLoaders 方法运行 Loader 和 getContext 方法生成 Loader ContextrunLoaders 方法主要完成以下工作:

  • 根据解析的 Loader 文件路径,加载 Loader 模块,兼容 commonJS、ESModule 或 SystemJS 方式
  • 按 Loader 链数组控制 Loader 的调用过程,具体调用顺序在 Pitching Loader 小节中进一步说明,简单来说包括三个部分:pitch阶段 => 处理资源内容阶段 => Loader 函数调用阶段(同步或异步)
  • 持续更新 Loader Context 信息
  • 获取执行后的结果并返回 Webpack 编译流程

如何编写 Loader

在正式开始编写 Loader 之前,需要先了解开发 Loader 必备的“正确姿势” —— 具体可以参考官网的 Guidelines ,我们就不做官网的搬运工了,只强调比较重要的两点:

  • 无状态:Loader 中不应该保留或依赖状态,Loader 运行时和其他模块、其他 Loader 之间应该保持相对独立

  • 绝对路径:Loader 中不能出现绝对路径,loader-utils 里有一个 stringifyRequest 方法,它可以把绝对路径转化为相对路径

本地调试

首先为了方便调试,在 Webpack 配置中添加 resolveLoader 配置,给本地 loader 添加一个别名:

  1. ... ...
  2. resolveLoader: {
  3. // 给本地 loader 添加别名
  4. alias: {
  5. "loader-demo": require.resolve("./loaderDemo")
  6. }
  7. }
  8. module: {
  9. rules: [
  10. // loader 可以直接指定为上边配置的别名
  11. { test: /\.js$/, use: { loader: "loader-demo" } }
  12. ]
  13. }
  14. ... ...

Loader 基础

注意:本小节例子使用到的 API 均基于 WebPack@4.30.0版本

正如上文介绍,Loader 本质是默认导出函数的 NodeJs 模块,被调用时将会注入源文件字符串作为第一个参数,通过 this 关键字可以访问到当前构建上下文信息和使用 Loader API 中指定的属性和方法,一个最简单的 Loader 结构如下:

  1. function loaderDemo(content) {
  2. // 可以查看 this 提供的上下文信息和可以使用的API
  3. console.log(this);
  4. // 该函数需要返回处理后的结果,直接返回输入source,相当于未对资源做任何处理
  5. return content;
  6. }
  7. module.exports = loaderDemo;

Loader 运行在 NodeJs 环境中,可以调用任何 NodeAPI 或者借助第三方库对源文件内容进行处理,它的一个重要作用就是作为资源的翻译器,比如我们可能会需要在 JavaScript 文件中引入 .txt 资源,将其内容转化为字符串,那么可以写一个 txt-loader 完成这项工作:

  1. function txtLoader(content) {
  2. // 在本例中,需要将非 JS 资源转为模块返回
  3. return `module.export="${content}"`;
  4. }
  5. module.exports = txtLoader;

Loader 是可以链式调用的,链式调用场景下,根据位置不同 Loader 的返回值可以分为以下两种:

  • 最终 Loader:可以处于链式调用最后一环的 Loader,返回必须是标准 JS 模块字符串代码
  • 非最终 Loader:产生内容仅是对资源的中间处理结果,后续必须有 Loader 对其进行进一步处理

Loader 进阶

获取配置

module.rule 配置中可以给 Loader 提供用户配置,使用 loader-utils 提供的 getOptions API 获取,如:

  1. const loaderUtils = require('loader-utils');
  2. module.exports = function(content) {
  3. // 获取到用户给当前 Loader 传入的配置
  4. const options = loaderUtils.getOptions(this);
  5. return content;
  6. };

二进制格式数据

默认情况下 Loader 函数中获取到的源码是 UTF-8 编码的字符串,在某些场景下需要获取到二进制格式的数据,比如 file-loader ,这时需要配置 raw 标记为 true,如:

  1. module.exports = function(content) {
  2. content instanceof Buffer === true;
  3. return content;
  4. };
  5. // 通过 exports.raw = true 表示该 Loader 需要二进制格式数据
  6. module.exports.raw = true;

返回其他结果

上边的例子中,Loader 通过 return 语句返回翻译后的内容,内容可以是 String 或 Buffer 格式,但如果需要返回其他额外信息, return 就不够用了,需要使用 this.callback 这个 API ,它有四个主要参数:

  1. this.callback(
  2. err: Error | null, // Loader处理过程中出错时,需要抛出一个Error,无错误时需要指定为 null
  3. content: string | Buffer, // 转换后的内容,可以是 String 或 Buffer 格式
  4. sourceMap?: SourceMap, // 可选:可被 Webpack 解析的 SourceMap
  5. meta?: any, // 可选:用户自定义的其他数据,会被 Webpack 忽略
  6. ... // 可选:任意个数其他参数
  7. );

实际上 this.callback 调用时除了第一个参数之外的所有其他参数,会按顺序作为入参传递给 Loader 函数:

  1. module.exports = function(content, sourceMap, meta, ... ) {
  2. // 可以通过 meta 在 Loader 之间传递自定义数据,比如 Loader 间可共享的 AST(abstract syntax tree)
  3. if(meta){
  4. ... ...
  5. }
  6. };

使用 this.callback ,则函数的 return 必须返回 undefined ,Webpack 根据 return 内容来确认从哪里获取 Loader 翻译的结果:

  1. module.exports = function(content, sourceMap, meta, ... ) {
  2. this.callback(null, content);
  3. // 必须返回 undefined, 告诉 Webpack 解析结果在 this.callback 中
  4. return;
  5. };

异步 Loader

Loader 函数除了同步返回( returnthis.callback )解析结果,还可以异步执行再返回结果,比如有些场景下需要异步请求网络或长时间的计算操作,此时同步返回会阻塞整个构建流程,建议借助 this.async API等待异步操作执行完成后再返回转化结果:

  1. module.exports = function(content) {
  2. // 通过 this.async API 获取异步返回的回调函数
  3. const callback = this.async();
  4. // 使用定时器模拟异步操作
  5. setTimeout(()=>{
  6. // 通过 callback 返回异步执行后的结果
  7. callback(err, content, sourceMap, meta);
  8. }, 2000);
  9. };

Pitching Loader

Pitching Load 是 Webpack Loader 中一个相对复杂的概念,为了理解这个概念我们先参考官网 对它的机制做解释 ,接着再以实际场景中常用的 style-loader 中如何使用这个机制做进一步讲解,便于大家理解在它的实际意义。

根据官网, Loader 可以导出 pitch 函数,在上文提到的 loader-runner 负责控制 Loader 链的调用过程, pitch 函数的调用也在执行流程中,比如这样一份链式调用的 Loader 的配置:

  1. ... ...
  2. module: {
  3. rules: [
  4. {
  5. //...
  6. use: [
  7. 'a-loader',
  8. 'b-loader',
  9. 'c-loader'
  10. ]
  11. }
  12. ]
  13. }
  14. ... ...

实际的调用执行顺序如下:

  1. |- a-loader `pitch` 调用
  2. |- b-loader `pitch` 调用
  3. |- c-loader `pitch` 调用
  4. |- 请求模块资源文件被读取,并添加到依赖中
  5. |- c-loader 调用
  6. |- b-loader 调用
  7. |- a-loader 调用

pitch 函数中的入参包括:

  • remainingRequest
  • precedingRequest
  • data
  1. module.exports.pitch = function(remainingRequest, precedingRequest, data) {
  2. // ... ...
  3. };

其中 data 对象可以在 Loader 函数中通过 this.data 访问,故可以在整个执行流程中传递数据。

另外如果在 pitch 函数中返回值,会跳过余下所有 Loader 的 pitch 函数的调用和 Loader 正常调用流程,仅执行已经历 pitch 函数调用流程的 Loader 的正常调用流程。实际上跳过的还有当前模块资源添加依赖和资源文件读取步骤 —— pitch 函数的返回会被当做资源内容传递给 Webpack 作为接下来的 Loader 函数调用流程的输入参数,在上边的例子中,如果在 b-loader 的 pitch 中返回值,类似:

  1. module.exports.pitch = function(remainingRequest, precedingRequest, data) {
  2. if (someCondition()) {
  3. return 'module.exports = require(' + JSON.stringify('-!' + remainingRequest) + ');';
  4. }
  5. };

原执行顺序将会变为:

  1. |- a-loader `pitch`
  2. |- b-loader `pitch` 返回值
  3. |- a-loader 调用,且 content 参数为 b-loader `pitch` 的返回值

可以看到 Pitching Loader 的机制能实现 Loader 链式调用的提前返回 —— 在某些条件下少执行几个 Loader。在实际场景中,常用的 style-loader 就利用了 Pitching Loader 机制,如果查看它的源码,可以发现它的结构大概如下:

  1. module.exports = () => {};
  2. module.exports.pitch = function loader(request) {
  3. // ... ...
  4. return [
  5. // 一些数组元素...
  6. ].join('\n')
  7. }

它是一个标准的 Pitching Loader,在 pitch 函数中返回结果。一般情况下我们会链式调用 style-loader 与 css-loader,比如以下配置:

  1. module:{
  2. rules:[
  3. {
  4. test: /\.css$/,
  5. use: [
  6. {
  7. loader: "style-loader"
  8. },
  9. {
  10. loader: "css-loader"
  11. }
  12. ]
  13. }
  14. ]
  15. }

style-loader 作为最终 Loader ,但实际上 css-loader 也是可以作为最终 Loader 使用的 —— 它返回标准的 JS 模块字符串如 "module.export=xxx"

如果 style-loader 按普通写法,在 Loader 函数中只能获取到 css-loader 返回的模块字符串,想提取其中纯 CSS 内容是比较困难的,所以 style-loader 利用 pitch 函数提前返回,且在 pitch 函数中通过第一个参数 remainingRequest 获取到剩余的所有 Loader Request,拼装为类似 require(css-loader!source) 的形式加入到返回的 JS 模块字符串,如:

  1. "
  2. // 示例中 css 资源文件名称为 test.css
  3. var content = require("!!../node_modules/css-loader/dist/cjs.js!./test.css");
  4. // 略
  5. ... ...
  6. }"

Loader Context 与其他工具库

上文提到 Loader 函数中的 this 关键字提供了很多有用的上下文信息和 API, thisLoader Context 的实例,它提供的比较常用的API还有以下这些:

  • this.cacheable :默认情况下 Loader 的处理结果都是可缓存的 —— 当前处理资源文件和其所依赖的文件未发生变化时,Loader 不会重新调用。通过在 Loader 函数内部 cacheable(false) 可以手动关闭缓存功能

  • this.addDependency & this.addContextDependency :给当前处理模块资源添加其依赖的文件/目录,在其依赖的文件/目录内文件发生变化时,会重新调用 Loader

  • this.emitFile :输出一个文件,使用方式为 emitFile(name: string, content: Buffer|string, sourceMap: {...}) ,常用的 file-loader 就利用了这个 API 实现图片类型资源文件的导出。

更多未提及的内容请参考官网

在获取配置小节的例子中有这么一句 require('loader-utils') ,引入的 loader-util 是 Webpack 官方提供的 Loader 工具集库,它提供了一些方便开发者使用的 API ,其中最常用的就是获取配置 getOptions ,编写自定义 Loader 时建议看一看它的 API 说明 ,另外 schema-util 可以配合它使用,用于验证配置的格式合法性。

Webpack4 进阶配置

TreeShaking

优化代码压缩效率

代码分割

可预测的持久化缓存

速度提升

总结