Custom Loader Development Guide
What is a Custom Loader
Webpack's Loader is essentially a JavaScript module that exports a function. This function takes the source file content as input, processes it, and returns new content. Loaders are executed from right to left or bottom to top, supporting chained calls.
module.exports = function(source) {
// Process the source
return transformedSource;
};
Basic Structure of a Loader
A complete Loader typically consists of the following parts:
- Export a function
- Accept
source
,sourceMap
, andmeta
as parameters - Return the processed content
module.exports = function(source, sourceMap, meta) {
// Processing logic
this.callback(null, transformedSource, sourceMap, meta);
// Or return directly
// return transformedSource;
};
Loader Context
The this
inside the Loader function points to a Loader context object, providing many utility methods:
module.exports = function(source) {
// Get Loader configuration options
const options = this.getOptions();
// Add dependencies
this.addDependency(this.resourcePath + '.dep');
// Cache support
if (this.cacheable) {
this.cacheable();
}
// Asynchronous callback
const callback = this.async();
// Emit warnings
this.emitWarning(new Error('This is a warning'));
return source;
};
Synchronous and Asynchronous Loaders
Loaders can be synchronous or asynchronous:
// Synchronous Loader
module.exports = function(source) {
return source.replace(/foo/g, 'bar');
};
// Asynchronous Loader
module.exports = function(source) {
const callback = this.async();
someAsyncOperation(source, (err, result) => {
if (err) return callback(err);
callback(null, result);
});
};
Handling Binary Data
For non-text files, set the raw
property to true
:
module.exports = function(source) {
// source is now a Buffer
return source;
};
module.exports.raw = true;
Chaining Loaders
Loaders can be chained, where the output of one Loader becomes the input of the next:
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.txt$/,
use: [
'uppercase-loader',
'reverse-loader'
]
}
]
}
};
Common Loader Development Patterns
1. Transformative Loader
module.exports = function(source) {
return `export default ${JSON.stringify(source)}`;
};
2. Validation Loader
module.exports = function(source) {
if (source.includes('TODO')) {
this.emitError('TODO comments are not allowed');
}
return source;
};
3. Composite Loader
const marked = require('marked');
module.exports = function(source) {
const html = marked(source);
return `module.exports = ${JSON.stringify(html)}`;
};
Advanced Loader Techniques
1. Getting Loader Options
const { getOptions } = require('loader-utils');
module.exports = function(source) {
const options = getOptions(this) || {};
// Use options to process source
};
2. Generating Source Maps
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. Caching and Building
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;
};
Testing Loaders
Write unit tests to ensure Loader behavior meets expectations:
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]); // Processed content
});
Performance Optimization Tips
- Avoid unnecessary processing
- Use caching
- Minimize AST operations
- Use worker threads for CPU-intensive tasks
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);
};
Publishing a Loader
- Follow naming conventions:
xxx-loader
- Provide clear documentation
- Include complete test cases
- Specify peerDependencies
{
"name": "my-custom-loader",
"version": "1.0.0",
"peerDependencies": {
"webpack": "^5.0.0"
}
}
Practical Example: Markdown to Vue Component
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 {
/* Styles */
}
</style>
`;
};
Debugging Loaders
- Use
debugger
statements - Configure Webpack devtool
- Use Node.js debugger
node --inspect-brk ./node_modules/webpack/bin/webpack.js
Collaboration Between Loaders and Plugins
Loaders can work with Plugins:
// 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';
});
});
}
}
Handling Resource Files
Loaders can process various resource files:
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;
Internationalization Loader Example
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());
});
};
Security Considerations
- Avoid unsafe operations like
eval
- Handle user input with caution
- Limit resource access scope
// Unsafe Loader example
module.exports = function(source) {
return eval(source); // Never do this
};
Performance Monitoring
Add performance monitoring to Loaders:
module.exports = function(source) {
const start = Date.now();
// Processing logic
const duration = Date.now() - start;
this.emitFile('loader-timing.json',
JSON.stringify({ [this.resourcePath]: duration }));
return source;
};
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
下一篇:Plugin与Loader的区别