Skip to content

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']

image.png 如果 js 中对 inline-loader 的调用有前缀

javascript
const someModule = import('-!inline-loader-a!inline-loader-b!./someModule.js') // 使用 -! 前缀禁用配置中的pre loader和normal loader

执行顺序变成: image.png 在这个前提下如果 inline-loader-b 的 pitch 方法有返回值,那么顺序就会变成 image.png

loader-runner ​

Loader 的实现本质上是一个方法,输入一个模块后,在 webpack 内部由 loader-runner 负责组织和调用 loader。工作流如下: ​

image.png 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 引入,这样可以让使用你的包的开发者更好地管理依赖。

如有转载或 CV 的请标注本站原文地址