Webpack - 手把手教你写一个 loader / plugin

时间:2022-04-08 02:48:55

Webpack - 手把手教你写一个 loader / plugin

一、Loader

 

1.1 loader 干啥的?

webpack 只能理解 JavaScript 和 JSON 文件,这是 webpack 开箱可用的自带能力。**loader **让 webpack 能够去处理其他类型的文件,并将它们转换为有效模块,以供应用程序使用,以及被添加到依赖图中。

也就是说,webpack 把任何文件都看做模块,loader 能 import 任何类型的模块,但是 webpack 原生不支持譬如 css 文件等的解析,这时候就需要用到我们的 loader 机制了。 我们的 loader 主要通过两个属性来让我们的 webpack 进行联动识别:

  1. test 属性,识别出哪些文件会被转换。
  2. use 属性,定义出在进行转换时,应该使用哪个 loader。

那么问题来了,大家一定想知道自己要定制一个 loader 的话需要怎么做呢?

1.2 开发准则

俗话说的好,没有规矩不成方圆,编写我们的 loader 时,官方也给了我们一套用法准则(Guidelines),在编写的时候应该按照这套准则来使我们的 loader 标准化:

  • 简单易用。
  • 使用链式传递。(由于 loader 是可以被链式调用的,所以请保证每一个 loader 的单一职责)
  • 模块化的输出。
  • 确保无状态。(不要让 loader 的转化中保留之前的状态,每次运行都应该独立于其他编译模块以及相同模块之前的编译结果)
  • 充分使用官方提供的 loader utilities。
  • 记录 loader 的依赖。
  • 解析模块依赖关系。

根据模块类型,可能会有不同的模式指定依赖关系。例如在 CSS 中,使用@import 和 url(...)语句来声明依赖。这些依赖关系应该由模块系统解析。 可以通过以下两种方式中的一种来实现:

  • 通过把它们转化成 require 语句。
  • 使用 this.resolve 函数解析路径。
  • 提取通用代码。
  • 避免绝对路径。
  • 使用 peer dependencies。如果你的 loader 简单包裹另外一个包,你应该把这个包作为一个 peerDependency 引入。

1.3 上手

一个 loader 就是一个 nodejs 模块,他导出的是一个函数,这个函数只有一个入参,这个参数就是一个包含资源文件内容的字符串,而函数的返回值就是处理后的内容。也就是说,一个最简单的 loader 长这样:

  1. module.exports = function (content) { 
  2.  // content 就是传入的源内容字符串 
  3.   return content 

当一个 loader 被使用的时候,他只可以接收一个入参,这个参数是一个包含包含资源文件内容的字符串。 是的,到这里为止,一个最简单 loader 就已经完成了!接下来我们来看看怎么给他加上丰富的功能。

1.4 四种 loader

我们基本可以把常见的 loader 分为四种:

  • 同步 loader
  • 异步 loader
  • "Raw" Loader
  • Pitching loader

① 同步 loader 与 异步 loader

一般的 loader 转换都是同步的,我们可以采用上面说的直接 return 结果的方式,返回我们的处理结果:

  1. module.exports = function (content) { 
  2.  // 对 content 进行一些处理 
  3.   const res = dosth(content) 
  4.   return res 

也可以直接使用 this.callback() 这个 api,然后在最后直接 **return undefined **的方式告诉 webpack 去 this.callback() 寻找他要的结果,这个 api 接受这些参数:

  1. this.callback( 
  2.   err: Error | null, // 一个无法正常编译时的 Error 或者 直接给个 null 
  3.   content: string | Buffer,// 我们处理后返回的内容 可以是 string 或者 Buffer() 
  4.   sourceMap?: SourceMap, // 可选 可以是一个被正常解析的 source map 
  5.   meta?: any // 可选 可以是任何东西,比如一个公用的 AST 语法树 
  6. ); 

接下来举个例子:

Webpack - 手把手教你写一个 loader / plugin

这里注意[this.getOptions()](https://webpack.docschina.org/api/loaders/#thisgetoptionsschema) 可以用来获取配置的参数

从 webpack 5 开始,this.getOptions 可以获取到 loader 上下文对象。它用来替代来自loader-utils中的 getOptions 方法。

  1. module.exports = function (content) { 
  2.   // 获取到用户传给当前 loader 的参数 
  3.   const options = this.getOptions() 
  4.   const res = someSyncOperation(content, options) 
  5.   this.callback(null, res, sourceMaps); 
  6.   // 注意这里由于使用了 this.callback 直接 return 就行 
  7.   return 

这样一个同步的 loader 就完成了!

再来说说异步: 同步与异步的区别很好理解,一般我们的转换流程都是同步的,但是当我们遇到譬如需要网络请求等场景,那么为了避免阻塞构建步骤,我们会采取异步构建的方式,对于异步 loader 我们主要需要使用 this.async() 来告知 webpack 这次构建操作是异步的,不多废话,看代码就懂了:

  1. module.exports = function (content) { 
  2.   var callback = this.async() 
  3.   someAsyncOperation(content, function (err, result) { 
  4.     if (err) return callback(err) 
  5.     callback(null, result, sourceMaps, meta) 
  6.   }) 

② "Raw" loader

默认情况下,资源文件会被转化为 UTF-8 字符串,然后传给 loader。通过设置 raw 为 true,loader 可以接收原始的 Buffer。每一个 loader 都可以用 String 或者 Buffer 的形式传递它的处理结果。complier 将会把它们在 loader 之间相互转换。大家熟悉的 file-loader 就是用了这个。简而言之:你加上 module.exports.raw = true; 传给你的就是 Buffer 了,处理返回的类型也并非一定要是 Buffer,webpack 并没有限制。

  1. module.exports = function (content) { 
  2.   console.log(content instanceof Buffer); // true 
  3.   return doSomeOperation(content) 
  4. // 划重点↓ 
  5. module.exports.raw = true

③ Pitching loader

我们每一个 loader 都可以有一个 pitch 方法,大家都知道,loader 是按照从右往左的顺序被调用的,但是实际上,在此之前会有一个按照从左往右执行每一个 loader 的 pitch 方法的过程。pitch 方法共有三个参数:

  • remainingRequest:loader 链中排在自己后面的 loader 以及资源文件的绝对路径以!作为连接符组成的字符串。
  • precedingRequest:loader 链中排在自己前面的 loader 的绝对路径以!作为连接符组成的字符串。
  • data:每个 loader 中存放在上下文中的固定字段,可用于 pitch 给 loader 传递数据。

在 pitch 中传给 data 的数据,在后续的调用执行阶段,是可以在 this.data 中获取到的:

  1. module.exports = function (content) { 
  2.   return someSyncOperation(content, this.data.value);// 这里的 this.data.value === 42 
  3. }; 
  4.  
  5. module.exports.pitch = function (remainingRequest, precedingRequest, data) { 
  6.   data.value = 42; 
  7. }; 

注意! 如果某一个 loader 的 pitch 方法中返回了值,那么他会直接“往回走”,跳过后续的步骤,来举个例子:

Webpack - 手把手教你写一个 loader / plugin

假设我们现在是这样:use: ['a-loader', 'b-loader', 'c-loader'],那么正常的调用顺序是这样:

Webpack - 手把手教你写一个 loader / plugin

现在 b-loader 的 pitch 改为了有返回值:

  1. // b-loader.js 
  2. module.exports = function (content) { 
  3.   return someSyncOperation(content); 
  4. }; 
  5.  
  6. module.exports.pitch = function (remainingRequest, precedingRequest, data) { 
  7.   return "诶,我直接返回,就是玩儿~" 
  8. }; 

那么现在的调用就会变成这样,直接“回头”,跳过了原来的其他三个步骤:

Webpack - 手把手教你写一个 loader / plugin

1.5 其他 API

  • this.addDependency:加入一个文件进行监听,一旦文件产生变化就会重新调用这个 loader 进行处理
  • this.cacheable:默认情况下 loader 的处理结果会有缓存效果,给这个方法传入 false 可以关闭这个效果
  • this.clearDependencies:清除 loader 的所有依赖
  • this.context:文件所在的目录(不包含文件名)
  • this.data:pitch 阶段和正常调用阶段共享的对象
  • this.getOptions(schema):用来获取配置的 loader 参数选项
  • this.resolve:像 require 表达式一样解析一个 request。resolve(context: string, request: string, callback: function(err, result: string))
  • this.loaders:所有 loader 组成的数组。它在 pitch 阶段的时候是可以写入的。
  • this.resource:获取当前请求路径,包含参数:'/abc/resource.js?rrr'
  • this.resourcePath:不包含参数的路径:'/abc/resource.js'
  • this.sourceMap:bool 类型,是否应该生成一个 sourceMap

官方还提供了很多实用 Api ,这边只列举一些可能常用的,更多可以戳链接