
loader
是什么
loader 是一个具有单一职责的转换器 在 webpack 中一切都是 js 模块,而 loader 的作用就是把非 js 模块转化为 js 模块,供 webpack 运行打包。
单一职责就是指一个 loader 只负责一种转换,单一职责是 webpack 社区对 loader 定义的约束,
写法
loader 的实现是 module.exports 为 function 的 js 模块
javascript
module.exports = function (content,map,meta) {
...
}
- content:资源文件的内容,对于起始 loader 只有这一个参数
- map:前面 loader 生成的 source map 可以传递给后方 loader 共享
- meta:其他需要传给后方 loader 共享的信息,可自定义
种类
- 前置-Pre
- 普通-Normal
- 后置-Post
- 行内-Inline
可在配置文件中通过 Rule.enforce 属性指定 loader 的类型,默认为空,表示 normal 值可为 pre 和 post。类型会影响 loader 执行顺序
javascript
module: {
rules: [
{
test: /\.js$/,
use: ['pre-loader'],
enforce: 'pre'
},
{
test: /\.js$/,
use: ['normal-loader']
},
{
test: /\.js$/,
use: ['post-loader'],
enforce: 'post'
}
]
}
可以 inline 调用,但是不推荐
输入和输出
默认情况下,资源文件会被转化为 UTF-8 字符串,然后传给 loader,通过设置 raw 为 true,loader 可以接收原始的 buffer
javascript
module.exports = function (content) {
return someSyncOperation(content)
}
module.exports.raw = true
loader 的输出内容必须是 String 或者 buffer 类型
同步和异步
loader 可以是同步的,也可以是异步的 同步: 使用 this.callback()或者直接 return 输出。this.callback 的好处在于可以传递更多的内容参数
javascript
module.exports = function (content, map, meta) {
const output = someSyncOperation(content)
return output
// or
this.callback(null, output, map, meta)
return
}
异步: 通过 this.async()获取回调方法
javascript
module.exports = function (content, map, meta) {
const callback = this.async()
someAsyncOperation(content, function (err, result, sourceMaps, meta) {
if (err) return callback(err)
callback(null, result, sourceMaps, meta)
})
}
缓存 默认情况下,webpack 会缓存 loader 的输出结果,输入和相关依赖(通过 this.addDependency 或者 this.addContextDependency 添加)没有变化时,会返回相同的结果
可以通过在 loader 中执行 this.cacheable(false)关闭缓存功能
javascript
cacheable(flag = true: boolean)
执行顺序 默认从右往左走
Pitch 和 Normal
Loader 执行包括两个阶段,pitch 阶段和 normal 阶段
Normal 阶段,就是大家一般认为的 loader 对源文件进行转译的阶段
Pitch 阶段会先于 normal 阶段执行,如果 loader 定义了 pitch 方法,就会在 pitch 阶段被执行,如果 loader 的 pitch 方法返回了内容,就会跳过 loader 的 pitch 和 mormal 阶段
javascript
module.exports = function (content) {
return someOperation(content)
}
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
if (someCondition()) {
return someContent
}
}
- remainingRequest:loader 链中在自己之后的 loader 的 request 字符串
- precedingRequest:loader 链中在自己之前的 loader 的 request 字符串
- data:data 对象,该对象在 normal 阶段可以通过 this.data 获取,可用于传递共享的信息
request 字符串:loader 以及目标资源文件的绝对路径以“!”拼接起来的字符串,类似 inline loader 的 require 路径。如:
javascript
'/src/project/node_modules/css-loader/index.js!/src/project/node_modules/less-loader/dist/cjs.js!/src/project/src/styles/index.less'
参数 data 的使用:
javascript
module.exports = function (content) {
console.log(this.data.value) // 42
return someOperation(content)
}
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
data.value = 42
}
真实顺序: loader 实际的执行顺序与 loader 的类型,pitch 方法,inline-loader 的前缀都有关系
各类型的 loader 的执行优先级为:Pre loader > Inline loader > Normal loader > Post loader
例子
javascript
module: {
rules: [
{
test: /\.js$/,
use: ['pre-loader'],
enforce: 'pre',
},
{
test: /\.js$/,
use: ['normal-loader-a', 'normal-loader-b'],
},
{
enforce: 'post',
test: /\.js$/,
use: ['post-loader'],
},
],
}
然后 js 中还调用了 inline-loader
javascript
const someModule = import('inline-loader-a!inline-loader-b!./someModule.js')
那么 loader 调用的顺序链为: ['pre-loader', 'inline-loader-a', 'inline-loader-b', 'normal-loader-a', 'normal-loader-b', 'post-loader']
如果 js 中对 inline-loader 的调用有前缀
javascript
const someModule = import('-!inline-loader-a!inline-loader-b!./someModule.js') // 使用 -! 前缀禁用配置中的pre loader和normal loader
执行顺序变成: 在这个前提下如果 inline-loader-b 的 pitch 方法有返回值,那么顺序就会变成
loader-runner
Loader 的实现本质上是一个方法,输入一个模块后,在 webpack 内部由 loader-runner 负责组织和调用 loader。工作流如下:
loader context
loader 存在运行上下文,可以通过 this 去访问一些属性和方法
this.getOptions 获取配置文件中传给该 loader 的 options this.callback
javascript
this.callback(
err: Error | null,
content: string | Buffer,
sourceMap?: SourceMap,
meta?: any
);
- sourceMap:返回本次转换中生成的 source map
- meta:本次转换中生成的额外信息,可自定义。例如本次转换为源文件生成了 AST,则可将该 ast 传给后面的 loader,以免需要 ast 的 loader 去重复生成而降低性能
this.async 告诉 loader-runner 这个 loader 将会异步的回调,返回 this.callback this.request 被解析出来的 request 字符串,类似 Inline loader 的调用比如: "/abc/loader1.js?xyz!/abc/node_modules/loader2/index.js!/abc/resource.js?rrr"
this.loaders loader 的调用链数组 this.addDependency 添加一个文件作为 loader 的依赖,若 loader 开启了缓存,该文件变化时会使缓存失效并重新调用 loader 例如,sass-loader 和 less-loader 就使用了这方法,当它发现导入的 css 文件发生变化时就会重新编译。
this.addContextDependency 添加一个目录作为 loader 的依赖
this.sourceMap 可通过 this.sourceMap()获取配置中是否要求生成 source map this.emitFile emitFile(name**😗* string, content**😗* Buffer**|string, sourceMap😗* {...})
本地开发
默认情况下,webpack 只会去 node_modules 中寻找 loader,可以通过修改配置文件中的 resolveLoader.modules 让 webpack 查找本地的 loader。
比如我们开发的 loader 路径为./path-to-your-loader/first-loader
javascript
module.exports = {
resolveLoader: {
modules: ['node_modules', 'path-to-your-loader'] // 指定webpack去哪些目录下查找loader(有先后顺序)
}
}
然后就可以使用了
javascript
{
module: {
rules: [
{
test: /\.css$/,
use: ['first-loader']
}
]
}
}
loader 开发原则
简单易用 loader 应该只做单一任务,这不仅使每个 loader 易维护,也可以在更多场景链式调用
使用链式传递
利用 loader 可以链式调用的优势,功能隔离不仅使 loader 更简单,可能还可以将它们用于你原先没有想到的功能。
模块化输出(Emit modular output)
保证输出模块化。loader 生成的模块与普通模块遵循相同的设计原则。
确保无状态(Make sure they're stateless)
确保 loader 在不同模块转换之间不保存状态。每次运行都应该独立于其他编译模块以及相同模块之前的编译结果。
使用 loader utilities(Employ loader utilities)
充分利用 loader-utils 包,它提供了许多有用的工具。
记录 loader 的依赖(Mark loader dependencies)
如果一个 loader 使用外部资源(例如从文件系统读取),必须声明它。这些信息用于使缓存 loaders 无效,以及在观察模式(watch mode)下重编译。
解析模块依赖关系(Resolve module dependencies)
根据模块类型,可能会有不同的模式指定依赖关系,这些依赖关系应该由模块系统解析。 解析模块有以下两种方式:
- 通过把它们转化成 require 语句
- 使用 this.resolve 函数解析路径
如 css-loader 就是第一种方式的一个例子。它将@import 语句替换为 require 来引用其他样式文件,将 url(...)替换为 require 来引用文件,从而实现将依赖关系转化为 require 声明。
提取通用代码(Extract common code)
避免在 loader 处理的每个模块中生成通用代码。相反,你应该在 loader 中创建一个运行时文件,并生成 require 语句以引用该共享模块。
避免绝对路径(Avoid absolute paths)
不要在模块代码中插入绝对路径,因为当项目根路径变化时,文件绝对路径也会变化。loader-utils 中的 stringifyRequest 方法,可以将绝对路径转化为相对路径。
使用 peer dependencies(Use peer dependencies)
如果你的 loader 依赖另一个包,你应该把这个包作为一个 peerDependency 引入,这样可以让使用你的包的开发者更好地管理依赖。