webpack-loader原理

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 + "
" + 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

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 = [];
    
    //准备loader对象
    loaders = loaders.map(createLoaderObject);

    loaderContext.context = contextDirectory; //当前文件所在目录
	loaderContext.loaderIndex = 0; //从0个开始
	loaderContext.loaders = loaders; //loaders数组
	loaderContext.resourcePath = resourcePath; //当前文件所在位置
	loaderContext.resourceQuery = resourceQuery; //当前文件的?部分
	loaderContext.async = null; //异步状态
	loaderContext.callback = null; //同步状态
	loaderContext.cacheable = function cacheable(flag) { //是否设置缓存
		if(flag === false) {
			requestCacheable = false;
		}
	};
	loaderContext.dependency = loaderContext.addDependency = function addDependency(file) {
		fileDependencies.push(file);
	};//记录文件依赖
	loaderContext.addContextDependency = function addContextDependency(context) {
		contextDependencies.push(context);
	};//记录目录依赖
	loaderContext.getDependencies = function getDependencies() {
		return fileDependencies.slice();
	};//获取文件依赖
	loaderContext.getContextDependencies = function getContextDependencies() {
		return contextDependencies.slice();
	};//获取文件目录依赖
	loaderContext.clearDependencies = function clearDependencies() {
		fileDependencies.length = 0;
		contextDependencies.length = 0;
		requestCacheable = true;
    };//删除依赖
    
    //设置响应属性,获取resource自动添加query,设置时自动解析
    Object.defineProperty(loaderContext, "resource", {
		enumerable: true,
		get: function() {
			if(loaderContext.resourcePath === undefined)
				return undefined;
			return loaderContext.resourcePath + loaderContext.resourceQuery;
		},
		set: function(value) {
			var splittedResource = value && splitQuery(value);
			loaderContext.resourcePath = splittedResource ? splittedResource[0] : undefined;
			loaderContext.resourceQuery = splittedResource ? splittedResource[1] : undefined;
		}
	});
	Object.defineProperty(loaderContext, "request", {
		enumerable: true,
		get: function() {
			return loaderContext.loaders.map(function(o) {
				return o.request;
			}).concat(loaderContext.resource || "").join("!");
		}
	});
	Object.defineProperty(loaderContext, "remainingRequest", {
		enumerable: true,
		get: function() {
			if(loaderContext.loaderIndex >= loaderContext.loaders.length - 1 && !loaderContext.resource)
				return "";
			return loaderContext.loaders.slice(loaderContext.loaderIndex + 1).map(function(o) {
				return o.request;
			}).concat(loaderContext.resource || "").join("!");
		}
	});
	Object.defineProperty(loaderContext, "currentRequest", {
		enumerable: true,
		get: function() {
			return loaderContext.loaders.slice(loaderContext.loaderIndex).map(function(o) {
				return o.request;
			}).concat(loaderContext.resource || "").join("!");
		}
	});
	Object.defineProperty(loaderContext, "previousRequest", {
		enumerable: true,
		get: function() {
			return loaderContext.loaders.slice(0, loaderContext.loaderIndex).map(function(o) {
				return o.request;
			}).join("!");
		}
	});
	Object.defineProperty(loaderContext, "query", {
		enumerable: true,
		get: function() {
			var entry = loaderContext.loaders[loaderContext.loaderIndex];
			return entry.options && typeof entry.options === "object" ? entry.options : entry.query;
		}
	});
	Object.defineProperty(loaderContext, "data", {
		enumerable: true,
		get: function() {
			return loaderContext.loaders[loaderContext.loaderIndex].data;
		}
    });
    
    // 完成loader上下文
    //冻结对象
    if(Object.preventExtensions) {
		Object.preventExtensions(loaderContext);
	}

	var processOptions = {
		resourceBuffer: null,
		readResource: readResource
    };
    
    //进入loaderPitching阶段
	iteratePitchingLoaders(processOptions, loaderContext, function(err, result) {
		if(err) {
			return callback(err, {
				cacheable: requestCacheable,
				fileDependencies: fileDependencies,
				contextDependencies: contextDependencies
			});
		}
		callback(null, {
			result: result,
			resourceBuffer: processOptions.resourceBuffer,
			cacheable: requestCacheable,
			fileDependencies: fileDependencies,
			contextDependencies: contextDependencies
		});
	});
}

//进入loaderPitch阶段
function iteratePitchingLoaders(options, loaderContext, callback) {
    // 在最后一个loader之后终止
    if(loaderContext.loaderIndex >= loaderContext.loaders.length)
        //开始递归解析依赖
        return processResource(options, loaderContext, callback);

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

    // 迭代
	if(currentLoaderObject.pitchExecuted) {
		loaderContext.loaderIndex++;
		return iteratePitchingLoaders(options, loaderContext, callback);
    }
    
    // 加载loader module
	loadLoader(currentLoaderObject, function(err) {
		if(err) return callback(err);
        var fn = currentLoaderObject.pitch;
        //记录pitch执行状态
        currentLoaderObject.pitchExecuted = true;
        //没有pitch方法就执行下一个
		if(!fn) return iteratePitchingLoaders(options, loaderContext, callback);
        //执行pitch方法
		runSyncOrAsync(
			fn,
			loaderContext, [loaderContext.remainingRequest, loaderContext.previousRequest, currentLoaderObject.data = {}],
			function(err) {
				if(err) return callback(err);
				var args = Array.prototype.slice.call(arguments, 1);
				// Determine whether to continue the pitching process based on
				// argument values (as opposed to argument presence) in order
				// to support synchronous and asynchronous usages.
				var hasArg = args.some(function(value) {
					return value !== undefined;
                });
                //根据有无返回值执行对象loader,如果有返回值就执行normalloader,不执行后面的pitch了
				if(hasArg) {
					loaderContext.loaderIndex--;
					iterateNormalLoaders(options, loaderContext, args, callback);
				} else {
					iteratePitchingLoaders(options, loaderContext, callback);
				}
			}
		);
	});
}

//运行异步或同步loader
function runSyncOrAsync(fn, context, args, callback) {
    //设置初始状态
	var isSync = true;
	var isDone = false;
	var isError = false; // 内部错误
    var reportedError = false;
    
    //挂载loader异步方法
	context.async = function async() {
		if(isDone) {
			if(reportedError) return; // ignore
			throw new Error("async(): The callback was already called.");
		}
		isSync = false;
		return innerCallback;
    };
    //挂载loader同步方法
	var innerCallback = context.callback = function() {
		if(isDone) {
			if(reportedError) return; // ignore
			throw new Error("callback(): The callback was already called.");
		}
		isDone = true;
		isSync = false;
		try {
			callback.apply(null, arguments);
		} catch(e) {
			isError = true;
			throw e;
		}
    };
    
	try {
		var result = (function LOADER_EXECUTION() {
			return fn.apply(context, args);
		}());
		if(isSync) {
			isDone = true;
			if(result === undefined)
				return callback();
			if(result && typeof result === "object" && typeof result.then === "function") {
				return result.catch(callback).then(function(r) {
					callback(null, r);
				});
			}
			return callback(null, result);
		}
	} catch(e) {
		if(isError) throw e;
		if(isDone) {
			// loader is already "done", so we cannot use the callback function
			// for better debugging we print the error on the console
			if(typeof e === "object" && e.stack) console.error(e.stack);
			else console.error(e);
			return;
		}
		isDone = true;
		reportedError = true;
		callback(e);
	}

}
//loaderLoader.js
module.exports = function loadLoader(loader, callback) {
    //加载loader,并且拿到loader设置的pitch与raw属性
	if(typeof System === "object" && typeof System.import === "function") {
		System.import(loader.path).catch(callback).then(function(module) {
			loader.normal = typeof module === "function" ? module : module.default;
			loader.pitch = module.pitch;
			loader.raw = module.raw;
			if(typeof loader.normal !== "function" && typeof loader.pitch !== "function")
				throw new Error("Module '" + loader.path + "' is not a loader (must have normal or pitch function)");
			callback();
		});
	} else {
		try {
			var module = require(loader.path);
		} catch(e) {
			// it is possible for node to choke on a require if the FD descriptor
			// limit has been reached. give it a chance to recover.
			if(e instanceof Error && e.code === "EMFILE") {
				var retry = loadLoader.bind(null, loader, callback);
				if(typeof setImmediate === "function") {
					// node >= 0.9.0
					return setImmediate(retry);
				} else {
					// node < 0.9.0
					return process.nextTick(retry);
				}
			}
			return callback(e);
		}
		if(typeof module !== "function" && typeof module !== "object")
			throw new Error("Module '" + loader.path + "' is not a loader (export function or es6 module))");
		loader.normal = typeof module === "function" ? module : module.default;
		loader.pitch = module.pitch;
		loader.raw = module.raw;
		if(typeof loader.normal !== "function" && typeof loader.pitch !== "function")
			throw new Error("Module '" + loader.path + "' is not a loader (must have normal or pitch function)");
		callback();
	}
};
原文地址:https://www.cnblogs.com/pluslius/p/10264865.html