3. Webpack Loader 实战:从配置到自定义开发

Loader 是 Webpack 的灵魂。说白了,Webpack 只认识 JavaScript 和 JSON,其他一切资源——TypeScript、CSS、图片、字体——都得靠 Loader 转成它吃得下的模块。这一章我们就把几个最常用的 Loader 掰开揉碎,最后再手写一个自定义 Loader,让你彻底搞懂它的运作机制。

3.1 Loader 的本质:一个转换函数

Loader 就是一个函数,输入是源文件内容,输出是转换后的模块代码。Webpack 会按配置的规则,把匹配到的文件依次交给 Loader 链处理。

核心原则:Loader 的执行顺序是从右到左,从下到上。

举个例子:

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

处理 .css 文件时,Webpack 会先交给 css-loader(右边的),再交给 style-loader(左边的)。css-loader 解析 CSS 中的 @import 和 url(),style-loader 把样式注入到 <style> 标签里。

我的习惯:写配置时把执行顺序写在注释里,避免搞反。我曾经在项目里把顺序写反了,结果样式死活不生效,排查了半天才发现是 Loader 顺序问题。

3.2 babel-loader:JS 转译的标配

Babel 把 ES6+ 代码转成 ES5,让浏览器能跑。而 babel-loader 就是 Webpack 和 Babel 之间的桥梁。

基本配置

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

这里有个关键点:exclude: /node_modules/。为什么?因为第三方库通常已经转译过了,再转一遍纯属浪费性能。我见过新人没加这个排除,结果构建时间从 5 秒变成了 50 秒。

缓存加速

每次构建都重新转译所有 JS 文件?太慢了。加个缓存:

{
  loader: 'babel-loader',
  options: {
    cacheDirectory: true,
    presets: ['@babel/preset-env']
  }
}

cacheDirectory: true 会把转译结果缓存到 node_modules/.cache/babel-loader 目录。只有文件内容变了才会重新转译。我在大项目里用这个配置,二次构建时间直接砍半。

避坑指南

我曾经踩过的坑:在 monorepo 项目里,babel-loader 默认只转译当前包的文件。如果引用了同仓库的其他包,那些包的代码不会被转译。解决方案是配置 include 或使用 @babel/preset-env 的 modules: false 配合 Webpack 的 resolve.symlinks: false。

3.3 css-loader + style-loader:样式模块化

这两个 Loader 通常成对出现。一个解析 CSS,一个注入样式。

css-loader 的核心能力

  • 解析 @import:把 CSS 文件中的 @import 语句解析成模块依赖
  • 处理 url():把 CSS 中的图片、字体路径解析成 Webpack 能处理的模块
  • CSS Modules:启用局部作用域,避免样式冲突
{
  loader: 'css-loader',
  options: {
    modules: {
      localIdentName: '[name]__[local]--[hash:base64:5]'
    },
    importLoaders: 1
  }
}

importLoaders: 1 的意思是:在 css-loader 处理之前,先让后面的 Loader(比如 postcss-loader)处理 @import 进来的文件。这个参数很容易被忽略,但少了它,@import 进来的文件可能不会被正确转译。

style-loader 的工作方式

style-loader 会把 CSS 内容通过 document.createElement('style') 注入到页面中。开发环境下很方便,但生产环境通常用 MiniCssExtractPlugin.loader 替代,把 CSS 抽成独立文件。

我的建议:开发环境用 style-loader(热更新快),生产环境用 MiniCssExtractPlugin.loader(缓存友好)。我一般会在 Webpack 配置里根据 process.env.NODE_ENV 动态切换。

3.4 file-loader 与 url-loader:静态资源处理

图片、字体这些静态资源,Webpack 没法直接处理。需要 Loader 把它们复制到输出目录,并返回对应的 URL。

file-loader:原样输出

{
  test: /\.(png|jpg|gif)$/,
  use: {
    loader: 'file-loader',
    options: {
      name: 'images/[name].[hash:8].[ext]'
    }
  }
}

它会将文件复制到 dist/images 目录,文件名带上 hash 用于缓存控制。返回的 URL 就是最终的文件路径。

url-loader:小文件内联

{
  test: /\.(png|jpg|gif)$/,
  use: {
    loader: 'url-loader',
    options: {
      limit: 8192,  // 8KB 以下转 base64
      fallback: 'file-loader',
      name: 'images/[name].[hash:8].[ext]'
    }
  }
}

小于 limit 的文件会被转成 base64 字符串直接嵌入 JS 中,减少 HTTP 请求。大于 limit 的则交给 file-loader 处理。

注意:Webpack 5 已经内置了资源模块(Asset Modules),可以替代 file-loader 和 url-loader。如果你在用 Webpack 5,建议直接用内置方案,少装一个依赖就少一个维护成本。

3.5 Loader 执行流程全景图

下面这张图展示了 Webpack 处理 CSS 文件时,Loader 链的完整执行流程:

Loader 链执行流程(以 CSS 为例) 源文件 style.css css-loader 解析 @import、url()、CSS Modules style-loader 注入 <style> 标签到页面 Webpack 模块 执行顺序 从右到左,从下到上 实际执行:css-loader → style-loader 配置顺序:style-loader → css-loader 注意 每个 Loader 的输出 是下一个 Loader 的输入

3.6 自定义 Loader 开发

搞懂了原理,我们自己写一个 Loader 试试。假设我们要写一个 remove-console-loader,用来移除代码中的 console.log 语句。

Loader 的基本结构

// remove-console-loader.js
module.exports = function(source) {
  // source 是源文件内容
  // this 是 Webpack 提供的 Loader 上下文
  const result = source.replace(/console\.log\([^)]*\);?/g, '');
  return result;
};

就这么简单?嗯,基本功能确实就这几行。但实际开发中,我们通常需要处理更多情况。

支持配置选项

module.exports = function(source) {
  const options = this.getOptions();  // 获取配置参数
  const patterns = options.patterns || ['log', 'warn', 'error'];

  let result = source;
  patterns.forEach(method => {
    const regex = new RegExp(`console\\.${method}\\([^)]*\\);?`, 'g');
    result = result.replace(regex, '');
  });

  return result;
};

配置方式:

{
  test: /\.js$/,
  use: {
    loader: 'remove-console-loader',
    options: {
      patterns: ['log', 'warn']  // 只移除 console.log 和 console.warn
    }
  }
}

处理异步场景

如果 Loader 需要异步操作(比如读取文件、请求网络),可以用 this.async():

module.exports = function(source) {
  const callback = this.async();
  
  // 模拟异步处理
  setTimeout(() => {
    const result = source.replace(/console\.log\([^)]*\);?/g, '');
    callback(null, result);
  }, 100);
};

返回多个结果

有时候 Loader 需要返回多个文件。比如 file-loader 复制文件后,还要返回文件路径。这时可以用 this.emitFile:

module.exports = function(source) {
  const filename = `images/${Date.now()}.png`;
  this.emitFile(filename, source);  // 输出文件
  return `module.exports = "${filename}"`;  // 返回模块代码
};

我的经验:写自定义 Loader 时,一定要处理边界情况。比如源文件是空文件怎么办?正则匹配不到怎么办?我写过的一个 Loader 因为没处理空文件,导致构建时报错,排查了半天才发现是正则匹配返回了 null。

3.7 Loader 配置对比表

Loader 核心作用 常用配置 注意事项
babel-loader JS 转译(ES6+ → ES5) presets、cacheDirectory 排除 node_modules,注意 monorepo 场景
css-loader 解析 CSS 依赖关系 modules、importLoaders importLoaders 容易被忽略
style-loader 注入 <style> 标签 无特殊配置 生产环境建议用 MiniCssExtractPlugin
file-loader 复制文件到输出目录 name、outputPath Webpack 5 可用 Asset Modules 替代
url-loader 小文件转 base64 内联 limit、fallback 注意 base64 会增加 JS 体积

3.8 实战建议

  • Loader 顺序:写配置时从右往左读,确保理解执行流程
  • 性能优化:善用 include、exclude 缩小处理范围,开启缓存
  • 自定义 Loader:从简单的字符串替换开始,逐步增加功能
  • 调试技巧:在 Loader 里加 console.log 看 source 内容,或者用 this.debug 获取调试信息

一句话总结:Loader 就是 Webpack 的翻译官,把各种资源翻译成它认识的模块。理解了这一点,配置和开发 Loader 就不再神秘。