loader 是導出爲一個函數的 node 模塊。該函數在 loader 轉換資源的時候調用。給定的函數將調用 loader API,並經過 this 上下文訪問。css
{ test: /\.js$/ use: [ { loader: path.resolve('path/to/loader.js'), options: {/* ... */} } ] }
resolveLoader: { modules: [ 'node_modules', path.resolve(__dirname, 'loaders') ] }
//返回簡單結果 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) }
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 使用外部資源(例如,從文件系統讀取),必須聲明它。這些信息用於使緩存 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'))
方法名 | 含義 |
---|---|
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-runnernode
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(); } };