loader 本质上是导出为函数的 JavaScript 模块。loader runner 会调用此函数,然后将上一个 loader 产生的结果或者资源文件传入进去。函数中的 this 作为上下文会被 webpack 填充,正因为此,所有loader函数不能是箭头函数。

如果是单个处理结果,可以在 同步模式 中直接返回。如果有多个处理结果,则必须调用 this.callback()。在 异步模式 中,必须调用 this.async() 来告知 loader runner 等待异步结果,它会返回 this.callback() 回调函数。随后 loader 必须返回 undefined 并且调用该回调函数。这里先简单介绍下this.callback函数:

# this.callback

可以同步或者异步调用的并返回多个结果的函数。预期的参数是:

/*
第一个参数必须是 Error 或者 null
第二个参数是一个 string 或者 Buffer。
可选的:第三个参数必须是一个可以被 this module 解析的 source map。
可选的:第四个参数,会被 webpack 忽略,可以是任何东西(例如一些元数据)。
*/
this.callback(
  err: Error | null,
  content: string | Buffer,
  sourceMap?: SourceMap,
  meta?: any
);

一个loader方法可以接收三个参数。起始 loader 只有一个入参:资源文件的内容。compiler 预期得到最后一个 loader 产生的处理结果。这个处理结果应该为 String 或者 Buffer(能够被转换为 string)类型,代表了模块的 JavaScript 源码。

/**
 *
 * @param {string|Buffer} content 源文件的内容
 * @param {object} [map] 可以被 https://github.com/mozilla/source-map 使用的 SourceMap 数据
 * @param {any} [meta] meta 数据,可以是任何内容
 */
function webpackLoader(content, map, meta) {
  // 你的 webpack loader 代码
}

# 编写同步loader

有了上面的基础知识,下面我们开始来编写一个简单的loader来实现字符串大写转小写。

loaders/lower-case-loader.js

module.exports = function (source) {
  return source.toLowerCase()
}

src/index.js

'FANGFEIYUE'

下面我们来使用我们编写的loader,resolveLoader仅用于配置解析 webpack 的 loader 包,可以为 loader 设置独立的解析规则。

webpack.config.js

const path = require('path')
module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules:[
      {
        test: /\.m?js$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'lower-case-loader'
        }
      }
    ]
  },
  resolveLoader: {
    // 配置别名,可以通过配置别名的方式来替换初始模块路径
    // alias: {
    //   "lower-case-loader": path.resolve(__dirname,'./loaders/lower-case-loader.js')
    // }

    // 告诉 webpack 解析模块时应该搜索的目录, 前面的目录优先于 node_modules
    modules: [path.resovle(__dirname, './loaders'), 'node_modules']
  }
};

查看打包后的结果:

dist/main.js

/*
 * ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
 * This devtool is neither made for production nor for readable output files.
 * It uses "eval()" calls to create a separate source file in the browser devtools.
 * If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
 * or disable the default devtool with "devtool: false".
 * If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
 */
/******/ (() => { // webpackBootstrap
/******/ 	var __webpack_modules__ = ({

/***/ "./src/index.js":
/*!**********************!*\
  !*** ./src/index.js ***!
  \**********************/
/***/ (() => {

eval("'fangfeiyue'\n\n//# sourceURL=webpack://loader/./src/index.js?");

/***/ })

/******/ 	});
/************************************************************************/
/******/ 	
/******/ 	// startup
/******/ 	// Load entry module and return exports
/******/ 	// This entry module can't be inlined because the eval devtool is used.
/******/ 	var __webpack_exports__ = {};
/******/ 	__webpack_modules__["./src/index.js"]();
/******/ 	
/******/ })()
;

# 编写异步loader

在 异步模式 中,必须调用 this.async() 来告知 loader runner 等待异步结果,它会返回 this.callback() 回调函数。随后 loader 必须返回 undefined 并且调用该回调函数。

loaders/lower-case-loader.js

module.exports = function (source) {
  const callback = this.async()
  setTimeout(() => {
    callback(null, source.toLowerCase())
  }, 1000)
}

# 向loader中传入参数

在平常的开发中,我们可以使用options来给loader配置参数,那我在我们自己实现的loader中如何给loader传递配置参数呢?

webpack.config.js

//...
module: {
  rules:[
    {
      test: /\.m?js$/,
      use: {
        loader: 'lower-case-loader',
        options: {
          content: '123'
        }
      }
    }
  ]
}
//...

webpack5之前可以使用loader-utils

const loaderUtils = require('loader-utils');
module.exports = function(source) {
  const opt = loaderUtils.getOptions(this) || {};
  console.log(opt) // { content: '123' }
  return source.toLowerCase()
}

从 webpack 5 开始,this.getOptions 可以获取到 loader 上下文对象。它用来替代来自 loader-utils 中的 getOptions 方法

module.exports = function (source) {
  const opt = this.getOptions() || {}
  console.log(opt) // { content: '123' }
  return source.toLowerCase()
}

参考:

Loader Interface (opens new window)

手把手教你撸一个 Webpack Loader (opens new window)