• webpack-loader原理


    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 + "
    " + 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();
        }
    };
  • 相关阅读:
    JavaScript 内置函数有什么?
    cursor(鼠标手型)属性
    用CSS制作箭头的方法
    CSS 的伪元素是什么?
    用CSS创建分页的实例
    CSS 按钮
    网页布局中高度塌陷的解决方法
    CSS 进度条
    DOM导航与DOM事件
    DOM 修改与DOM元素
  • 原文地址:https://www.cnblogs.com/cangqinglang/p/11149494.html
Copyright © 2020-2023  润新知