如何封装一个loader

关于loader

loader,我们在上一篇文章已经说过了是一个模块转换器。我们都知道webpack是基于node环境的工具,会把其他文件最终转换成js文件来处理。那么怎么去识别这些文件并且做处理呢,这个时候loader就该上场了。

loader的使用

loader路径解析

我们先来看看一个普通的loader是怎么使用的。

1
2
3
4
5
6
7
8
9
rules: [
{
test: /\.css/,
use: [
'style-loader',
'css-loader',
]
},
]

实际上我们在配置中使用的是两个loader的路径名称,通过node_modules暴露出来的,所以省略了路径。所以我们自己写loader的时候也可以直接用路径。

1
2
3
4
5
6
7
8
9
10
rules: [
{
test: /\.css/,
use: [
'style-loader',
'css-loader',
path.reslove(__dirname, 'loaders', 'loader1.js')
]
},
]

但是这样写很突兀有没有,不要紧,webpack给我们提供了配置解析loader的API

1
2
3
4
5
6
resolveLoader: {
modules: [
'node_modules',
path.resolve(__dirname, 'loaders')
]
},

这样他在node_modules里没找到回去loaders文件夹下找名为loader1.js的loader

loader自定义函数

除开路径不谈,loader本身是向外暴露了一个方法,该方法有三个参数:

  • source
  • map
  • meta

source代表的是文件内容,其余两个我也不太清楚,没用到,后续有再补充

这个时候最简单的一个loader就可以形成了。

1
2
3
4
// 注意这边是声明函数,不能使用箭头函数,否则会丢失this指向
module.exports = function (source) {
return source
}

然后介绍一下自定义loader的两个最重要的API:

  • callback
1
2
3
4
5
6
7
8
module.exports = function (source) {
// content: string | Buffer,
// sourceMap?: SourceMap,
// meta?: any

// 同步loader,这个时候可以对content进行处理
this.callback(null, content, map, meta)
}
  • async
1
2
3
4
5
6
7
module.exports = function (source) {
// 异步loader
const callback = this.async();
setTimeout(() => {
callback(null, content)
}, 1000)
}

同时,在我们使用loader的使用把需要的参数通过options传进来

我们需要使用到loader-utils和schema-utils两个依赖包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ? 获取loader参数的库
const { getOptions } = require('loader-utils');
// ? 校验loader参数的库
const { validate } = require('schema-utils');
// ? 校验参数的格式json
const schema = require('../loader-params/loader1.json')

module.exports = function (source) {

console.log(111)

// * 获取options
const options = getOptions(this);

// * 校验options是否合法
validate(schema, options, {
name: 'loader1'
})


const result = content.replace('我是console', options.desc)

return result;
}

关于校验函数的格式是一个json,如下:

1
2
3
4
5
6
7
8
9
10
{
"type": "object",
"properties": {
"desc": {
"type": "string",
"description": "描述~~~~"
}
},
"additionalProperties": true
}

有趣的是loader在执行时候执行pitch方法,感兴趣的可以研究一下

1
2
3
module.exports.pitch = () => {
console.log('pitch 111')
}

封装loader

我们简单利用babel这个工具封装一个babelLoader

babelLoader.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const { getOptions } = require('loader-utils');
const { validate } = require('schema-utils')
const babelSchema = require('../loader-params/babel.json')
const babel = require('@babel/core');
const util = require('util')

// ? util.promiseify 将普通异步方法转化为基于promise的异步方法
const transform = util.promisify(babel.transform)

module.exports = function (source, map, meta) {
const options = getOptions(this);

validate(babelSchema, options, {
name: 'babelLoader'
})

// ? 创建异步
const callback = this.async();

// ? 利用babel编译
transform(source, options)
.then(({ code, map }) => callback(null, code, map, meta))
.catch(e => callback(e))
}

babel.json

1
2
3
4
5
6
7
8
9
{
"type": "object",
"properties": {
"presets": {
"type": "array"
}
},
"additionalProperties": true
}