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
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;