返回首页

在实际工程中,配置文件上百行乃是常事,如何保证各个 loader 按照预想方式工作?

问题解析

在复杂的 Webpack 配置中,loader 的执行顺序至关重要。当配置文件达到上百行时,确保 loader 按照预期顺序执行是工程化实践中的关键问题。

核心概念

Loader 执行顺序

默认情况下,loader 按照从右到左、从下到上的顺序执行:

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['loader-a', 'loader-b', 'loader-c']
        // 执行顺序:loader-c -> loader-b -> loader-a
      }
    ]
  }
};

Enforce 强制执行顺序

类型 执行时机 说明
pre 在所有普通 loader 之前 最先执行
normal 默认 按配置顺序
inline 内联 官方不推荐
post 在所有普通 loader 之后 最后执行

详细解答

使用 enforce 控制 loader 顺序

module.exports = {
  module: {
    rules: [
      // 1. Pre loader:最先执行
      {
        test: /\.js$/,
        enforce: 'pre',  // 在所有普通 loader 之前执行
        use: 'eslint-loader'
      },

      // 2. Normal loader:默认顺序
      {
        test: /\.js$/,
        use: ['babel-loader', 'cache-loader']
        // 执行顺序:cache-loader -> babel-loader
      },

      // 3. Post loader:最后执行
      {
        test: /\.js$/,
        enforce: 'post',  // 在所有普通 loader 之后执行
        use: 'some-post-loader'
      }
    ]
  }
};

实际应用场景

场景 1:ESLint 必须在 Babel 之前执行

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        enforce: 'pre',  // 确保 ESLint 先执行,检查原始代码
        exclude: /node_modules/,
        use: {
          loader: 'eslint-loader',
          options: {
            fix: true
          }
        }
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: 'babel-loader'  // 后执行,转换代码
      }
    ]
  }
};

场景 2:Source Map 处理

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        enforce: 'pre',  // 先添加 source map 支持
        use: 'source-map-loader'
      },
      {
        test: /\.js$/,
        use: 'babel-loader'  // 后转换代码
      }
    ]
  }
};

场景 3:PostCSS 和 CSS 处理

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        enforce: 'post',  // 最后执行,添加浏览器前缀等
        use: 'postcss-loader'
      },
      {
        test: /\.css$/,
        use: [
          'style-loader',  // 最后(从右到左)
          'css-loader'     // 最先
        ]
      }
    ]
  }
};

深入理解

Loader 的四种类型详解

// Webpack 内部将 loader 分为四类,按优先级排序:
// pre -> normal -> inline -> post

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        enforce: 'pre',
        use: 'loader-a'  // 第 1 个执行
      },
      {
        test: /\.js$/,
        use: 'loader-b'  // 第 2 个执行(normal)
      },
      {
        test: /\.js$/,
        enforce: 'post',
        use: 'loader-c'  // 第 3 个执行
      }
    ]
  }
};

Inline Loader(不推荐)

// 在代码中直接指定 loader(官方不推荐)
import Styles from 'style-loader!css-loader?modules!./styles.css';

// 使用 !! 前缀禁用其他 loader
import Styles from '!!css-loader!./styles.css';

// 使用 ! 前缀禁用 normal loader
import Styles from '!css-loader!./styles.css';

不推荐原因

  • 破坏配置文件的单一职责
  • 难以维护和追踪
  • 与配置文件中的规则可能冲突

Loader 的 pitch 阶段

// loader 有两个阶段:pitch 和 normal
// pitch 阶段:从左到右执行
// normal 阶段:从右到左执行

module.exports = {
  // pitch 阶段(从左到右)
  pitch: function(remainingRequest, precedingRequest, data) {
    console.log('pitch 阶段');
  },

  // normal 阶段(从右到左)
  default: function(content) {
    console.log('normal 阶段');
    return content;
  }
};

最佳实践

大型项目的 loader 组织策略

// 将 loader 配置按功能拆分
const jsLoaders = [
  {
    test: /\.js$/,
    enforce: 'pre',
    use: 'eslint-loader'
  },
  {
    test: /\.js$/,
    exclude: /node_modules/,
    use: {
      loader: 'babel-loader',
      options: {
        cacheDirectory: true
      }
    }
  }
];

const cssLoaders = [
  {
    test: /\.css$/,
    use: [
      'style-loader',
      {
        loader: 'css-loader',
        options: { importLoaders: 1 }
      },
      'postcss-loader'
    ]
  }
];

const imageLoaders = [
  {
    test: /\.(png|jpe?g|gif)$/,
    type: 'asset/resource'
  }
];

module.exports = {
  module: {
    rules: [...jsLoaders, ...cssLoaders, ...imageLoaders]
  }
};

使用 oneOf 优化匹配性能

module.exports = {
  module: {
    rules: [
      {
        // 使用 oneOf,匹配到一个后不再继续匹配
        oneOf: [
          {
            test: /\.js$/,
            use: 'babel-loader'
          },
          {
            test: /\.css$/,
            use: ['style-loader', 'css-loader']
          },
          {
            test: /\.(png|jpe?g|gif)$/,
            type: 'asset/resource'
          }
        ]
      }
    ]
  }
};

完整的 loader 配置示例

module.exports = {
  module: {
    rules: [
      // ========== Pre Loaders ==========
      {
        test: /\.js$/,
        enforce: 'pre',
        exclude: /node_modules/,
        use: {
          loader: 'eslint-loader',
          options: {
            cache: true,
            fix: true
          }
        }
      },

      // ========== Normal Loaders ==========
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'cache-loader'  // 缓存 babel 编译结果
          },
          {
            loader: 'thread-loader',  // 多线程编译
            options: {
              workers: 2
            }
          },
          {
            loader: 'babel-loader',
            options: {
              cacheDirectory: true
            }
          }
        ]
      },
      {
        test: /\.css$/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1,
              modules: true
            }
          },
          {
            loader: 'postcss-loader',
            options: {
              postcssOptions: {
                plugins: ['autoprefixer']
              }
            }
          }
        ]
      },

      // ========== Post Loaders ==========
      {
        test: /\.js$/,
        enforce: 'post',
        use: 'some-custom-loader'
      }
    ]
  }
};

面试要点

  1. 默认执行顺序:从右到左、从下到上

  2. enforce 的三种值

    • pre:在所有普通 loader 之前执行
    • post:在所有普通 loader 之后执行
    • 默认(normal):按配置顺序
  3. inline loader 不推荐:破坏配置一致性,难以维护

  4. 实际应用场景

    • ESLint 使用 pre 确保在 Babel 之前检查原始代码
    • Source map loader 使用 pre 确保先处理 source map
    • PostCSS 通常放在 CSS loader 链的最后
  5. 性能优化:使用 oneOf 避免不必要的匹配,使用 cache-loader 缓存结果