是否写过 Loader?简单描述一下编写 loader 的思路?
问题解析
Loader 是 Webpack 的核心概念之一,用于对模块的源代码进行转换。理解如何编写 loader 是深入掌握 Webpack 的重要一步。
核心概念
Loader 的特点
| 特性 | 说明 |
|---|---|
| 链式调用 | 多个 loader 可以串联使用 |
| 单一职责 | 每个 loader 只做一件事 |
| Node.js 环境 | 运行在 Node.js 中,可调用任意 Node API |
| 无状态 | 不保留状态,确保可复用 |
| 异步优先 | 尽可能使用异步方式 |
Loader 的两种模式
| 模式 | 说明 | 使用场景 |
|---|---|---|
| Normal | 默认模式,处理 UTF-8 字符串 | 文本文件处理 |
| Raw | 处理二进制数据(Buffer) | 图片、字体等文件 |
详细解答
最简单的 Loader 示例
// my-loader.js
module.exports = function(source) {
// source: 模块源代码字符串
// 对 source 进行处理
const result = source.replace(/console\.log\(.*\);?/g, '');
// 返回处理后的代码
return result;
};
异步 Loader
// async-loader.js
module.exports = function(source) {
// 声明异步操作
const callback = this.async();
// 模拟异步操作
setTimeout(() => {
const result = source.toUpperCase();
// 第一个参数:错误对象(无错误则为 null)
// 第二个参数:处理后的源代码
// 第三个参数:source map(可选)
// 第四个参数:附加数据(可选)
callback(null, result);
}, 100);
};
处理二进制文件的 Loader
// raw-loader.js
module.exports = function(source) {
// 当 raw = true 时,source 是 Buffer
// 可以对图片、字体等二进制文件进行处理
const size = source.length;
const base64 = source.toString('base64');
return `module.exports = "data:image/png;base64,${base64}";`;
};
// 声明处理原始数据
module.exports.raw = true;
使用工具库编写 Loader
// 使用 loader-utils 和 schema-utils
const { getOptions } = require('loader-utils');
const { validate } = require('schema-utils');
// 定义选项的 JSON Schema
const schema = {
type: 'object',
properties: {
text: {
type: 'string'
},
transform: {
type: 'string',
enum: ['uppercase', 'lowercase', 'capitalize']
}
},
additionalProperties: false
};
module.exports = function(source) {
// 获取 loader 选项
const options = getOptions(this) || {};
// 验证选项
validate(schema, options, {
name: 'My Loader',
baseDataPath: 'options'
});
const { text = '', transform = 'uppercase' } = options;
let result = source;
// 根据选项转换
switch (transform) {
case 'uppercase':
result = source.toUpperCase();
break;
case 'lowercase':
result = source.toLowerCase();
break;
case 'capitalize':
result = source.replace(/\b\w/g, c => c.toUpperCase());
break;
}
// 添加自定义文本
result = `${text}\n${result}`;
return result;
};
深入理解
Loader 的 Pitch 阶段
// pitch-loader.js
module.exports = function(source) {
console.log('Normal 阶段');
return source;
};
// Pitch 阶段:在 loader 链中从左到右执行
module.exports.pitch = function(remainingRequest, precedingRequest, data) {
console.log('Pitch 阶段');
console.log('Remaining request:', remainingRequest);
console.log('Preceding request:', precedingRequest);
// 可以在这里提前返回,中断 loader 链
// return 'module.exports = "pitched";';
};
Loader 上下文(this)
module.exports = function(source) {
// this 指向 loader 上下文
// 获取 loader 选项
const options = this.getOptions();
// 声明异步
const callback = this.async();
// 添加依赖文件(用于 watch 模式)
this.addDependency('/path/to/dependency');
// 发出警告
this.emitWarning(new Error('Warning message'));
// 发出错误
this.emitError(new Error('Error message'));
// 解析路径
const resolvedPath = this.resolve('/context', './request');
// 加载模块
this.loadModule('./module', (err, source, sourceMap, module) => {
// ...
});
// 获取资源文件信息
const resourcePath = this.resourcePath;
const resourceQuery = this.resourceQuery;
return source;
};
完整的 Loader 示例
// replace-loader.js
const { getOptions } = require('loader-utils');
const { validate } = require('schema-utils');
const schema = {
type: 'object',
properties: {
search: {
type: 'string'
},
replace: {
type: 'string'
},
flags: {
type: 'string'
}
},
required: ['search', 'replace']
};
module.exports = function(source) {
const options = getOptions(this) || {};
validate(schema, options, {
name: 'Replace Loader'
});
const { search, replace, flags = 'g' } = options;
// 支持异步
const callback = this.async();
try {
const regex = new RegExp(search, flags);
const result = source.replace(regex, replace);
// 生成 source map(如果需要)
if (this.sourceMap) {
// 使用 magic-string 等库生成 source map
callback(null, result, map);
} else {
callback(null, result);
}
} catch (error) {
callback(error);
}
};
最佳实践
编写 Loader 的原则
// 1. 单一职责原则
// 好的做法:一个 loader 只做一件事
// markdown-loader.js
const marked = require('marked');
module.exports = function(source) {
const html = marked.parse(source);
return `module.exports = ${JSON.stringify(html)};`;
};
// 2. 链式组合
// 使用多个 loader 组合完成复杂任务
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.md$/,
use: [
'html-loader', // 处理 HTML
'./markdown-loader' // 转换 Markdown
]
}
]
}
};
// 3. 无状态设计
// 不要在 loader 中保留状态
// 好的做法
module.exports = function(source) {
// 每次调用都是独立的
return source.replace(/foo/g, 'bar');
};
// 不好的做法
let count = 0; // 不要在 loader 外部定义状态
module.exports = function(source) {
count++;
return source;
};
加载本地 Loader
// 方法 1:使用绝对路径
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: path.resolve(__dirname, './loaders/my-loader.js')
}
]
}
};
// 方法 2:使用 resolveLoader
module.exports = {
resolveLoader: {
// 添加本地 loader 目录
modules: ['node_modules', path.resolve(__dirname, 'loaders')]
},
module: {
rules: [
{
test: /\.js$/,
use: 'my-loader' // 直接写 loader 名
}
]
}
};
// 方法 3:使用 npm link(开发时)
// cd my-loader
// npm link
// cd my-project
// npm link my-loader
实用的 Loader 示例
Markdown Loader
// markdown-loader.js
const marked = require('marked');
const hljs = require('highlight.js');
module.exports = function(source) {
const callback = this.async();
// 配置 marked
marked.setOptions({
highlight: (code, lang) => {
if (lang && hljs.getLanguage(lang)) {
return hljs.highlight(code, { language: lang }).value;
}
return hljs.highlightAuto(code).value;
}
});
const html = marked.parse(source);
// 返回 HTML 字符串
callback(null, `module.exports = ${JSON.stringify(html)};`);
};
YAML Loader
// yaml-loader.js
const yaml = require('js-yaml');
module.exports = function(source) {
try {
const data = yaml.load(source);
return `module.exports = ${JSON.stringify(data)};`;
} catch (error) {
this.emitError(error);
return 'module.exports = {};';
}
};
SVG Sprite Loader
// svg-sprite-loader.js
const { parse } = require('svg-parser');
module.exports = function(source) {
const callback = this.async();
const options = this.getOptions() || {};
try {
const parsed = parse(source);
const svgContent = JSON.stringify(source);
const code = `
var svg = ${svgContent};
if (typeof document !== 'undefined') {
var div = document.createElement('div');
div.innerHTML = svg;
var svgEl = div.querySelector('svg');
svgEl.style.position = 'absolute';
svgEl.style.width = 0;
svgEl.style.height = 0;
document.body.appendChild(div);
}
module.exports = svg;
`;
callback(null, code);
} catch (error) {
callback(error);
}
};
面试要点
-
Loader 的特点:
- 链式调用,单一职责
- 运行在 Node.js 环境
- 默认处理 UTF-8,raw=true 处理二进制
- 无状态,尽可能异步化
-
编写 Loader 的基本结构:
module.exports = function(source) { // 处理 source return result; }; -
异步 Loader:
const callback = this.async(); callback(null, result, sourceMap); -
使用工具库:
loader-utils:获取选项、解析请求等schema-utils:验证选项配置
-
加载本地 Loader:
- 绝对路径引用
resolveLoader.modules配置npm link链接本地包
-
Loader 的 Pitch 阶段:
- 在 loader 链中从左到右执行
- 可以中断 loader 链
-
注意事项:
- 保持无状态
- 尽可能异步
- 使用
this.async()处理异步操作 - 使用
this.emitError()报告错误