嵘么么

18 天前

玩转webpack之loader开发

本文作者:IMWeb 嵘么么 原文出处:IMWeb社区 未经同意,禁止转载

前言

webpack提倡一切皆模块,所有类型的文件都可以经过文件加载器处理变成我们可加载的模块,那么这个文件加载器便是loader。

那么我们如何开发一个webpack loader呢,让我们一起探索探索吧~

一、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开发

那么如何来开发一个loader呢?让我们慢慢来揭开webpack loader 的什么面纱~

loader其实是一个导出为函数的 JavaScript模块,是不是看起来很简单?实际呢,loader开发也很简单。即

test.loader.js内容如下:

module.exports = function(content) {
    return transform(content);  // 对content进行处理并返回给webpack
}

1、loader的传入参数

既然我们说了所谓 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的。

2、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 最后调用,期望值是传出 JavaScript和 source map(可选)。
  • 中间的 loader 执行时,会传入前一个 loader 传出的结果。

3、获取用户自定义参数

到这里基本已经清楚了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
}

4、控制loader执行

前面讲到过,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 的开发,敬请期待~

0条评论

    您需要 注册 一个IMWeb账号或者 才能进行评论。