在这一小节我们会来介绍如何创建一个 webpack 可用的 loader。
loader 是一个函数
先来看一个简单的例子:
"use strict";
const marked = require("marked");
const loaderUtils = require("loader-utils");
module.exports = function (markdown) {
// 使用 loaderUtils 来获取 loader 的配置项
// this 是构建运行时的一些上下文信息
const options = loaderUtils.getOptions(this);
this.cacheable();
// 把配置项直接传递给 marked
marked.setOptions(options);
// 使用 marked 处理 markdown 字符串,然后返回
return marked(markdown);
};
这是 markdown-loader 的实现代码,笔者添加了一些代码说明,看上去很简单。
markdown-loader 本身仅仅只是一个函数,接收模块代码的内容,然后返回代码内容转化后的结果。webpack loader 的本质就是这样的一个函数。
上述代码中用到的 loader-utils 是 webpack 官方提供的一个工具库,提供 loader 处理时需要用到的一些工具方法,例如用来解析上下文 loader 配置项的 getOptions。关于这个工具库的内容和功能不是特别复杂,就不展开了,直接参考这个库的官方文档即可。
代码中还用到了 marked,marked 是一个用于解析 Markdown 的类库,可以把 Markdown 转为 HTML,markdown-loader 的核心功能就是用它来实现的。基本上,webpack loader 都是基于一个实现核心功能的类库来开发的,例如 sass-loader 是基于 node-sass 实现的,等等。
开始一个 loader 的开发
我们可以在 webpack 配置中直接使用路径来指定使用本地的 loader,或者在 loader 路径解析中加入本地开发 loader 的目录。
看看配置例子:
// ...
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: path.resolve('./loader/index.js'), // 使用本地的 ./loader/index.js 作为 loader
},
],
},
// 在 resolveLoader 中添加本地开发的 loaders 存放路径
// 如果你同时需要开发多个 loader,那么这个方式会更加适合你
resolveLoader: {
modules: [
'node_modules',
path.resolver(__dirname, 'loaders')
],
},
如果你熟悉 Node 的话,也可以使用 npm link 的方式来开发和调试,关于这个方式,可以参考 npm 的官方文档 npm-link。
复杂一点的情况
当我们选择上述任意一种方法,并且做好相应的准备后,我们就可以开始写 loader 的代码了,然后通过执行 webpack 构建来查看 loader 是否正常工作。
上面已经提到,loader 是一个函数,接收代码内容,然后返回处理结果,有一些 loader 的实现基本上就是这么简单,但是有时候会遇见相对复杂一点的情况。
首先 loader 函数接受的参数是有三个的:content, map, meta。content 是模块内容,但不仅限于字符串,也可以是 buffer,例如一些图片或者字体等文件。map 则是 sourcemap 对象,meta 是其他的一些元数据。loader 函数单纯返回一个值,这个值是当成 content 去处理,但如果你需要返回 sourcemap 对象或者 meta 数据,甚至是抛出一个 loader 异常给 webpack 时,你需要使用 this.callback(err, content, map, meta) 来传递这些数据。
我们日常使用 webpack,有时候会把多个 loader 串起来一起使用,最常见的莫过于 css-loader 和 style-loader 了。当我们配置 use: [‘bar-loader’, ‘foo-loader’] 时,loader 是以相反的顺序执行的,即先跑 foo-loader,再跑 bar-loader。这一部分内容在配置 loader 的小节中有提及,这里再以开发 loader 的角度稍稍强调下,搬运官网的一段说明:
最后的 loader 最早调用,传入原始的资源内容(可能是代码,也可能是二进制文件,用 buffer 处理) 第一个 loader 最后调用,期望返回是 JS 代码和 sourcemap 对象(可选) 中间的 loader 执行时,传入的是上一个 loader 执行的结果 虽然有多个 loader 时遵循这样的执行顺序,但对于大多数单个 loader 来说无须感知这一点,只负责好处理接受的内容就好。
还有一个场景是 loader 中的异步处理。有一些 loader 在执行过程中可能依赖于外部 I/O 的结果,导致它必须使用异步的方式来处理,这个使用需要在 loader 执行时使用 this.async() 来标识该 loader 是异步处理的,然后使用 this.callback 来返回 loader 处理结果。
例子可以参考官方文档:异步 loader。
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;
}
我们可以简单把 pitch 理解为 loader 的前置钩子,它可以使用 this.data 来传递数据,然后具备跳过剩余 loader 的能力。
在一个 use 配置中所有 loader 执行前会先执行它们对应的 pitch,并且与 loader 执行顺序是相反的,如:
use: [
'bar-loader',
'foo-loader',
],
// 执行 bar-loader 的 pitch
// 执行 foo-loader 的 pitch
// bar-loader
// foo-loader
其中,当 pitch 中返回了结果,那么执行顺序会回过头来,跳掉剩余的 loader,如 bar-loader 的 pitch 返回结果了,那么执行只剩下
// 执行 bar-loader 的 pitch
可能只有比较少的 loader 会用到 pitch 这个功能,但有的时候考虑实现 loader 功能需求时把 pitch 纳入范围会有不一样的灵感,它可以让你更加灵活地去定义 loader 的执行。
这里的简单介绍仅做抛砖引玉之用,详细的学习和了解可以参考官方文档 Pitching loader 或者 bundler-loader 源码 bundler-loader。
loader 上下文
上述提及的一些代码会使用到 this,即 loader 函数的上下文,包括 this.callback 和 this.data 等,可以这样简单地理解: this 是作为 loader 运行时数据和调用方法的补充载体。
loader 上下文有很多运行时的信息,如 this.context 和 this.request 等等,而最重要的方法莫过于 this.callback 和 this.async,关于上下文这里不做展开,官方文档有比较详细的说明:loader API。
当你在开发 loader 过程中发现需要某些运行时数据时,就可以查阅 loader API,基本上该有的数据都有了。
一个好 loader 是怎么样的
loader 作为 webpack 解析资源的一种扩展方式,最重要的是足够简单易用,专注于处理自己那一块的内容,便于维护,可以和其他多个 loader 协同来处理更加复杂的情况。
官方对于 loader 的使用和开发有一些准则,一个好的 loader 应该符合官方的这些定义:Loader 准则。
社区中有相当多的优秀 loader 可以作为参考,例如刚开始提及的 markdown-loader,相当地简单易用。由于 loader 的这种准则和特性,大部分的 loader 源码都相对容易解读,便于我们学习参考。
作为一个 loader 开发者,你应该尽可能遵循这些准则(有些特殊情况需要特殊处理),这样会让你开发出质量更高、更易维护和使用的 webpack loader。
小结
本小节我们从下面几个方面介绍了如何开发一个 webpack loader:
-
loader 本质上的实现是一个函数
-
如何开始着手开发一个 loader
-
loader 的输入和输出
-
pitch 函数的作用
-
loader 函数的上下文
-
一个好的 loader 是怎么样的
loader 的实现相对简单,webpack 社区现成可用的 loader 很多,当你在开发 loader 时遇见了问题,不妨去查阅一下现有 loader 的源码,或许会有不一样的灵感。
参考资料
https://www.kancloud.cn/sllyli/webpack/1242359