实现一个简易的webpack-loader
# 前言
loader是webpack中非常重要的一个东西。大家都知道webpack是只认识js文件和json文件的。那在构建过程中,他遇到的其他文件该怎么办呢?loader就是来干这些事情的,把一些webpack无法识别的资源转变成可以识别的类型。 下面就来剖析一下loader
一个loader其实就是一个nodejs模块,他导出一个函数,这个函数只有一个入参,那就是待处理的文件内容的字符串。而这个函数的返回值就是处理过后的文件内容字符串。 也就是说,最简单的loader长这样
module.exports = function (content){
// content 就是传入的源内容字符串
return content
}
2
3
4
以上就是一个最简单的loader了。下面让我们来看看怎么让这个loader变得更丰富
# 四种loader
我们可以把loader大致分为四种
- 同步loader
- 异步loader
- “Raw” loader
- pitching loader
# 同步loader
同步loader就像上面最简单的loader一样,直接采用return的方式返回处理结果。当然也可以采用callback的方式(webpack提供了很多方法让开发者使用)然后在最后直接 **return undefined **的方式告诉 webpack 去 this.callback() 寻找他要的结果,这个 api 接受这些参数:
this.callback(
err: Error | null, // 一个无法正常编译时的 Error 或者 直接给个 null
content: string | Buffer,// 我们处理后返回的内容 可以是 string 或者 Buffer()
sourceMap?: SourceMap, // 可选 可以是一个被正常解析的 source map
meta?: any // 可选 可以是任何东西,比如一个公用的 AST 语法树
);
2
3
4
5
6
7
和上述提到的callback一样,还有一个常用的api---getOptions,这个api是用来获取用户传入的参数的
module.exports = function (content) {
// 获取到用户传给当前 loader 的参数
const options = this.getOptions()
const res = someSyncOperation(content, options)
this.callback(null, res, sourceMaps);
// 注意这里由于使用了 this.callback 直接 return 就行
return
}
2
3
4
5
6
7
8
这样,一个同步的loader就完成了
# 异步loader
而异步的loader和同步的loader其实区别不大。只需要通过async函数来返回处理结果就行了
module.exports = function (content) {
var callback = this.async()
someAsyncOperation(content, function (err, result) {
if (err) return callback(err)
callback(null, result, sourceMaps, meta)
})
}
2
3
4
5
6
7
8
# "Raw" loader
Raw laoder其实就是给这个loader设置一个raw属性为true,
module.exports = function (content) {
console.log(content instanceof Buffer); // true
return doSomeOperation(content)
}
// 划重点↓
module.exports.raw = true;
2
3
4
5
6
7
默认情况下资源文件会被转换成utf-8的字符串传递给loader,而通过设置raw为true,我们的loader就可以接收到原始的buffer文件流。
# Pitching loader
每个loader都可以有一个pitch方法,我们都知道loader的执行顺序是从右往左的,其实在这之前还会从左到右去执行一遍loader的pitch方法。 pitch方法共有三个参数
- remainingRequest:loader 链中排在自己后面的 loader 以及资源文件的绝对路径以!作为连接符组成的字符串。
- precedingRequest:loader 链中排在自己前面的 loader 的绝对路径以!作为连接符组成的字符串。
- data:每个 loader 中存放在上下文中的固定字段,可用于 pitch 给 loader 传递数据。
在 pitch 中传给 data 的数据,在后续的调用执行阶段,是可以在 this.data 中获取到的:
module.exports = function (content) {
return someSyncOperation(content, this.data.value);// 这里的 this.data.value === 42
};
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
data.value = 42;
};
2
3
4
5
6
7
8
注意,如果某个loader的pitch直接返回的值,那他就会往回走。跳过后续的步骤,举个例子
现在有如下几个loader:use:['a-loader','b-loader','c-loader']
。那么他的执行顺序应该是这样的
a-loader pitch->b-loader pitch->c-loader pitch->c-loader->b-loader->a-loader
而当有一个pitch有返回值时,就会这样。比如b-loader的pitch有返回值
a-loader pitch->b-loader pitch->b-loader pitch返回了值->a-loader
# 其他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 ,这边值列举一些可能常用的,更多可以戳链接 更多详见官方链接 (opens new window)
# 实践
下面来写两个简单的loader练练手,第一个 给每个js文件都添加一段注释,第二个删除代码中的console.log
添加注释
module.exports = function (source) {
const options = this.getOptions() // 获取 webpack 配置中传来的 option
this.callback(null, addSign(source, options.sign))
return
}
function addSign(content, sign) {
return `/** ${sign} */\n${content}`
}
// 使用
{
test: /\.js$/
use: [
{
loader: path.resolve('path/to/loader.js'),
options: {
sign:'xxxxxx'
}
}
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
删除打印
module.exports = function (content) {
return handleConsole(content)
}
function handleConsole(content) {
return content.replace(/console.log\(['|"](.*?)['|"]\)/, '')
}
2
3
4
5
6
7
8
本地测试可以采用path.resolve的方式引入loader。
# 总结
loader在webpack生态链里扮演了将webpack不认识的资源编译成他认识的资源的角色,他本质上就是一个nodejs模块,导出一个函数,这个函数接收当前文件资源的内容字符串。并且这个函数需要返回处理之后的资源内容。loader也分为异步和同步两种,同步的直接返回值即可,异步的需要使用webpack提供的async函数来返回处理完成的资源内容。 当我们设置loader的raw属性为true时就可以接受buffer资源而不是字符串资源。 loader的执行顺序是从右到左,但是在这之前还会从左到右的去执行一遍loader的pitch函数,这个函数可以获取到上一个和下一个loader的信息,以及可以在当前loader的data中定义属性,我们可以在loader被执行的时候再this.data中访问到。 当pitch有返回值时,那么loader的执行顺序会立即回头,并不会再按原先的顺序执行下去,而是直接掉头开始执行上一个loader。