# webpack loader

# 1. 介绍

# 1.1. 概念

帮助 webpack 将不同类型的文件转换为 webpack 可识别的模块

参考:

# 1.2. 分类

category desc
pre 前置 loader
normal 普通 loader
inline 内联 loader
post 后置 loader

# 1.3. 执行顺序

说明:

  • 相同类别的 loader 执行顺序: 从右到左,从下到上
  • 不同类别的 loader 执行顺序: pre > normal > inline > post

示例1:

// 此时 loader 执行顺序:loader3 - loader2 - loader1
module.exports = {
  module: {
    rules: [
      { 
        test: /\.css$/, 
        use: [ 'loader1', 'loader2', 'loader3' ]
      },
    ],
  },
};

示例2:

// 此时 loader 执行顺序:loader3 - loader2 - loader1
module.exports = {
  module: {
    rules: [
      { test: /\.jsx$/, loader: 'loader1' },
      { test: /\.css$/, loader: 'loader2' },
      { test: /\.jpg$/, loader: 'loader3' },
    ],
  },
};

示例3:

// 此时 loader 执行顺序:loader1 - loader2 - loader3
module.exports = {
  module: {
    rules: [
      { test: /\.jsx$/, loader: 'loader1', enforce: 'pre' },  // 前置 loader
      { test: /\.css$/, loader: 'loader2' },                  // 普通 loader
      { test: /\.jpg$/, loader: 'loader3', enforce: 'post' }, // 后置 loader
    ],
  },
};

# 1.4. 使用方式

  • 配置方式,指定 pre、normal、post loader
  • 内联方式,指定 inline loader

# 1.4.1. 配置方式

说明:

  • 在 webpack.config.js 中配置 module.rules.Rule 即可

示例:

module.exports = {
  module: {
    rules: [
      {
        test: /\.jsx$/,
        loader: 'loader1',
        enforce: 'pre'
      },
    ],
  },
};

# 1.4.2. 内联方式

说明:

  • inline loader (内联 loader)
  • import 语句中指定用哪些 loader 来处理这个模块,指定的那些 loader 都是 inline loader

示例1. import 'style-loader!css-loader?modules!./styles.css';

  • 使用 style-loadercss-loader 处理 ./styles.css 模块
  • css-loader?modules 中的 ?modules 是 query string(查询字符串)
  • 各个部分(loader、资源路径)之间用 ! 分隔

示例2. import '!style-loader!css-loader?modules!./styles.css';

  • 打头的前缀 !,表示跳过 normal loader
  • 也就是说,跳过其它也能处理这个文件的 普通 loader (webpack.config.js 中配置的)

示例3. import '-!style-loader!css-loader?modules!./styles.css';

  • 打头的前缀 -!,表示跳过 pre、normal loader

示例4. import '!!style-loader!css-loader?modules!./styles.css';

  • 打头的前缀 !!,表示跳过 pre、normal、post loader

# 2. 第一个 loader

目录:

  • 项目目录

    proj/
      loaders/
        my-first-loader.js
      public/
        index.html
      src/
        main.js
      package.json
      webpack.config.js
    

配置:

  • webpack.config.js

    module.exports = {
      // ...
      module: {
        rules: [
          { test: /\.js$/, loader: './loaders/my-first-loader.js' }
        ],
      },
    }
    
  • my-first-loader.js:

    /**
     * @param {string|Buffer} content Content of the resource file
     * @param {object} map SourceMap data consumable by https://github.com/mozilla/source-map
     * @param {any} meta Meta data, could be anything, 别的 loader 传递的数据
     * @return {string}
     */
    function myFirstLoader(content, map, meta) {
      console.log('hello, my first loader!');
      return content;
    }
    
    module.exports = myFirstLoader;
    

参考:

# 3. loader 分类

  1. 同步 loader (Synchronous Loaders)
  2. 异步 loader (Asynchronous Loaders)
  3. "Raw" Loader
  4. Pitching Loader

# 3.1. 同步 loader

方式一:

module.exports = function (content, map, meta) {
  const transformedContent = someSyncOperation(content, map, meta)
  return transformedContent;
};

方式二:

module.exports = function (content, map, meta) {
  /*
    第一个参数:(err)         代表是否有错误
    第二个参数:(content)     处理后的内容
    第三个参数:(source-map)  继续传递 source-map
    第四个参数:(meta)        给下一个 loader 传递参数
  */
  this.callback(null, content, map, meta);
  return; // always return undefined when calling callback()
};

参考:loaders#synchronous-loaders (opens new window)

# 3.2. 异步 loader

module.exports = function (content, map, meta) {
  const callback = this.async();
  
  setTimeout(() => {
    const transformedContent = someSyncOperation(content, map, meta)
    
    // 参数列表与 this.callback 相同
    callback(null, transformedContent, map, meta);
  }, 1000)
};

参考:loaders/#asynchronous-loaders (opens new window)

# 3.3. "Raw" Loader

说明:

  • 可以使用异步、同步操作
  • 接收 Buffer 数据,通常用于处理媒体文件(图片等)

示例:

module.exports = function rawLoader(content) {
  assert(content instanceof Buffer);
  return someSyncOperation(content);
  // return value can be a `Buffer` too, This is also allowed if loader is not "raw"
};

// raw 属性也可以设置在函数对象(rawLoader)上
module.exports.raw = true;

参考:

# 3.4. Pitching Loader

说明:

  • 所有的 pitch 方法从左到右执行完后,再从右到左执行 loader,如下:

    loader-1 ~ pitch method
    loader-2 ~ pitch method
    loader-3 ~ pitch method
    loader-3
    loader-2
    loader-1
    
  • 熔断机制,一旦某个 pitch 方法有返回值,则该 pitch 所在 loader 及后续 loader 都不执行

    loader-1 ~ pitch method
    loader-2 ~ pitch method
    loader-1
    

示例:

/* webpack.config.js */
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['./pitch-loader-1.js', './pitch-loader-2.js', './pitch-loader-3.js']
      }
    ],
  },
}


/* pitch-loader-1.js */
module.exports = function (content) {
  console.log('loader-1')
  return content;
};
module.exports.pitch = function () {
  console.log('loader-1 ~ pitch method')
};


/* pitch-loader-2.js */
module.exports = function (content) {
  console.log('loader-2')
  return content;
};
module.exports.pitch = function () {
  console.log('loader-2 ~ pitch method')
  // return 'something';
};


/* pitch-loader-3.js */
module.exports = function (content) {
  console.log('loader-3')
  return content;
};
module.exports.pitch = function () {
  console.log('loader-3 ~ pitch method')
};

结果:

  • 正常情况

    • loader1.png

      loader-1 ~ pitch method
      loader-2 ~ pitch method
      loader-3 ~ pitch method
      loader-3
      loader-2
      loader-1
      
  • pitch-loader-2.js 中 pitch 函数有返回值,则会熔断:

    • loader2.png

      loader-1 ~ pitch method
      loader-2 ~ pitch method
      loader-1
      

参考:

# 4. loader API

The Loader Context: (this):

this.callback:

  • A function that can be called synchronously or asynchronously in order to return multiple results.

  • 参数

    this.callback(
      err: Error | null,
      content: string | Buffer,
      sourceMap?: SourceMap,
      meta?: any
    );
    
  • 示例

    module.exports = function(content, map, meta) {
      this.callback(null, content, map, meta);
    }
    
  • loaders/#thiscallback (opens new window)

this.async:

  • Tells the loader-runner that the loader intends to call back asynchronously. Returns this.callback.

  • 示例

    module.exports = function(content, map, meta) {
      const callback = this.async();
      setTimeout(() => {
        callback(null, content, map, meta);
      }, 2000)
    }
    
  • loaders/#thisasync (opens new window)

this.getOptions(schema?: JSONSchema):

  • Extracts given loader options. Optionally, accepts JSON schema as an argument.
  • Since webpack 5, this.getOptions is available in loader context. It substitutes getOptions method from loader-utils.
  • loaders/#thisgetoptionsschema (opens new window)

this.emitFile:

this.utils:

  • Access to contextify and absolutify utilities.

    • contextify: Return a new request string avoiding absolute paths when possible.(返回相对路径)
    • absolutify: Return a new request string using absolute paths when possible.(返回绝对路径)
  • 示例

    module.exports = function (content) {
      // 获取相对路径,相对 当前 loader 所在文件
      this.utils.contextify(
        this.context,
        this.utils.absolutify(this.context, './index.js')
      );
    
      this.utils.absolutify(this.context, this.resourcePath);
    
      // …
      return content;
    };
    
  • loaders/#thisutils (opens new window)

# 5. clean-log-loader

说明:

  • 自定义一个 loader ,用于清除文件中的 console.log(xxx); 语句

配置:

  • webpack.config.js:

    module.exports = {
      module: {
        rules: [
          { test: /\.js$/, loader: './loaders/clean-log-loader.js' },
        ],
      },
    }
    
  • clean-log-loader.js:

    module.exports = function (content, map, meta) {
      const transformedContent = content.replace(/console\.log\(.*\);?/g, '');
    
      this.callback(null, transformedContent, map, meta);
    }
    

说明:

  • 自定义一个 loader,用于给文件设置版权信息

配置:

  • webpack.config.js:

    module.exports = {
      module: {
        rules: [
          {
            test: /\.js$/,
            loader: './loaders/copyright-loader.js',
            options: {
              author: '吴钦飞',
            }
          },
        ],
      },
    }
    
  • copyright-loader.js:

    const schema = {
      type: 'object',
      properties: {
        author: {
          type: 'string'
        }
      },
      // 额外的属性
      additionalProperties: false
    };
    
    module.exports = function (content, map, meta) {
      const { author } = this.getOptions(schema);
    
      const copyright = `/* 当前代码归 ${author} 所有 */`;
    
      const transformedContent = `
        ${copyright}
        ${content}
      `;
    
      this.callback(null, transformedContent, map, meta);
    }
    

# 7. babel-loader

说明:

  • 自定义一个 loader,用于 ES6 转 ES5

配置:

  • webpack.config.js:

    module.exports = {
      module: {
        rules: [
          {
            test: /\.js$/,
            loader: './loaders/babel-loader.js',
            options: {
              presets: ['@babel/preset-env'],
            }
          },
        ],
      },
    }
    
  • babel-loader.js:

    const { transform } = require("@babel/core");
    
    const schema = {
      type: 'object',
      properties: {
        presets: {
          type: 'array'
        }
      },
      // 额外的属性
      additionalProperties: false
    };
    
    module.exports = function (content, map, meta) {
      const options = this.getOptions(schema);
      const callback = this.async();
    
      transform(content, options, function(err, result) {
        if (err) {
          callback(err);
          return;
        }
        const { code, map, ast } = result;
    
        callback(null, code, map, ast);
      });
    }
    

参考:

# 8. file-loader

说明:

  • 自定义一个 loader,用于处理图片资源

安装:

npm i -D loader-utils

配置:

  • webpack.config.js:

    module.exports = {
      module: {
        rules: [
          {
            test: /\.(png|jpe?g|gif)$/,
            loader: './loaders/file-loader.js',
            // webpack5 默认会处理 asset 资源,
            // 组织 Asset Module 处理 asset 资源
            type: 'javascript/auto',
          },
          { test: /\.css$/, use: [ 'style-loader', 'css-loader' ] },
        ],
      },
    }
    
  • file-loader.js

    const { interpolateName } = require('loader-utils');
    
    module.exports = function (buffer) {
      // 根据文件内容生成 hash,并格式化文件名
      const interpolatedName = interpolateName(
        this,
        `images/[name].[hash:8].[ext][query]`,
        { content: buffer }
      );
    
      // 生成文件
      this.emitFile(interpolatedName, buffer);
    
      // 暴露出去,交给 webpack 使用
      // 注意:使用 export default 会出问题
      const content = `module.exports = '${interpolatedName}'`
    
      return content;
    }
    
    module.exports.raw = true;
    
  • main.js:

    import './css/style.css';
    
  • style.css:

    .img-png {
      background-image: url(../images/1.png);
    }
    

参考:

# 9. style-loader

说明:

  • 自定义一个 loader,用于将 css 代码插入页面

配置:

  • webpack.config.js:

    module.exports = {
      module: {
        rules: [
          { 
            test: /\.less$/, 
            use: [ './loaders/style-loader.js', 'css-loader', 'less-loader' ] 
          },
        ],
      },
    }
    
  • style-loader.js

    const styleLoader = function () {};
    
    /*
      css-loader 处理后的结果是 JS 代码, 这些 JS 代码执行完毕后导出的才是 css 代码
    
      执行后续 loader 处理完的 JS 代码:
    
        import result from '!!css-loader!less-loader!./style.less'
     */
    styleLoader.pitch = function(remainingRequest) {
      /*
        W:\dev\blog\codes\frontend\webpack\guide\custom-loaders\node_modules\css-loader\dist\cjs.js
        !
        W:\dev\blog\codes\frontend\webpack\guide\custom-loaders\node_modules\less-loader\dist\cjs.js
        !
        W:\dev\blog\codes\frontend\webpack\guide\custom-loaders\src\css\style.less
       */
      console.log('remainingRequest:', remainingRequest)
    
      const relativeRequest = remainingRequest
        .split("!")
        .map((part) => this.utils.contextify(this.context, part))
        .join("!");
    
      /*
       ../../node_modules/css-loader/dist/cjs.js
       !
       ../../node_modules/less-loader/dist/cjs.js
       !
       ./style.less
       */
      console.log('relativeRequest:', relativeRequest);
    
      const script = `
        import css from "!!${relativeRequest}"
        const styleEl = document.createElement('style')
        styleEl.innerHTML = css
        document.head.appendChild(styleEl)
      `;
    
      return script;
    };
    
    
    module.exports = styleLoader;
    
  • main.js:

    import './css/style.less';
    
  • style.css:

    .img-png {
      background-image: url(../images/1.png);
    }
    

参考:

本章目录