写 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 在代码层面就是一个导出为函数的模块:
// 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/JSX | babel-loader / swc-loader | 语法转译、JSX 编译 |
| TS | ts-loader / babel-loader | 编译 TypeScript |
| CSS | css-loader + style-loader | 解析 CSS、注入页面 |
| Less/Sass | less-loader / sass-loader | 编译预处理器 |
| 样式预处理 | postcss-loader | 自动前缀、新语法 |
| Vue | vue-loader | 解析单文件组件 |
| 资源(旧) | file-loader / url-loader | 输出/内联资源 |
| 通用转换 | 自定义 loader | 处理特定文本、DSL |
五、怎样配置 loader
loader 配置在 module.rules 中,核心字段是 test(匹配文件)和 use(使用哪些 loader)。
1. 数组写法
module.exports = {
module: {
rules: [
{
test: /\.css$/i,
// 从右到左:先 css-loader,再 style-loader
use: ['style-loader', 'css-loader'],
},
],
},
};
2. 对象写法(需要传参时)
{
test: /\.m?js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
},
},
}
3. 用 enforce 控制顺序
{
test: /\.js$/,
enforce: 'pre', // 'pre' | 'post'
use: 'eslint-loader',
}
pre 在最前执行,post 在最后执行。
4. 内联 loader
也可以直接在代码里指定 loaders:
import data from 'raw-loader!./data.txt';
5. 限定范围
用 include / exclude 避免对不必要的文件(尤其是 node_modules)执行 loader。
六、loader 和 plugin 的区别
这是最容易混淆的一对概念:
| loader | plugin | |
|---|---|---|
| 作用对象 | 单个文件/模块 | 整个构建流程 |
| 职责 | 转换源码 | 扩展构建能力 |
| 形态 | 函数 | 带 apply 方法的类/函数 |
| 配置位置 | module.rules | plugins |
| 典型例子 | babel-loader、css-loader | HtmlWebpackPlugin、MiniCssExtractPlugin |
记忆方式:
loader 负责「把模块变成另一个模块」;plugin 负责「在构建的各个阶段做额外的事」。
七、自己写一个 loader
理解了本质,写 loader 并不难。
1. 同步 loader
// loaders/replace-loader.js
module.exports = function (source) {
const options = this.getOptions() || {};
return source.replace(/__NAME__/g, options.name || 'world');
};
2. 异步 loader
有些操作(读文件、请求接口)是异步的,用 this.async() 获取回调:
module.exports = function (source) {
const callback = this.async();
setTimeout(() => {
callback(null, source + '\n// processed async');
}, 100);
};
3. 接收二进制内容
默认 source 是字符串。要处理图片等二进制,声明 raw:
module.exports = function (source) {
// source 是 Buffer
return source;
};
module.exports.raw = true;
4. 在配置中使用
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 更容易复用和测试。
八、注意事项
- 顺序不能乱:
use数组从右到左执行,写反了结果就错了。 - 范围要收窄:用
include/exclude避免对node_modules全量执行,否则构建会非常慢。 - loader 应该是纯转换:尽量不产生副作用(写文件、改全局),副作用交给 plugin。
- 尽量小且单一:一个 loader 解决一个问题,便于组合与维护。
- 注意缓存:loader 结果会被 webpack 和工具自身缓存,调试时若改了不生效,先清缓存。
小结
| 问题 | 答案 |
|---|---|
| loader 是什么 | 把文件转成 webpack 能处理模块的转换器 |
| 本质 | 一个接收 source、返回新内容的函数 |
| 为什么需要 | webpack 只认识 JS 和 JSON |
| 怎么工作 | 按文件匹配规则,链式从右到左执行 |
| 和 plugin 的区别 | loader 转换模块,plugin 扩展流程 |
一句话:loader 是 webpack 的「翻译官 + 加工厂」——把各种它看不懂的文件翻译成 JS 模块,并在翻译过程中完成转译、编译、解析等加工。 理解了这一点,module.rules 里的每一行配置就不再是玄学。