如何编写一个 Loader
官方说明文档:
- https://webpack.docschina.org/contribute/writing-a-loader/
- https://webpack.docschina.org/api/loaders/
loader 只是一个导出为函数的 JavaScript 模块。该函数在 loader 转换资源的时候调用。给定的函数将调用 loader API,并通过 this 上下文访问。loader runner 会调用这个函数,然后把上一个 loader 产生的结果或者资源文件(resource file)传入进去。函数的 this 上下文将由 webpack 填充。
注意:loader就是一个函数,不能写成箭头函数module.exports = () => {},而用声明式module.exports = function(){},否则this指向会有问题
简单案例:编写loader实现将world替换为camille:
//1、index.js:
console.log('hello world');
//2、webpack.common.js:
module: {
rules: [
{
test: /\.js$/,
//include: path.resolve(__dirname, '../src'),
loader: path.resolve(__dirname, '../loaders/replaceLoader.js')
}
]
}
//3、replaceLoader.js
module.exports = function(source) { //source引入文件的源代码(内容)
console.log('-------', source);
return source.replace('world', 'camille'); //return source;
}
输出如下:
1、loader添加配置
获取配置有两种方式:
1)this.query:loader使用时配置options,处理的loader通过this.query获取这个options对象(注意上面loader定义要使用函数式)。
- 如果这个 loader 配置了 options 对象的话,this.query 就指向这个 option 对象。
- 如果 loader 中没有 options,而是以 query 字符串作为参数调用时,this.query 就是一个以 ? 开头的字符串。
2)loader-utils:除了this.query获取options对象,官方还推荐使用 loader-utils 中提供的 getOptions 方法 来提取给定 loader 的 options。
//1、webpack.commmon.js:
{
test: /\.js$/,
loader: path.resolve(__dirname, '../loaders/replaceLoader.js'),
options: {
name: "Camille"
}
}
//2、replaceLoader.js
//1)this.query
module.exports = function(source) {
console.log('-------', this.query);
return source.replace('world', this.query.name);
}
//2)、loader-utils
//npm install loader-utils --save-dev
const loaderUtils = require("loader-utils");
module.exports = function(source) {
const options = loaderUtils.getOptions(this);
console.log('-------', options);
return source.replace('world', options.name);
}
输出如下:
2、this.callback:一个可以同步或者异步调用的可以返回多个结果的函数。
return只能返回一个资源,有的时候可能会把sourcemap或其他信息一并返回。即单个处理结果,可以在同步模式中直接return返回。如果有多个处理结果,则必须调用 this.callback()。
this.callback(
err: Error | null, //第一个参数必须是 Error 或者 null
content: string | Buffer, //第二个参数是一个 string 或者 Buffer。
sourceMap?: SourceMap, //可选的:第三个参数必须是一个可以被这个模块解析的 source map
meta?: any //可选的:第四个选项,会被 webpack 忽略,可以是任何东西(例如一些元数据)
)
module.exports = function(content, map, meta) {
this.callback(null, someSyncOperation(content), map, meta);
return; // 当调用 callback() 时总是返回 undefined
};
修改上面的案例:
//replaceLoader.js:
const loaderUtils = require("loader-utils");
module.exports = function(source) {
const options = loaderUtils.getOptions(this);
const result = source.replace('world', options.name);
console.log("-----result-----", result);
this.callback(null, result); // return source.replace('world', options.name);
}
3、this.async
修改上面的案例并改名为replaceLoaderAsync.js:
//replaceLoaderAsync.js:
const loaderUtils = require("loader-utils");
module.exports = function(source) {
const options = loaderUtils.getOptions(this);
setTimeout(() => {
const result = source.replace('dell', options.name);
this.callback(null, result); //return result;
}, 1000)
}
运行结果直接报错:
解决:在异步模式中,必须调用 this.async(),来指示 loader runner 等待异步结果,它会返回 this.callback() 回调函数--使用 this.async 来获取 callback 函数。
this.async: 告诉 loader-runner 这个 loader 将会异步地回调。返回 this.callback。
const loaderUtils = require("loader-utils");
module.exports = function(source) {
const options = loaderUtils.getOptions(this);
const callback = this.async();
setTimeout(() => {
const result = source.replace('world', options.name);
console.log("-----result-----", result);
callback(null, result);
}, 1000)
}
上面只是1s,打包时间如下:
修改上面异步的时间为5000,打包所耗用时间如下:
几乎是5倍,即使用this.async()会指示 loader runner 等待异步结果。
4、多个loader顺序问题:
同时运用以上两个loader:同步、异步loader
use: [
{
loader: path.resolve(__dirname, "../loaders/replaceLoader.js"),
options: {
name: "Camille"
}
},
{
loader: path.resolve(__dirname, "../loaders/replaceLoaderAsync.js"),
options: {
name: "Vina"
}
}
]
看最终输出如下:异步loader处理完,将处理后的结果交给了同步的loader,同步loader接收source是上一个loader处理后的而不是最初的
优化:以上loader引入路径多处重复,匹配(test)多个 loaders,可以使用 resolveLoader.modules 配置,webpack 将会从这些目录中搜索这些 loaders。
module.exports = {
resolveLoader: {
modules: ["node_modules", "./loaders"] //注意:优先级从前到后
},
module: {
rules: [
{
test: /\.js$/,
include: path.resolve(__dirname, "../src"),
use: [
{
loader: "replaceLoader",
options: {
name: "camille"
}
},
{
loader: "replaceLoaderAsync",
options: {
name: "vina"
}
}
]
}
]
}
}
5、自定义loader使用场景:
1、不需要修改业务代码,就可以添加try catch错误捕获,如下loader处理的业务代码中遇到function就给添加try catch包裹
module.exports = function () {
try {
function func() {
/**.... */
}
} catch (e) {}
};
2、中英文切换,如下获取全局变量来获取语言,从而用相应内容替换占位符: