自定义Loader开发指南
什么是自定义Loader
Webpack的Loader本质上是导出函数的JavaScript模块。这个函数接收源文件内容作为输入,经过处理后返回新的内容。Loader的执行顺序是从右到左或从下到上,支持链式调用。
module.exports = function(source) {
// 对source进行处理
return transformedSource;
};
Loader的基本结构
一个完整的Loader通常包含以下部分:
- 导出一个函数
- 接收source、sourceMap和meta作为参数
- 返回处理后的内容
module.exports = function(source, sourceMap, meta) {
// 处理逻辑
this.callback(null, transformedSource, sourceMap, meta);
// 或者直接return
// return transformedSource;
};
Loader的上下文
Loader函数中的this指向一个Loader上下文对象,提供了许多实用方法:
module.exports = function(source) {
// 获取Loader配置选项
const options = this.getOptions();
// 添加依赖
this.addDependency(this.resourcePath + '.dep');
// 缓存支持
if (this.cacheable) {
this.cacheable();
}
// 异步回调
const callback = this.async();
// 发出警告
this.emitWarning(new Error('This is a warning'));
return source;
};
同步与异步Loader
Loader可以是同步或异步的:
// 同步Loader
module.exports = function(source) {
return source.replace(/foo/g, 'bar');
};
// 异步Loader
module.exports = function(source) {
const callback = this.async();
someAsyncOperation(source, (err, result) => {
if (err) return callback(err);
callback(null, result);
});
};
处理二进制数据
对于非文本文件,需要设置raw属性为true:
module.exports = function(source) {
// source现在是Buffer
return source;
};
module.exports.raw = true;
Loader的链式调用
Loader可以串联使用,前一个Loader的输出是后一个Loader的输入:
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.txt$/,
use: [
'uppercase-loader',
'reverse-loader'
]
}
]
}
};
常用Loader开发模式
1. 转换型Loader
module.exports = function(source) {
return `export default ${JSON.stringify(source)}`;
};
2. 校验型Loader
module.exports = function(source) {
if (source.includes('TODO')) {
this.emitError('TODO comments are not allowed');
}
return source;
};
3. 组合型Loader
const marked = require('marked');
module.exports = function(source) {
const html = marked(source);
return `module.exports = ${JSON.stringify(html)}`;
};
高级Loader技巧
1. 获取Loader选项
const { getOptions } = require('loader-utils');
module.exports = function(source) {
const options = getOptions(this) || {};
// 使用options配置处理source
};
2. 生成Source Map
const { SourceMapGenerator } = require('source-map');
module.exports = function(source, sourceMap) {
const map = new SourceMapGenerator();
map.setSourceContent('input.js', source);
map.addMapping({
source: 'input.js',
original: { line: 1, column: 0 },
generated: { line: 1, column: 0 }
});
this.callback(null, source, map.toString());
};
3. 缓存与构建
module.exports = function(source) {
this.cacheable && this.cacheable();
const key = 'my-loader:' + this.resourcePath;
const cached = this.cache && this.cache.get(key);
if (cached) {
return cached;
}
const result = expensiveOperation(source);
this.cache && this.cache.set(key, result);
return result;
};
测试Loader
编写单元测试确保Loader行为符合预期:
const myLoader = require('./my-loader');
const { runLoaders } = require('loader-runner');
runLoaders({
resource: '/path/to/file.txt',
loaders: [path.resolve(__dirname, './my-loader')],
context: {
emitWarning: (warning) => console.warn(warning)
},
readResource: fs.readFile.bind(fs)
}, (err, result) => {
if (err) throw err;
console.log(result.result[0]); // 处理后的内容
});
性能优化建议
- 避免不必要的处理
- 使用缓存
- 最小化AST操作
- 使用worker线程处理CPU密集型任务
const { Worker } = require('worker_threads');
module.exports = function(source) {
const callback = this.async();
const worker = new Worker(require.resolve('./worker.js'), {
workerData: { source }
});
worker.on('message', (result) => {
callback(null, result);
});
worker.on('error', callback);
};
发布Loader
- 遵循命名规范:
xxx-loader
- 提供清晰的文档
- 包含完整的测试用例
- 指定peerDependencies
{
"name": "my-custom-loader",
"version": "1.0.0",
"peerDependencies": {
"webpack": "^5.0.0"
}
}
实际案例:Markdown转Vue组件
const marked = require('marked');
const hljs = require('highlight.js');
marked.setOptions({
highlight: (code, lang) => {
return hljs.highlight(lang, code).value;
}
});
module.exports = function(source) {
const content = marked(source);
return `
<template>
<div class="markdown">${content}</div>
</template>
<script>
export default {
name: 'MarkdownContent'
}
</script>
<style>
.markdown {
/* 样式 */
}
</style>
`;
};
调试Loader
- 使用
debugger
语句 - 配置Webpack devtool
- 使用Node.js调试器
node --inspect-brk ./node_modules/webpack/bin/webpack.js
Loader与Plugin的配合
Loader可以与Plugin协同工作:
// loader.js
module.exports = function(source) {
if (this.myPluginData) {
source = source.replace(/__PLUGIN_DATA__/g, this.myPluginData);
}
return source;
};
// plugin.js
class MyPlugin {
apply(compiler) {
compiler.hooks.compilation.tap('MyPlugin', (compilation) => {
compilation.hooks.normalModuleLoader.tap('MyPlugin', (loaderContext) => {
loaderContext.myPluginData = 'Hello from plugin';
});
});
}
}
处理资源文件
Loader可以处理各种资源文件:
const sharp = require('sharp');
module.exports = function(source) {
const callback = this.async();
sharp(source)
.resize(800, 600)
.toBuffer()
.then(data => {
callback(null, data);
})
.catch(callback);
};
module.exports.raw = true;
国际化Loader示例
const i18n = require('i18n');
module.exports = function(source) {
const lang = this.query.lang || 'en';
i18n.setLocale(lang);
return source.replace(/\$t\(([^)]+)\)/g, (match, key) => {
return i18n.__(key.trim());
});
};
安全注意事项
- 避免eval等不安全操作
- 处理用户输入要谨慎
- 限制资源访问范围
// 不安全的Loader示例
module.exports = function(source) {
return eval(source); // 绝对不要这样做
};
性能监控
可以在Loader中添加性能监控:
module.exports = function(source) {
const start = Date.now();
// 处理逻辑
const duration = Date.now() - start;
this.emitFile('loader-timing.json',
JSON.stringify({ [this.resourcePath]: duration }));
return source;
};
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
下一篇:Plugin与Loader的区别