本文作者:IMWeb 嵘么么 原文出处:IMWeb社区 未经同意,禁止转载
webpack提倡一切皆模块,所有类型的文件都可以经过文件加载器处理变成我们可加载的模块,那么这个文件加载器便是loader。
那么我们如何开发一个webpack loader呢,让我们一起探索探索吧~
在开发loader之前,我们先了解一下webpack loader的执行顺序。
webpack是支持loader的链式调用的,即一个文件可以经多个loader处理。当一个文件使用多个loader处理时,他的处理顺序是倒序,即传入loader数组的从右到左执行。
例如,对于scss文件,我们的配置如下,那么它的执行顺序是sass-loader -》 css-loader -》 postcss-loader -》style-loader:
module: {
rules: [
{
test: /\.scss|\.css/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
},
},
'postcss-loader',
loader: 'sass-loader',
],
}
}
那么如何来开发一个loader呢?让我们慢慢来揭开webpack loader 的什么面纱~
loader其实是一个导出为函数的 JavaScript模块,是不是看起来很简单?实际呢,loader开发也很简单。即
test.loader.js内容如下:
module.exports = function(content) {
return transform(content); // 对content进行处理并返回给webpack
}
既然我们说了所谓 loader 只是一个导出为函数的 JavaScript模块,那么它的传入是什么呢?
content: string | Buffer, // 文件内容
sourceMap?: SourceMap, // 上一个loader解析完后生成的 source map
meta?: any // 会被 webpack 忽略,可以是任何东西(例如一些元数据)
显然loader就是对文件进行处理的,那么这里的content便是文件内容。
默认情况下,资源文件会被转化为 UTF-8 字符串,然后传给 loader。通过设置 raw,loader 可以接收原始的 Buffer。我们常用的file-loader就设置了raw为true,以告诉webpack传入原始的二进制数据
module.exports.raw = true;
需要注意的是:第一个 loader 的传入参数只有一个:资源文件的内容content。其他都是经过loader处理后可选择传递给下一个loader的。
loader返回的处理结果应该和传入一样是 String 或者 Buffer。 上面有讲到除了content,loader其实还接受两个可选的入参,返回也一样。所以当我们如果是单个处理结果,可以在函数中直接返回。但是如果有多个处理结果,我们则必须通过this.callback()将处理结果传递给下一个loader。
module.exports = function(content) {
const newContent = transform(content); // 对content进行处理
this.callback(null,newContent, sourceMaps, meta);
return; // 当使用this.callback时函数应该return undefined
}
这里的this
既不是webpack实例,也不是compiler、compilation、normalModule等这些实例。而是loader-runner构造的loaderContext对象,提供了各种loader API(具体API可见 https://webpack.js.org/api/loaders/ )。
对于meta
参数,一般传入抽象语法树(abstract syntax tree - AST),这样可以在多个 loader 之间共享通用的 AST,这样做有助于加速编译时间。
所以总结来说,loader的工作流程是:
到这里基本已经清楚了loader的整个工作流程。我们在使用loader时,经常会传入一些自定义的options,那么loader怎么获取这些options呢?
webpack 提供了loader-utils包和schema-utils 包。loader-utils提供了许多有用的工具,但最常用的一种工具是获取传递给 loader 的选项。schema-utils 包配合 loader-utils,用于保证 loader 选项,进行与 JSON Schema 结构一致的校验。
const loaderUtils = 'loader-utils';
module.exports = function(content) {
const options = loaderUtils.getOptions(this); // 用户传入的options
return transform(content); // 对content进行处理并返回给webpack
}
前面讲到过,webpack的loader执行顺序是从后往前。有些时候我们希望选择性的越过后续loader执行,webpack给每个loader提供了pitch方法进行设置。
根据webpack官网给出的案例,对于下面的配置:
module.exports = {
//...
module: {
rules: [
{
//...
use: [
'a-loader',
'b-loader',
'c-loader'
]
}
]
}
};
webpack 在实际(从右到左)执行 loader 之前,会先从左到右调用 loader 上的 pitch 方法。所以实际执行顺序如下:
|- a-loader `pitch`
|- b-loader `pitch`
|- c-loader `pitch`
|- requested module is picked up as a dependency
|- c-loader normal execution
|- b-loader normal execution
|- a-loader normal execution
pitch方法若有返回值,则会跳过后续的loader。比如上面如果 b-loader 的 pitch 方法有返回值,那么此时loader的执行流程是:
|- a-loader `pitch`
|- b-loader `pitch` returns a module
|- a-loader normal execution
接下来我们来开发一个自己的loader
比如现在有个场景,要求我们给所有的apng 请求url加上参数?nowebp=1。loader代码如下:
apng-url-resolve.js
module.exports = function(content) {
return content.replace(/\.apng(.*\.png)?/, '.apng$1?nowebp=1')
}
webpack loader 配置:
const path = require('path');
modules:
{
rules: [
{
test: /.js$/
use: path.resolve(__dirname, 'build/loaders/apng-url-resolve.js' )
}
]
}
这篇文章从webpack loader的工作流程出发讲解了如何开发一个自定义loader,下篇文章将讲解webpack plugin 的开发,敬请期待~