loader

loader 是导出为一个函数的 node 模块。该函数在 loader 转换资源的时候调用。给定的函数将调用 loader API,并通过 this 上下文访问。

loader配置

{
test: /\.js$/
use: [
{
loader: path.resolve('path/to/loader.js'),
options: {/* ... */}
}
]
}

本地loader配置

resolveLoader: {
modules: [
'node_modules',
path.resolve(__dirname, 'loaders')
]
}

loader用法

//返回简单结果
module.exports = function(content){
return content
}

//返回多个值
module.exports = function(content){
this.callback(...)
}

//同步loader
module.exports = function(content){
this.callback(...)
}

//异步loader
module.exports = function(content){
let callback = this.async(...)
setTimeout(callback,1000)
}

loader 工具库

1.loader-utils 但最常用的一种工具是获取传递给 loader 的选项

2.schema-utils 用于保证 loader 选项,进行与 JSON Schema 结构一致的校验
import { getOptions } from 'loader-utils';
import validateOptions from 'schema-utils';

const schema = {
type: 'object',
properties: {
test: {
type: 'string'
}
}
}

export default function(source) {
const options = getOptions(this);

validateOptions(schema, options, 'Example Loader');

// 对资源应用一些转换……

return `export default ${ JSON.stringify(source) }`;
};

loader依赖

如果一个 loader 使用外部资源(例如,从文件系统读取),必须声明它。这些信息用于使缓存 loaders 无效,以及在观察模式(watch mode)下重编译。
import path from 'path';

export default function(source) {
var callback = this.async();
var headerPath = path.resolve('header.js');

this.addDependency(headerPath);

fs.readFile(headerPath, 'utf-8', function(err, header) {
if(err) return callback(err);
callback(null, header + "\n" + source);
});
};

模块依赖

根据模块类型,可能会有不同的模式指定依赖关系。例如在 CSS 中,使用 @import 和 url(...) 语句来声明依赖。这些依赖关系应该由模块系统解析。

可以通过以下两种方式中的一种来实现:

通过把它们转化成 require 语句。
使用 this.resolve 函数解析路径。
css-loader 是第一种方式的一个例子。它将 @import 语句替换为 require 其他样式文件,将 url(...) 替换为 require 引用文件,从而实现将依赖关系转化为 require 声明。

对于 less-loader,无法将每个 @import 转化为 require,因为所有 .less 的文件中的变量和混合跟踪必须一次编译。因此,less-loader 将 less 编译器进行了扩展,自定义路径解析逻辑。然后,利用第二种方式,通过 webpack 的 this.resolve 解析依赖。
loaderUtils.stringifyRequest(this,require.resolve('./xxx.js'))

loader API

方法名

含义

this.request

被解析出来的 request 字符串。例子:"/abc/loader1.js?xyz!/abc/node_modules/loader2/index.js!/abc/resource.js?rrr"

this.loaders

所有 loader 组成的数组。它在 pitch 阶段的时候是可以写入的。

this.loaderIndex

当前 loader 在 loader 数组中的索引。

this.async

异步回调

this.callback

回调

this.data

在 pitch 阶段和正常阶段之间共享的 data 对象。

this.cacheable

默认情况下,loader 的处理结果会被标记为可缓存。调用这个方法然后传入 false,可以关闭 loader 的缓存。cacheable(flag = true: boolean)

this.context

当前处理文件所在目录

this.resource

当前处理文件完成请求路径,例如 /src/main.js?name=1

this.resourcePath

当前处理文件的路径

this.resourceQuery

查询参数部分

this.target

webpack配置中的target

this.loadModule

但 Loader 在处理一个文件时,如果依赖其它文件的处理结果才能得出当前文件的结果时,就可以通过 this.loadModule(request: string, callback: function(err, source, sourceMap, module)) 去获得 request 对应文件的处理结果

this.resolve

解析指定文件路径

this.addDependency

给当前处理文件添加依赖文件,依赖发送变化时,会重新调用loader处理该文件

this.addContextDependency

把整个目录加入到当前正在处理文件的依赖当中

this.clearDependencies

清除当前正在处理文件的所有依赖中

this.emitFile

输出一个文件

loader-utils.stringifyRequest

把绝对路径转换成相对路径

loader-utils.interpolateName

用多个占位符或一个正则表达式转换一个文件名的模块。这个模板和正则表达式被设置为查询参数,在当前loader的上下文中被称为name或者regExp

loader原理

loader-runner

webpack-loader原理_ide

runLoaders({
resource: "/abs/path/to/file.txt?query",
// String: Absolute path to the resource (optionally including query string)

loaders: ["/abs/path/to/loader.js?query"],
// String[]: Absolute paths to the loaders (optionally including query string)
// {loader, options}[]: Absolute paths to the loaders with options object

context: { minimize: true },
// Additional loader context which is used as base context

readResource: fs.readFile.bind(fs)
// A function to read the resource
// Must have signature function(path, function(err, buffer))

}, function(err, result) {
// err: Error?

// result.result: Buffer | String
// The result

// result.resourceBuffer: Buffer
// The raw resource as Buffer (useful for SourceMaps)

// result.cacheable: Bool
// Is the result cacheable or do it require reexecution?

// result.fileDependencies: String[]
// An array of paths (files) on which the result depends on

// result.contextDependencies: String[]
// An array of paths (directories) on which the result depends on
})


function splitQuery(req) {
var i = req.indexOf("?");
if(i < 0) return [req, ""];
return [req.substr(0, i), req.substr(i)];
}

function dirname(path) {
if(path === "/") return "/";
var i = path.lastIndexOf("/");
var j = path.lastIndexOf("\\");
var i2 = path.indexOf("/");
var j2 = path.indexOf("\\");
var idx = i > j ? i : j;
var idx2 = i > j ? i2 : j2;
if(idx < 0) return path;
if(idx === idx2) return path.substr(0, idx + 1);
return path.substr(0, idx);
}


//loader开始执行阶段
function processResource(options, loaderContext, callback) {
// 将loader索引设置为最后一个loader
loaderContext.loaderIndex = loaderContext.loaders.length - 1;

var resourcePath = loaderContext.resourcePath
if(resourcePath) {
//添加文件依赖
loaderContext.addDependency(resourcePath);
//读取文件
options.readResource(resourcePath, function(err, buffer) {
if(err) return callback(err);
//读取完成后放入options
options.resourceBuffer = buffer;
iterateNormalLoaders(options, loaderContext, [buffer], callback);
});

} else {
iterateNormalLoaders(options, loaderContext, [null], callback);
}
}

//从右往左递归执行loader
function iterateNormalLoaders(options, loaderContext, args, callback) {
//结束条件,loader读取完毕
if(loaderContext.loaderIndex < 0)
return callback(null, args);

var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];

//迭代
if(currentLoaderObject.normalExecuted) {
loaderContext.loaderIndex--;
return iterateNormalLoaders(options, loaderContext, args, callback);
}


var fn = currentLoaderObject.normal;
currentLoaderObject.normalExecuted = true;

if(!fn) {
return iterateNormalLoaders(options, loaderContext, args, callback);
}

//转换buffer数据。如果当前loader设置了raw属性
convertArgs(args, currentLoaderObject.raw);

runSyncOrAsync(fn, loaderContext, args, function(err) {
if(err) return callback(err);

var args = Array.prototype.slice.call(arguments, 1);
iterateNormalLoaders(options, loaderContext, args, callback);
});

}


function convertArgs(args, raw) {
if(!raw && Buffer.isBuffer(args[0]))
args[0] = utf8BufferToString(args[0]);
else if(raw && typeof args[0] === "string")
args[0] = Buffer.from(args[0], "utf-8");
}

exports.getContext = function getContext(resource) {
var splitted = splitQuery(resource);
return dirname(splitted[0]);
};

function createLoaderObject(loader){
//初始化loader配置
var obj = {
path: null,
query: null,
options: null,
ident: null,
normal: null,
pitch: null,
raw: null,
data: null,
pitchExecuted: false,
normalExecuted: false
};

//设置响应式属性
Object.defineProperty(obj, "request", {
enumerable: true,
get: function() {
return obj.path + obj.query;
},
set: function(value) {
if(typeof value === "string") {
var splittedRequest = splitQuery(value);
obj.path = splittedRequest[0];
obj.query = splittedRequest[1];
obj.options = undefined;
obj.ident = undefined;
} else {
if(!value.loader)
throw new Error("request should be a string or object with loader and object (" + JSON.stringify(value) + ")");
obj.path = value.loader;
obj.options = value.options;
obj.ident = value.ident;
if(obj.options === null)
obj.query = "";
else if(obj.options === undefined)
obj.query = "";
else if(typeof obj.options === "string")
obj.query = "?" + obj.options;
else if(obj.ident)
obj.query = "??" + obj.ident;
else if(typeof obj.options === "object" && obj.options.ident)
obj.query = "??" + obj.options.ident;
else
obj.query = "?" + JSON.stringify(obj.options);
}
}
});

obj.request = loader;

//冻结对象
if(Object.preventExtensions) {
Object.preventExtensions(obj);
}
return obj;

}

exports.runLoaders = function runLoaders(options, callback) {
//options = {resource...,fn...}

// 读取options
var resource = options.resource || "";
var loaders = options.loaders || [];
var loaderContext = options.context || {};
var readResource = options.readResource || readFile;

//
var splittedResource = resource && splitQuery(resource);
var resourcePath = splittedResource ? splittedResource[0] : undefined;
var resourceQuery = splittedResource ? splittedResource[1] : undefined;
var contextDirectory = resourcePath ? dirname(resourcePath) : null;

//执行状态
var requestCacheable = true;
var fileDependencies = [];
var contextDependencies = [];