# 一.loader的实现

# 1.loader 是一个函数

先看一个简单的例子:

const marked = require("marked")
const laoderUtils = require("laoder-utils")

module.exports = function(markdown) {
  // 使用loaderUtils来获取loader的配置项
  // this 是构建运行时的一些上下文信息
  const options = loaderUtils.getOptions(this)

  this.cacheable()

  // 把配置项直接传递给marked
  marked.setOptions(options)

  // 使用marked处理marked字符串,然后返回
  return marked(markdown)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

这是markdown-loader的实现代码,笔者添加了一些代码说明,看上去很简单。

markdown-loader 本身仅仅只是一个函数,接收模块代码的内容,然后返回代码内容转化后的结果。webpack loader 的本质就是这样的一个函数。

上述代码中用到loader-utils是 webpack 官方提供的一个工具库,提供 loader 处理是需要用到的一些工具方法,例如用来解析上下文 loader 配置项的getOptions

代码中还用到了marked,marked 是一个用于解析 Markdown 的类库,可以把 Markedown 转为 HTML,markdown-loader 的核心功能就是用它实现的。基本上,webpack loader 都是基于一个实现核心功能的类库来开发的,例如sass-loader是基于node-sass实现的,等等。

# 2.开始一个 laoder 的开发

我们可以在 webpack 配置中直接使用路径来指定使用本地 loader,或者在 loader 路径解析中加入本地开发 loader 的目录。看看配置例子:

module: {
  rules: [
    {
      test: /\.js$/,
      exclude: /node_modules/,
      loader: path.resolve("./loader/index.js"), //使用本地的 ./loader/index.js作为loader
    },
  ]
},

// 在resolveLoader中添加本地开发的loaders存放路径
// 如果你同时需要开发多个laoder,那么这个方式会更加适合你
resolveLoader:{
  modules:[
    'node_modules',
    path.resolve(__dirname,'loaders')
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 3.复杂一点的情况

当我们选择上述任意一种方法,并且做好相应的准备后,我们就可以开始写 loader 的代码了,然后通过执行 webpack 构建来查看 loader 是否正常工作。

上面已经提到,loader 是一个函数,接收代码内容,然后返回处理结果,有一些 laoder 的实现基本上就是这么简单,但是有时候遇见相对复杂一点的情况。

首先 loader 函数接受的参数是有三个的:content, map, metacontent是模块内容,但不仅限于字符串,也可以是 buffer,例如一些图片或者字体等文件。map则是 sourcemap 对象,meta是其他的一些元数据。loader 函数单纯返回一个值,这个值是当成 content 去处理,但如果你需要返回 sourcemap 对象或者 meta 数据,甚至是抛出一个 laoder 异常给 webpack 是,你需要使用this.callback(err, content, map, meta)来传递这些数据。

我们日常使用 webpack,有时候会把多个 laoder 串起来一起使用,最常见的莫过于 css-loader 和 style-loader 了。当我们皮遏制use:['bar-loader', 'foo-loader']时,loader 是以相反的顺序执行的,即先跑 foo-loader,再跑 bar-loader。这一部分内容在配置 loader 的小节中有提及,这里再以开放 loader 的角度稍稍强调下,搬运官网的一段说明:

  • 最后的 loader 最早调用,传入原始的资源内容(可能是代码,也可能是二进制文件,用 buffer 处理)
  • 第一个 laoder 最后调用,期望返回时 JS 代码和 sourcemap 对象
  • 中间 laoder 执行时,传入的是上一个 loader 执行的结果

虽然有多个 loader 时遵循这样的执行顺序,但对于大多数单个 laoder 来说无须感知这一点,只负责处理接受的内容就好。

还有一个场景是 loader 中的异步处理。有一些 loader 在执行过程中坑你依赖外部 I/O 的结果,导致他必须使用异步的方式来处理,这个使用需要咋 loader 是使用this.async()来标识该 loader 是异步处理的,然后使用this.callback来返回 laoder 处理结果。

# 4.Pitching loader

我们可以使用pitch来跳过 loader 的处理,pitch方法是 loader 额外实现的一个函数,看下官方文档中的一个例子:

module.exports = function(content) {
  return someSyncOperation(content, this.data.value) //pitch的缘故,这里的data.value为42
}

// 挂在loader函数上的pitch函数
module.exports.pitch = function(remainingRequest, precedingRequest, data) {
  data.value = 42
}
1
2
3
4
5
6
7
8

我们可以简单把pitch理解为 loader 的前置钩子,它可以使用this.data来传递数据,然后具备跳过剩余 loader 的能力。

在一个use配置中所有 loader 执行前会先执行它们对应的pitch,并且与 loader 执行顺序是相反的,如:

use: [
  'bar-loader',
  'foo-loader'
],
// 执行bar-loader的pitch
// 执行foo-loader的pitch
// bar-loader
// foo-loader
1
2
3
4
5
6
7
8

其中,当 pitch 中返回了结果,那么执行顺序会回过头来,跳掉剩余的 loader,如bar-laoder的 pitch 返回结果了,那么执行剩下

// 执行bar-loader 的pitch
1

可能只有比较少的 loader 会用到 pitch 这个功能,但有的时候需要考虑实现 loader 功能需求时把 pitch 纳入范围会有不一样的灵感,它可以让你更加灵活地去定义 loader 的执行。

# 5.loader 上下文

上述提及的一些代码会使用到this,即 loader 函数的上下文,包括this.callbackthis.data等。可以这样简单地理解:this是作为 laoder 运行时数据和调用犯法的补充载体。

laoder 上下文有很多运行时的信息,如this.contextthis.request等等,而最重要的方法莫过于this.callbackthis.async,关于上下文这里不做展开,官方文档有比较详细的说明。