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 + "\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 = [];
    
     
                    
                     
                    
                 
                    
                
