IT序号网

webpack-loader原理知识解答

sanshao 2021年05月25日 程序员 158 0

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; //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(); } };


发布评论
IT序号网

微信公众号号:IT虾米 (左侧二维码扫一扫)欢迎添加!

webpack4.x版本splitChunksPlugin的配置项详解与实际应用场景知识解答
你是第一个吃螃蟹的人
发表评论

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。