基于本文回答

播面 播面

文解图解播客视频,多维讲透八股文
0
评论

如何编写一个自定义的 Loader?处理同步/异步 Loader 的 API 是什么?

在 Webpack 中,Loader 本质上是一个导出的 JavaScript 函数。它接收输入的源文件内容,对其进行转换,然后返回转换后的结果。

下面将详细介绍如何编写自定义 Loader,以及处理同步和异步 Loader 的核心 API。


一、 编写 Loader 的基本结构

一个最简单的 Loader 结构如下:

javascript
// my-loader.js
module.exports = function (content, map, meta) {
  // content: 上一个 loader 产生的内容(字符串或 Buffer)
  // map: 可选,上一个 loader 产生的 SourceMap
  // meta: 可选,上一个 loader 产生的自定义元数据

  // 对 content 进行处理...
  const result = content.replace(/console\.log\(.*?\);?/g, '');

  // 返回处理后的内容
  return result;
};

二、 同步 Loader(Synchronous Loaders)

如果你的 Loader 转换过程是同步的,有两种方式返回结果:

1. 直接 return

适合只需要返回转换后的字符串/Buffer,不需要返回 SourceMap 的简单场景。

javascript
module.exports = function (content) {
  const transformed = content + '\n/* append by my-loader */';
  return transformed; // 直接 return
};

2. 使用 this.callback() API

当你需要返回多个结果(例如同时返回转换后的代码、SourceMap、AST 或 Meta 信息)时,必须使用 this.callback

  • API 签名
    this.callback(err: Error | null, content: string | Buffer, sourceMap?: SourceMap, meta?: any)
javascript
module.exports = function (content, map, meta) {
  const transformedContent = content.toUpperCase();
  
  // 使用 this.callback 返回多个值
  this.callback(
    null,               // error: 如果有错误则传入 Error 对象,没有传 null
    transformedContent, // content: 处理后的代码
    map,                // sourceMap: 继续传递 SourceMap
    meta                // meta: 继续传递元数据
  );

  // 注意:使用 this.callback 时,函数内部不要再 return 内容,直接 return 即可(或者不写 return)
  return; 
};

三、 异步 Loader(Asynchronous Loaders)

如果 Loader 内部包含异步操作(例如文件读取、网络请求、定时器、复杂计算等),绝对不能直接 return 或直接调用 this.callback,否则 Webpack 会在异步操作完成前就认为 Loader 执行结束了。

核心 API:this.async()

  1. 调用 this.async() 告诉 Webpack“这是一个异步 Loader”。
  2. this.async() 会返回一个与 this.callback 参数相同的回调函数。
  3. 在异步操作完成后,调用该回调函数。
javascript
module.exports = function (content, map, meta) {
  // 1. 告诉 Webpack 这是异步 Loader,并获取回调函数
  const callback = this.async();

  // 2. 执行异步操作
  someAsyncOperation(content)
    ? .then(result => {
        // 3. 异步成功,将结果传给 callback
        // 参数:callback(err, content, map, meta)
        callback(null, result.content, result.map);
      })
    : .catch(err => {
        // 4. 异步失败,将错误传给 callback
        callback(err);
      });
};

四、 常用 Loader 内置 API (this 上下文)

Webpack 为 Loader 函数的 this 上下文绑定了许多实用的 API(注意:Loader 不能写成箭头函数,否则会丢失 this):

API 说明
this.getOptions() (Webpack 5 推荐) 获取在 webpack.config.js 中给该 Loader 配置的 options 参数。
this.async() 标记 Loader 为异步,并返回 callback 函数。
this.callback() 同步 Loader 中用于返回多个结果。
this.addDependency(filepath) 添加文件依赖。当该文件发生变化时,Webpack 会重新触发重新编译(用于开发模式)。
this.emitFile(name, content) 直接输出一个新文件到构建目录(常用于处理图片、字体等静态资源)。
this.cacheable(boolean) 设置是否缓存 Loader 的处理结果(Webpack 默认开启)。
this.resourcePath 当前正在处理的文件的绝对路径。

五、 实战演练:编写一个带 Options 的异步 Banner Loader

这个 Loader 会在代码顶部异步注入一段自定义的注释文本(Banner)。

1. 编写 Loader (src/loaders/async-banner-loader.js)

javascript
module.exports = function (content, map, meta) {
  // 1. 获取 webpack.config.js 中配置的 options
  const options = this.getOptions() || {};
  const author = options.author || 'Anonymous';

  // 2. 声明异步
  const callback = this.async();

  // 3. 模拟异步操作(如:向服务器发送请求或进行耗时计算)
  setTimeout(() => {
    const prefix = `/**\n * Author: ${author}\n * Date: ${new Date().toISOString()}\n */\n\n`;
    const result = prefix + content;

    // 4. 完成异步处理并返回结果
    callback(null, result, map, meta);
  }, 1000);
};

2. 在 Webpack 中配置并使用它 (webpack.config.js)

javascript
const path = require('path');

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            // 使用自定义 Loader
            loader: path.resolve(__dirname, 'src/loaders/async-banner-loader.js'),
            options: {
              author: 'Developer High',
            },
          },
        ],
      },
    ],
  },
  // 优化:如果你不想写冗长的 path.resolve,可以配置 resolveLoader
  resolveLoader: {
    modules: ['node_modules', path.resolve(__dirname, 'src/loaders')],
  },
};

总结 Cheat Sheet

  • 同步简洁版return newContent;
  • 同步全能版this.callback(null, newContent, map, meta);
  • 异步标准版
    javascript
    const callback = this.async();
    asyncTask().then(res => callback(null, res));