1. 程式人生 > >webpack-loader原理

webpack-loader原理

tco 轉化 nload 屬性 -a property 返回 對象 選項

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

webpack-loader原理