创见博客
webpack loader 是什么:从原理到自定义
七崽爱吃小饼干2026/09/17阅读 0专栏 webpack原理

写 webpack 配置时,module.rules 里几乎全是 loader:babel-loader、css-loader、ts-loader……但很多人只是照着抄,并不清楚 loader 到底是什么、怎么工作。本文从「为什么需要它」讲起,把 loader 的本质和作用讲透。

一、为什么需要 loader

webpack 的本质是模块打包器:从入口出发,递归地分析模块之间的依赖,最后把整个依赖图打包成浏览器可用的产物。

但它有一个前提——webpack 只认识 JavaScript 和 JSON。这是刻在它设计里的:模块解析、依赖分析、作用域处理,全都建立在 JS 语法之上。

于是问题来了:项目里除了 JS,还有一堆东西——

  • <div> 这样的 JSX
  • .ts、.vue 这样的特殊语法
  • .css、.less、.scss 这样的样式
  • .png、.svg、.woff2 这样的资源

webpack 遇到它们会直接报错,因为它「看不懂」。

loader 就是用来填补这个缺口的:它把这些 webpack 不认识的文件,转换成 webpack 能处理的模块。

二、loader 是什么

一句话定义:

loader 是一个「源码转换器」,它把某个文件的内容作为输入,经过转换后输出 webpack 能处理的模块。

它有几个关键性质:

1. 它就是一个函数

一个 loader 在代码层面就是一个导出为函数的模块:

js
// my-loader.js
module.exports = function (source) {
  // source 是上一个 loader 的输出,或文件的原始内容
  return transformedSource;
};
  • 输入的 source 默认是字符串(文件内容)。
  • 返回的也应是字符串,通常是 JS 代码。
  • 它可以接收配置参数(通过 options)。

2. 它是链式执行的

多个 loader 可以串成一条链,前一个的输出是后一个的输入:

源文件 → loader A → loader B → loader C → JS 模块

这解释了为什么处理 CSS 需要两个 loader:css-loader 负责把 CSS 解析成模块,style-loader 负责把它注入页面,前者是后者的输入。

3. 它按「文件」工作

loader 是模块级的:每当 webpack 解析到一个文件,就为它匹配对应的 loader 进行处理。这一点和 plugin 不同(见后文)。

4. 它最终要输出 JS

因为 webpack 的模块系统建立在 JS 上,所以 loader 通常要把内容变成 JS。例如 css-loader 会把 CSS 变成一段导出字符串的 JS 代码,这样它才能进入依赖图、被其他模块引用。

补充:webpack 5 引入了 asset modules,图片、字体等资源可以不再依赖 loader,直接由内置能力处理。但需要「转换逻辑」的场合(编译、转译、解析 DSL)依然离不开 loader。

三、loader 的工作流程

一个非 JS 文件从被引用到进入产物,大致经历这几步:

1. 某个模块里 import './style.css'
        ↓
2. webpack 在 module.rules 中查找匹配 .css 的规则
        ↓
3. 根据规则组装出 loader 链,如 [style-loader, css-loader]
        ↓
4. 从右到左执行:css-loader 先处理,style-loader 再处理
        ↓
5. 输出 JS 模块,加入依赖图
        ↓
6. 最终打进 bundle

其中最关键的一点是 loader 链从右到左执行:写在 use 数组最右边的先执行,它的输出交给左边,直到最左边的 loader 产出最终结果。

四、常见 loader 一览

文件类型常用 loader作用
JS/JSXbabel-loader / swc-loader语法转译、JSX 编译
TSts-loader / babel-loader编译 TypeScript
CSScss-loader + style-loader解析 CSS、注入页面
Less/Sassless-loader / sass-loader编译预处理器
样式预处理postcss-loader自动前缀、新语法
Vuevue-loader解析单文件组件
资源(旧)file-loader / url-loader输出/内联资源
通用转换自定义 loader处理特定文本、DSL

五、怎样配置 loader

loader 配置在 module.rules 中,核心字段是 test(匹配文件)和 use(使用哪些 loader)。

1. 数组写法

js
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/i,
        // 从右到左:先 css-loader,再 style-loader
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
};

2. 对象写法(需要传参时)

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

3. 用 enforce 控制顺序

js
{
  test: /\.js$/,
  enforce: 'pre',   // 'pre' | 'post'
  use: 'eslint-loader',
}

pre 在最前执行,post 在最后执行。

4. 内联 loader

也可以直接在代码里指定 loaders:

js
import data from 'raw-loader!./data.txt';

5. 限定范围

用 include / exclude 避免对不必要的文件(尤其是 node_modules)执行 loader。

六、loader 和 plugin 的区别

这是最容易混淆的一对概念:

loaderplugin
作用对象单个文件/模块整个构建流程
职责转换源码扩展构建能力
形态函数带 apply 方法的类/函数
配置位置module.rulesplugins
典型例子babel-loader、css-loaderHtmlWebpackPlugin、MiniCssExtractPlugin

记忆方式:

loader 负责「把模块变成另一个模块」;plugin 负责「在构建的各个阶段做额外的事」。

七、自己写一个 loader

理解了本质,写 loader 并不难。

1. 同步 loader

js
// loaders/replace-loader.js
module.exports = function (source) {
  const options = this.getOptions() || {};
  return source.replace(/__NAME__/g, options.name || 'world');
};

2. 异步 loader

有些操作(读文件、请求接口)是异步的,用 this.async() 获取回调:

js
module.exports = function (source) {
  const callback = this.async();
  setTimeout(() => {
    callback(null, source + '\n// processed async');
  }, 100);
};

3. 接收二进制内容

默认 source 是字符串。要处理图片等二进制,声明 raw:

js
module.exports = function (source) {
  // source 是 Buffer
  return source;
};
module.exports.raw = true;

4. 在配置中使用

js
const path = require('path');

module.exports = {
  module: {
    rules: [
      {
        test: /\.txt$/,
        use: [
          {
            loader: path.resolve(__dirname, 'loaders/replace-loader.js'),
            options: { name: 'webpack' },
          },
        ],
      },
    ],
  },
};

一个 loader 通常只做一件事,需要多步处理时,就拆成多个 loader 串起来——这既是社区约定,也让 loader 更容易复用和测试。

八、注意事项

  1. 顺序不能乱:use 数组从右到左执行,写反了结果就错了。
  2. 范围要收窄:用 include / exclude 避免对 node_modules 全量执行,否则构建会非常慢。
  3. loader 应该是纯转换:尽量不产生副作用(写文件、改全局),副作用交给 plugin。
  4. 尽量小且单一:一个 loader 解决一个问题,便于组合与维护。
  5. 注意缓存:loader 结果会被 webpack 和工具自身缓存,调试时若改了不生效,先清缓存。

小结

问题答案
loader 是什么把文件转成 webpack 能处理模块的转换器
本质一个接收 source、返回新内容的函数
为什么需要webpack 只认识 JS 和 JSON
怎么工作按文件匹配规则,链式从右到左执行
和 plugin 的区别loader 转换模块,plugin 扩展流程

一句话:loader 是 webpack 的「翻译官 + 加工厂」——把各种它看不懂的文件翻译成 JS 模块,并在翻译过程中完成转译、编译、解析等加工。 理解了这一点,module.rules 里的每一行配置就不再是玄学。

评论
0/100