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