编写自定义 Loader
约 1191 字大约 4 分钟
webpackLoader自定义
2026-04-15
Loader 基础概念
什么是 Loader
Loader 是 Webpack 的转换器,用于将各种类型的文件转换为 Webpack 能够识别的模块。
核心特点:
- Loader 本质上是一个函数
- 接收源文件内容作为输入
- 返回转换后的内容
- 支持链式调用(从右到左执行)
Loader 的执行顺序
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader']
}
]
}
// 执行顺序:postcss-loader → css-loader → style-loaderpitch 机制
Loader 的 pitch 阶段先于正常阶段执行:
// loader pitch 执行顺序
// pitch 从左到右,normal 从右到左
module.exports = function(source) {
console.log('normal:', this.resource);
return source;
};
module.exports.pitch = function() {
console.log('pitch:', this.resource);
};Loader 开发 API
同步 Loader
最简单的 Loader 形式:
// my-loader.js
module.exports = function(source) {
// source 是文件内容(字符串)
// 可以做一些转换
const result = source.replace(/old/g, 'new');
return result;
};异步 Loader
处理耗时操作时使用异步:
module.exports = function(source) {
const callback = this.async();
// 异步处理
setTimeout(() => {
const result = source.replace(/old/g, 'new');
callback(null, result);
}, 1000);
};Raw Loader
默认情况下,Loader 接收的是 UTF-8 字符串。如果需要处理二进制:
module.exports = function(source) {
// source 是 Buffer
console.log(source instanceof Buffer); // true
return source;
};
module.exports.raw = true;使用 loader-utils
const loaderUtils = require('loader-utils');
module.exports = function(source) {
// 获取配置参数
const options = loaderUtils.getOptions(this);
// 生成缓存标识
const cacheKey = loaderUtils.getHashDigest(source);
// 插值处理
const filename = loaderUtils.interpolateName(this, '[name].[hash].[ext]', {
content: source
});
return source;
};实战:手写 style-loader
style-loader 的作用是将 CSS 通过 <style> 标签注入到 DOM 中。
原理分析
// style-loader 做的事情
const css = 'body { color: red; }';
const style = document.createElement('style');
style.innerHTML = css;
document.head.appendChild(style);完整实现
// style-loader.js
module.exports = function(source) {
// 返回运行时代码
return `
const style = document.createElement('style');
style.innerHTML = ${JSON.stringify(source)};
document.head.appendChild(style);
`;
};
// 支持热更新
module.exports.pitch = function() {
if (module.hot) {
module.hot.accept();
}
};更完善的实现
// style-loader.js
const loaderUtils = require('loader-utils');
module.exports = function(source) {
const options = loaderUtils.getOptions(this) || {};
const insertAt = options.insertAt || 'head';
return `
var style = document.createElement('style');
style.innerHTML = ${JSON.stringify(source)};
if (${JSON.stringify(insertAt)} === 'head') {
document.head.appendChild(style);
} else {
document.body.appendChild(style);
}
// 热更新支持
if (module.hot) {
module.hot.accept();
module.hot.dispose(function() {
style.parentNode.removeChild(style);
});
}
`;
};实战:手写 css-loader
css-loader 的作用是解析 CSS 中的 @import 和 url()。
处理 @import
// css-loader.js
const postcss = require('postcss');
const postcssImport = require('postcss-import');
module.exports = async function(source) {
const callback = this.async();
// 使用 postcss 处理 @import
const result = await postcss([postcssImport])
.process(source, { from: this.resourcePath });
callback(null, result.css);
};
module.exports.raw = false;处理 url()
// css-loader.js - 处理 url()
const path = require('path');
module.exports = function(source) {
// 匹配 url() 中的路径
const urlReg = /url\\((['"]?)([^'")]*)\\1\\)/g;
const result = source.replace(urlReg, (match, quote, url) => {
// 将 url 转换为 require
const absolutePath = path.resolve(path.dirname(this.resourcePath), url);
const relativePath = path.relative(this.rootContext, absolutePath);
return `url(require('./${relativePath}'))`;
});
return `
var css = ${JSON.stringify(result)};
module.exports = css;
`;
};实战:手写 less-loader
less-loader 的作用是将 Less 编译为 CSS。
基础实现
// less-loader.js
const less = require('less');
const loaderUtils = require('loader-utils');
module.exports = async function(source) {
const callback = this.async();
const options = loaderUtils.getOptions(this) || {};
try {
const result = await less.render(source, {
filename: this.resourcePath,
paths: [path.dirname(this.resourcePath)],
sourceMap: options.sourceMap
});
// 将依赖的 less 文件添加到监听
result.imports.forEach(file => {
this.addDependency(file);
});
callback(null, result.css);
} catch (err) {
callback(err);
}
};支持 Source Map
module.exports = async function(source, inputSourceMap) {
const callback = this.async();
const options = loaderUtils.getOptions(this) || {};
try {
const result = await less.render(source, {
filename: this.resourcePath,
sourceMap: options.sourceMap ? {} : false
});
// 传递 source map
if (result.map) {
callback(null, result.css, JSON.parse(result.map));
} else {
callback(null, result.css);
}
} catch (err) {
callback(err);
}
};Loader 测试和调试
使用 loader-runner 测试
// test-loader.js
const { runLoaders } = require('loader-runner');
const path = require('path');
runLoaders({
resource: path.resolve(__dirname, 'test.css'),
loaders: [
path.resolve(__dirname, 'style-loader.js'),
path.resolve(__dirname, 'css-loader.js')
],
readResource: fs.readFile.bind(fs)
}, (err, result) => {
if (err) {
console.error('Loader 执行失败:', err);
return;
}
console.log('结果:', result.result[0]);
});调试技巧
添加 console.log
module.exports = function(source) { console.log('输入:', source); const result = transform(source); console.log('输出:', result); return result; };使用 VSCode 断点
// 在关键位置添加 debugger debugger;查看依赖关系
this.addDependency(file); // 添加文件监听 this.emitFile(filename, content); // 输出文件
Loader 最佳实践
1. 单一职责
每个 Loader 只做一件事:
// 好的做法
module.exports = function(source) {
return transform(source);
};
// 不好的做法
module.exports = function(source) {
let result = transform1(source);
result = transform2(result);
result = transform3(result);
return result;
};2. 链式调用
让 Loader 可以链式调用:
// 确保返回字符串或 Buffer
module.exports = function(source) {
this.callback(null, transformedSource, sourceMap);
return undefined;
};3. 模块化
使用 loader-utils 获取参数:
const loaderUtils = require('loader-utils');
module.exports = function(source) {
const options = loaderUtils.getOptions(this);
// 使用 options
};4. 缓存
标记 Loader 可缓存:
module.exports = function(source) {
this.cacheable(true); // 启用缓存
return source;
};5. 错误处理
完善的错误处理:
module.exports = function(source) {
try {
const result = transform(source);
return result;
} catch (err) {
this.emitError(err);
return source;
}
};常见 Loader 列表
| Loader | 作用 | 安装 |
|---|---|---|
| style-loader | 注入 CSS 到 DOM | npm i style-loader |
| css-loader | 解析 @import/url() | npm i css-loader |
| less-loader | 编译 Less | npm i less-loader less |
| babel-loader | 编译 ES6+ | npm i babel-loader @babel/core |
| ts-loader | 编译 TypeScript | npm i ts-loader typescript |
| file-loader | 处理文件 | npm i file-loader |
| url-loader | 文件转 base64 | npm i url-loader |
总结
Loader 是 Webpack 的核心机制,掌握 Loader 开发可以:
- 处理任意类型的文件
- 实现自定义编译逻辑
- 优化构建流程
- 提高开发效率
