3. loader专

668 阅读5分钟

自定义loader的配置:

  • 可以先在resolveLoader中指定路径,然后直接使用
  • 也可以配置modules,默认在node_modules下找loader,如果找不到,可以指定查找路径, 这样也是可以的
{
    resolveLoader: {
        alias: {
            loader1: path.resolve(__dirname, "loaders", "loader1.js")
        },
        modules: ["node_modules", path.resolve(__dirname, "loaders")]
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                use: "loader1"
            }
        ]
    }
}

配置多个loader

  • loader的执行顺序: 从右到左,从下到上
  • loader的分类:enforce:"pre" 在前面的; enforce:"post" 在后面; normal
  • loader的执行顺序:pre => normal => inline => post
    • inline-loader: let str = require("loader4!./a.js")
    • let str = require("-!loader4!./a.js")
    • -!不会让文件再去通过pre+normal loader来处理
    • 前面加! 没有normal
    • 前面加 !! 什么都没有,只要行内loader来处理
  • use 配置成数组
rules: [
    {
        test: /\.js$/,
        use: ["loader3","loader2","loader1"]
    }
]

// 也可以这样:
rules: [
    {
        test: /\.js$/,
        use: "loader3"
    },
    {
        test: /\.js$/,
        use: "loader2"
    }
    {
        test: /\.js$/,
        use: "loader1"
    }
]

pitchLoader和normalLoader

  • loader的特点
    • 第一个loader要返回js脚本【(最后执行的loader), 因为要将返回的内容放到eval函数中执行】
    • 每一个loader只做一件内容,为了使loader在更多场景链式调用
    • 每一个loader都是一个模块
    • 每个loader都是无状态的,确保loader在不同模块之间不保存状态
// loader默认是由两部分组成:pitchLoader和normalLoader
function loader1(resource) {
    console.log("loader1");
    return resource;
}
loader1.pitch  =  function() {
    console.log("loader1-pitch");
    // return "xxx"; 无返回值时正常向后执行
}
module.exports =  loader1;

use: ["loader3","loader2", "loader1"]
当pitchloader没有返回值的时候:执行的顺序是:
loader3-pitch => loader2-pitch => loader1-pitch => loader1 => loader2 => loader3
当pitchloader有返回值的时候,会跳过后面loader-pitch的执行和loader(包括自己的loader)的执行
假设loader2-pitch有返回值,执行顺序会变成:
loader3-pitch => lodaer2-pitch => loader3

babel-loader的实现

  • 先安装babel相关插件:@babel/core @babel/preset-env
let babel = require("@babel/core");
let loaderUtils = require("loader-utils");
function babelLoader(resource) {
    // loader中的this上有很多属性
    console.log("babel loader");
    // babel-loader本身只是一个桥梁的作用,就是这个函数本身
    // 1.要拿到预设(presets),babel要根据这个来转换代码 
    // 自定义loader可以通过this(this.query)接收到传递过来的options里面的参数
    // 可以在自定义loader中通过loader-utils插件来分析和获取options传递的参数
    // 需要通过一个工具库拿到loader的定义参数,叫做: loader-utils
    let options = loaderUtils.getOptions(this);
    const callback = this.async(); // 异步返回时执行 babel.transform异步,不能再同步return resource
    // 进行代码转换
    babel.transform(resource, {
        ...options,
        sourceMaps: true,
        filename: this.resourcePath.split("/").pop(), // 文件名
    }, function(err, result) {
        callback(err, result.code, result.map);
    })
    return resource;
}

module.exports = babelLoader;

banner-loader的实现

let loaderUtils = require("loader-utils");
let {validate :validateOptions} = require("schema-utils"); // 对参数进行校验
let fs = require("fs");

function bannerLoader(resource) {
    let options = loaderUtils.getOptions(this);
    let callback = this.async();
    let schema = {
        type: "object",
        properties: {
            text: {
                type: "string"
            },
            filename: {
                type: "string"
            }
        }
    };
    validateOptions(schema,options, "banner-loader");
    // 如果在webpack配置中加上watch:true,则是边写边打包,这时候修改banner.js里的内容,是不会触发实时编译的
    // 在loader里面去读了一个文件,但是webpack认为这个文件和webpack没有关系
    // 希望banner-loader当依赖的文件发生变化的时候 webpack能重新去打包  需要把这个文件加入到依赖当中
    if(options.filename) {
        this.addDependency(options.filename); // 自动添加文件依赖
        fs.readFile(options.filename, "utf8", (err,data) => {
            callback(err, `/**${data}**/${resource}`)
        })
    }else{
        callback(null, `/**${options.text}**/${resource}`)
    }
}
module.exports = bannerLoader;


file-loader的实现

let loaderUtils = require("loader-utils");

// file-loader的目的就是根据图片生成一个md5 发射到dist目录下, file-loader还会返回当前的图片路径 
function fileLoader(resource) {
    // 将图片buffer内容生成md5
    let filename = loaderUtils.interpolateName(this, '[hash].[ext]', {content: resource});
    this.emitFile(filename, resource); // 发射文件
    // return resource;
    // file-loader需要返回一个文件路径
    return `module.exports="${filename}"`;
}
// 源码拿到的都是字符串,需要把源码改成buffer
fileLoader.raw = true; // 二进制
module.exports = fileLoader;

url-loader的实现

// url-loader会调用file-loader
let loaderUtils = require("loader-utils");
let mime = require("mime");
function urlLoader(resource) {
    let options = loaderUtils.getOptions(this);
    let limit = options.limit;
    if(limit&& limit > resource.length) {
        // base64 
        // 还是返回路径 base64开头都是:data:image/jpeg;base64,  后面是图片的内容
        // this.resourcePath是资源的路径
        // buffer转base64:  toString("base64")
        return `module.exports="data:${mime.getType(this.resourcePath)};base64,${resource.toString("base64")}"`
    }else{
        return require("./file-loader").call(this, resource);
    }
}
urlLoader.raw = true;
module.exports = urlLoader;

less-loader的实现

// loader就是一个函数, loader里的参数就是源码,对源码进行过滤
let less = require("less");
function lessloader(source) {
    let css =  "";
    less.render(source, function(err, c) {
        css = c.css;
    });
    // css = css.replace(/\n/g, "\\n"); // 用于zf-pack 手写的webpack的实现
    return css;
}


module.exports = lessloader;

style-loader的实现

function styleloader(source) {
    let style =  `
        let style = document.createElement("style");
        style.innerHTML=${JSON.stringify(source)};
        document.head.appendChild(style);
    `
    return style;
}
module.exports = styleloader;

css-loader

/*
@color: skyblue;
body {
  background: @color;
  background: url('./logo.jpeg');
}
如果不作处理 url里的路径打包后就是这个字符串, 找不到对应文件
所以需要将路径转化成require()的形式,这样就可以被打包
*/
function cssLoader(resource) {
    let reg=/url\((.+?)\)/g;
    let pos = 0;
    let current;
    let arr = ["let list = []"]
    while(current = reg.exec(resource)) {
        console.log(current, 123123123)
        let [matchUrl, g] = current;
        let last = reg.lastIndex - matchUrl.length;
        arr.push(`list.push(${JSON.stringify(resource.slice(pos, last))})`)
        pos = reg.lastIndex;
        // 把g替换成require的写法
        arr.push(`list.push('url('+require(${g})+')')`);
    }
    arr.push(`list.push(${JSON.stringify(resource.slice(pos))})`)
    arr.push(`module.exports=list.join(" ")`)
    return arr.join("\r\n");
}
module.exports = cssLoader;


// style-loader

// function loader(source) {
//     let style =  `
//         let style = document.createElement("style");
//         style.innerHTML=${JSON.stringify(source)};
//         document.head.appendChild(style);
//     `
//     return style;
// }

// module.exports = loader;


let loaderUtils = require("loader-utils");
function loader(source) {
    let style =  `
        let style = document.createElement("style");
        style.innerHTML=${JSON.stringify(source)};
        document.head.appendChild(style);
    `
    return style;
}
// 在style-loader上写了pitch 后面的loader都不执行
// style-loader css-loader less-loader 
loader.pitch = function(remainingRequest) { // remainingRequest剩余的参数
    // require("!!css-loader!less-loader!index.less")
    let style =  `
        let style = document.createElement("style");
        style.innerHTML=require(${loaderUtils.stringifyRequest(this,"!!" + remainingRequest)});
        document.head.appendChild(style);
    `
    return style;
}
module.exports = loader;

对于加载css文件,配置一般都是 style-loader css-loader
即css代码会先被css-loader处理一次,然后交给style-loader进行处理
    css-loader的作用是处理css中的@import和url这样的外部资源
    style-loader的作用是把样式插入到DOM中,方法是在head中插入一个style标签,并把样式写入到这个标签的innerHTML

pitch方法:
默认的loader都是从右向左执行,用pitching loader可以从左向右执行
为什么要使用pitching loader?
    因为要把css文件的内容插入dom,所以要获取css样式,如果使用css-loader,他返回的结果是一段js字符串
    这样就取不到css样式了。
    为了获取css样式,我们会在style-loader中直接通过require来获取,这样返回的js就不是字符串而是一段代码,
    同样的字符串,在默认模式下是当成字符串传入的,在pitching模式下是当代码运行的,这就是区别

也就是说,我们处理css的时候,其实是style-loader先执行了,他里面会调用css-loader来拿到css的内容,拿到的内容
当然是经过css-loader编译过的

在style-loader的pitch方法有返回值时,剩余的css-loader的pitch方法、css-loader的normal方法
以及style-loader的normal方法都不会执行了。而style-loader的pitch方法里面调用了require(!!.../x.css)
这就会把require的css文件当作新的入口文件,重新链式调用 剩余的loader来进行处理
值得注意的是 !!是一个标志,表示不会再重复递归调用style-loader,而只会调用css-loader处理了


loader-runner

  • loader-runner 是一个执行loader链条的模块
  • 是webpack为了执行loader开发的一个模块
  • const {runLoaders} = require("loader-runner");
const path = require("path");
const fs = require("fs");
const {runLoaders} = require("loader-runner");

const entry = path.resolve(__dirname, 'src', 'index.js');
/*
在加载模块的时候可以指定一些前缀
-! 不执行前置和普通loader
! 不执行普通loader
!! 不执行前后置和普通 只要内联
*/
let request = `inline1-loader!inline2-loader!${entry}`;
let rules = [
    {
        test: /\.js$/,
        use: ['normal1-loader', 'normal2-loader']
    },
    {
        enforce: "pre", // 前置
        test: /\.js$/,
        use: ['pre1-loader', 'pre2-loader']
    },
    {
        enforce: "post", // 后置
        test: /\.js$/,
        use: ['post1-loader', 'post2-loader']
    }
];

let parts = request.split("!"); // ["inline1-loader", "inline2-loader", entry]
let resource = parts.pop(); // entry
// 把自定义loader的名称转成一个绝对路径
const resolveLoader = loader => path.resolve(__dirname, 'loaders', loader);
let inlineLoaders = parts;
let preLoaders = [];
let normalLoaders = [];
let postLoaders = [];
for(let i = 0; i < rules.length; i++) {
    let rule = rules[i];
    if(rule.test.test(resource)) {
        if(rule.enforce === 'pre') {
            preLoaders.push(...rule.use);
        }else if(rule.enforce === 'post') {
            postLoaders.push(...rule.use);
        }else{
            normalLoaders.push(...rule.use);
        }
    }
}

let loaders = [];
if(request.startsWith("!!")) {
    loaders = inlineLoaders;
}else if(request.startsWith("-!")) {
    loaders = [...postLoaders, ...inlineLoaders];
}else if(request.startsWith("!")) {
    loaders = [...postLoaders, ...inlineLoaders, ...preLoaders];
}else {
    loaders = [...postLoaders, ...inlineLoaders, ...normalLoaders, ...preLoaders];
}
loaders = loaders.map(resolveLoader);
runLoaders({
    resource, // 将要加载和转换的模块路径
    loaders, // 使用哪些loader进行转换
    context: {name: "zhuzhu"}, // 上下文对象
    readResource: fs.readFile.bind(fs), // 可以自定义读取文件的方法
}, (err, result) => {
    console.log(err);
    console.log(result);
    console.log(result.resourceBuffer.toString("utf8"));
})

babel-loader

const babel = require("@babel/core");
const path = require("path");
function babelLoader(inputSource) {
    // const options = {
    //     preset: ["@babel/preset-env"],
    //     sourceMap: true, // 如果这个参数不传,默认值是false, 不会生成sourceMao
    //     filename: path.basename(this.resourcePath)
    // };
    const { getOptions } = require("loader-utils");
    let options = getOptions(this) || {};
    options.filename = path.basename(this.resourcePath);
    /*
        code 转换后的es5代码
        map 转换后的代码到转换前的代码的映射
        ast 转换后的抽象语法树
    */
    let {code, map, ast} = babel.transform(inputSource, options);
    /*
        loader 可以返回一个值 也可以返回多个值
        返回一个值可以return 返回多个值必须this.callback(err, 传递给下一个loader的参数)
        callback是loader-runner提供的一个方法
        this是context,默认是空对象,但是在loader-runner执行时会给context增加很多的方法和属性
    */
   return this.callback(null, code, map, ast);
}
module.exports = babelLoader;

file-loader

  • file-loader并不会对文件内容进行任何转换,只是复制一份文件内容,并根据配置为他生成唯一的文件名
  • 通过loaderUtils.interpolateName方法可以根据options.name以及文件内容生成一个唯一的文件名url(一般配置都会带上hash,否则很可能由于文件重名而冲突)
  • 通过this.emitFile(url,content)告诉webpack需要创建一个文件,webpack会根据参数创建对应的文件,放在public path目录下
  • 返回module.exports=${JSON.stringify(url)}
// 如图片模块不能直接使用,需要通过loader转成一个js模块
const { getOptions, interpolateName} = require("loader-utils");
function fileLoader(content) {
    let options = getOptions(this);
    // 得到文件名
    let url = interpolateName(this, options.filename || "[hash].[ext]",{content});
    /*
    向输出目录里输出一个文件,是loader-runner提供的一个方法
    原理是 compilation.assets[url]=content
    */
    this.emitFile(url, content);
    // 最后一个loader肯定要返回一个js模块代码,导出一个值,这个值将会成为此模块的导出结果
    return `module.exports=${JSON.stringify(url)}`
}
/*
content是上一个loader传给当前loader的内容,或者源文件的内容
content默认是字符串 先把content转为字符串传递给loader
如果希望得到的是buffer,不希望转成字符串传递到loadef当中就设置 loader.raw=true
*/
fileLoader.raw = true;
module.exports = fileLoader;

url-loader

const { getOptions } = require("loader-utils");
const mime = require("mime");
function urlLoader(source) {
    const options = getOptions(this) || {};
    let { limit, fallback="file-loader" } = options;
    if(limit) {
        limit = parseInt(limit, 10);
    }
    const mimeType = mime.getType(this.resourcePath);
    if(!limit || source.length < limit) {
        let base64 = `data:${mimeType};base64,${source.toString("base64")}`;
        return `module.exports=${JSON.stringify(base64)}`
    }else{
        let fileLoader = require(fallback || "file-loader");
        return fileLoader.call(this, source)
    }
}
urlLoader.raw = true;
module.exports = urlLoader;

less-loader

let less = require("less");

function loader(inputSource) {
    // 默认情况下loader的执行是同步的,如果调用了async方法,可以把loader的执行变成异步
    let callback = this.async();
    less.render(inputSource, { filename: this.resource}, (err, output) => {
        // 目前less编译过程本身是同步的 并不是异步
        callback(null, output.css);
    })
}
module.exports = loader;
  • less-loader处理完成后返回的是一段css脚本,说明less-loader不能放在最左侧。因为最左侧loader的返回值要给到webpack,webpack只能接收js,不是js时webpack处理不了

style-loader

function styleLoader(inputSource) { // inputSource是less-loader编译后的css内容
    let script = `
        let style = document.createElement('style');
        style.innerHTML = ${JSON.stringify(inputSource)};
        document.head.appendChild(style);
    `;
    return script;
}
module.exports = styleLoader;

源码中less-loader的实现

  • 返回 module.exports=${JSON.stringify(output.css)}
// 希望less-loader能更灵活一点, 能放到最左侧来使用
// 这样 less-loader就需要返回一段js脚本
let less = require("less");

function loader(inputSource) {
    // 默认情况下loader的执行是同步的,如果调用了async方法,可以把loader的执行变成异步
    let callback = this.async();
    less.render(inputSource, { filename: this.resource}, (err, output) => {
        // 目前less编译过程本身是同步的 并不是异步
        callback(null, `module.exports=${JSON.stringify(output.css)}`);
    })
}
module.exports = loader;

源码中style-loader的实现

  • pitch取代了当前loader,并加载了之后的loader的返回值
// inputSource是less-loader编译后的css内容
// inputSource:  module.exports = "#root{color:red}"
// 如何拿到导出的内容: 整体是一个js模块,可以通过require去加载这个模块内容

let loaderUtils = require('loader-utils');
function styleLoader(inputSource) {

}
// 如果pitch函数有返回值,不需要执行后续的loader和读文件了
// style-loader在最左边 pitch返回的结果就直接给到webpack了
styleLoader.pitch = function(remainingRequest, previousRequest, data) {
    /*
        例如有5个loader来处理一个文件: loader1 loader2 loader3 loader4 loader5 file
        request= loader1!loader2!loader3!loader4!loader5!file
        当执行在loader3的pitch的时候:
        remainingRequest表示 loader4!loader5!file
        previousRequest表示 loader1!loader2
        data默认是一个空对象 可以自定义一些想存储的数据放在里面 在normalLoader函数中可以通过this.data访问到这些数据
        每个loader都有自己的data对象,互不干扰
    */ 
   let script = `
    let style = document.createElement('style');
    style.innerHTML = require(${loaderUtils.stringifyRequest(this, "!!"+remainingRequest)});
    document.head.appendChild(style);
   `
   return script;
}
/*
[style-loader, less-loader]
request = style-loader!less-loader!index.less
remainingRequest=less-loader!index.less
!!less-loader!index.less
remainingRequest本身都是绝对路径 C:\less-loader.js!C:\index.less
stringifyRequest把绝对路径转成相对路径 相对于根目录的路径 类似于此模块的ID: ./loaders/less-loader.js!./src/index.less
require()里的内容最终变成: !!./loaders/less-loader.js!./src/index.less

pitch函数里返回的js脚本给到webpack了
会把这个js脚本转成ast抽象语法树 分析里面的require依赖
!!表示只走内联loader
require(!!./loaders/less-loader.js!./src/index.less) 的返回值就是
`module.exports=${JSON.stringify(output.css)}`的导出结果
这样就拿到了css内容,再document.head.appendChild(style);
*/
module.exports = styleLoader;

loader-runner的实现

// 将loader转成loader对象
// 参数loader是loader的绝对路径
function createLoaderObject(loader) {
    let loaderObject = {
        path: loader, // loader的绝对路径
        normal: null, // loader的normal函数
        pitch: null, // loader的pitch函数
        raw: false, // 是否要转成buffer,raw=true 要转成buffer raw=false参数就要转成字符串
        data: {}, // 每个loader都有一个自己的data自定义对象,用来存放一些自定义数据
        pitchExecuted: false, // 此loader的pitch方法睡否已经执行过了
        normalExecuted: false, // 此loader的normal方法是否已经执行过了
    };
    return loaderObject;
}
// 读取文件的方法
function processResource(options, loaderContext, runLoadersCallback){
    options.readResource(loaderContext.resource, (err, buffer) => {
        loaderContext.loaderIndex = loaderContext.loaders.length - 1;
        options.resourceBuffer = buffer; // 要加载的文件的原始文件内容
        iterateNormalLoaders(options, loaderContext,  [buffer], runLoadersCallback)
    })
}
function convertArgs(args, raw) {
    if(raw && !Buffer.isBuffer(args[0])) {
        args[0] =  Buffer.from(args[0]);
    }else if(!raw && Buffer.isBuffer(args[0])) {
        args[0] = args[0].toString('utf8')
    }
}
// 执行normal loaders
function iterateNormalLoaders(options, loaderContext, [buffer], runLoadersCallback) {
    // 越界
    if(loaderContext.loaderIndex <0) {
        // 读取文件
        return runLoadersCallback(null, ...args);
    }
    // 获取当前索引对应的loader
    let currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
    if(currentLoaderObject.normalExecuted) {
        loaderContext.loaderIndex--;
        return iterateNormalLoaders(options, loaderContext, runLoadersCallback)
    }
    // 加载当前loader
    loadLoader(currentLoaderObject, () => {
        // 获取当前loader的normal函数
        let fn = currentLoaderObject.normal;
        // 表示当前loader的normal函数已经执行过了
        currentLoaderObject.normalExecuted = true; 
        if(!fn) { // 如果当前的loader没有normal
            return iterateNormalLoaders(options, loaderContext, runLoadersCallback)
        }
        convertArgs(args, currentLoaderObject.raw);
        // 如果这个loader有normal方法
        // normal fn可以是同步也可以是异步执行
        runSyncOrAsync(fn, loaderContext, args, (err, ...args) => {
            if(err) return runLoadersCallback(err);
            return iterateNormalLoaders(options, loaderContext, args, runLoadersCallback)
        })
    });
}
// 执行pitch loaders
function iteratePitchLoaders(options, loaderContext, runLoadersCallback)  {
    // 如果当前索引已经大于loader的数量 则表示所有的loader pitch执行完了
    if(loaderContext.loaderIndex >= loaderContext.loaders.length) {
        // 读取文件
        return processResource(options, loaderContext, runLoadersCallback)
    }
    // 获取当前索引对应的loader
    let currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
    if(currentLoaderObject.pitchExecuted) {
        loaderContext.loaderIndex++;
        return iteratePitchLoaders(options, loaderContext, runLoadersCallback)
    }
    // 加载当前loader
    loadLoader(currentLoaderObject, () => {
        // 获取当前loader的pitch函数
        let fn = currentLoaderObject.pitch;
        // 表示当前loader的pitch函数已经执行过了
        currentLoaderObject.pitchExecuted = true; 
        if(!fn) { // 如果当前的loader没有pitch
            return iteratePitchLoaders(options, loaderContext, runLoadersCallback)
        }
        // 如果这个loader有pitch方法
        // pitch fn可以是同步也可以是异步执行
        runSyncOrAsync(fn, loaderContext, [
            loaderContext.remainingRequest,
            loaderContext.previousRequest,
            loaderContext.data
        ], (err, ...args) => {
            //  args可能有多个值,可能没有值
            if(args.length >  0 && args.some(item => !!item)){
                // 跳过后续的pitch和读文件 直接掉头执行前一个loader的normal
        
            }else{
                return iteratePitchLoaders(options, loaderContext, runLoadersCallback)
            }
        })
    });
}
// 以同步或者异步的方式执行fn
function runSyncOrAsync(fn, context, args, callback) {
    let isSync = true; // 默认是同步执行
    let isDone = false; // 是否已经执行完了此函数
    let innerCallback = context.callback = (...args) => {
        isDone = true;
        callback(null, ...args);
    }
    context.async = () => {
        isSync = false; // 把同步变成异步
        return innerCallback
    }
    // 用指定的参数和this对象执行函数 返回一个结果
    var result = fn.apply(context, args);
    if(isSync) {
        isDone = true;
        if(result === undefined) {
            return callback();
        }else{
            return callback(null, result);
        }
    }
}

function loadLoader(loaderObject, callback) {
    var module = require(loaderObject.path);
    loaderObject.normal = module.normal;
    loaderObject.pitch = module.pitch;
    loaderObject.raw = module.raw;
    callback();
}


function runLoader(options, finalCallback) {
    let resource = options.resource; // 加载的文件 src/index.js
    let loaders = options.loaders || []; // 处理文件的loader数组
    let loaderContext = options.context || {}; // loader函数执行时的上下文对象
    let readResource = options.readResource || fs.readFile; // 用来 读加载的文件 的方法
    let loaderObjects = loaders.map(createLoaderObject);

    // 给loaderContext上增加属性
    loaderContext.resource = resource;
    loaderContext.readResource  = readResource;
    loaderContext.loaders = loaderObjects;
    loaderContext.loaderIndex = 0; // 当前正在执行的loader的索引
    loaderContext.callback =  null;
    loaderContext.async = null; 

    Object.defineProperty(loaderContext, 'request', {
        get() {
            return loaderContext.loaders.map(l => l.path).concat(loaderContext.resource).join("!");
        }
    });
    Object.defineProperty(loaderContext, 'remainingRequest', {
        get() {
            return loaderContext.loaders.slice(loaderContext.loaderIndex + 1).map(i => l.path).concat("!");
        }
    });
    // currentRequest包括自己 remainingRequest不包括自己
    Object.defineProperty(loaderContext, 'currentRequest', {
        get() {
            return loaderContext.loaders.slice(loaderContext.loaderIndex).map(i => l.path).concat("!");
        }
    });
    Object.defineProperty(loaderContext, 'previousRequest', {
        get() {
            return loaderContext.loaders.slice(0, loaderContext.loaderIndex).map(i => l.path).concat("!");
        }
    });
    Object.defineProperty(loaderContext, 'data', {
        get() {
            return loaderContext.loaders[loaderContext.loaderIndex].data
        }
    });

    let processOptions = {
        resourceBuffer:null, // 存放原始内容对应的buffer 用loader转换前的内容buffer
        readResource
    }
    // 开始从左往右开始执行每个loader的pitch方法
    iteratePitchLoaders(processOptions, loaderContext, (err, result) => {
        finalCallback(
            err,{
                result,
                resourceBuffer: processOptions.resourceBuffer
            }
        )
    })
}