国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

Webpack Loader 高手進階(二)

jackzou / 926人閱讀

摘要:如果函數沒有返回值的話,那么進入到下一個的函數的執行階段。這也是異步化的一種方式如果執行后有返回值,執行開始下一個執行以上就是對于在構建過程中執行流程的源碼分析。

文章首發于個人github blog: Biu-blog,歡迎大家關注~

Webpack 系列文章:

Webpack Loader 高手進階(一)
Webpack Loader 高手進階(二)
Webpack Loader 高手進階(三)

Webpack Loader 詳解

上篇文章主要講了 loader 的配置,匹配相關的機制。這篇主要會講當一個 module 被創建之后,使用 loader 去處理這個 module 內容的流程機制。首先我們來總體的看下整個的流程:

在 module 一開始構建的過程中,首先會創建一個 loaderContext 對象,它和這個 module 是一一對應的關系,而這個 module 所使用的所有 loaders 都會共享這個 loaderContext 對象,每個 loader 執行的時候上下文就是這個 loaderContext 對象,所以可以在我們寫的 loader 里面通過 this 來訪問。

// NormalModule.js

const { runLoaders } = require("loader-runner")

class NormalModule extends Module {
  ...
  createLoaderContext(resolver, options, compilation, fs) {
    const requestShortener = compilation.runtimeTemplate.requestShortener;
    // 初始化 loaderContext 對象,這些初始字段的具體內容解釋在文檔上有具體的解釋(https://webpack.docschina.org/api/loaders/#this-data)
        const loaderContext = {
            version: 2,
            emitWarning: warning => {...},
            emitError: error => {...},
            exec: (code, filename) => {...},
            resolve(context, request, callback) {...},
            getResolve(options) {...},
            emitFile: (name, content, sourceMap) => {...},
            rootContext: options.context, // 項目的根路徑
            webpack: true,
            sourceMap: !!this.useSourceMap,
            _module: this,
            _compilation: compilation,
            _compiler: compilation.compiler,
            fs: fs
        };

    // 觸發 normalModuleLoader 的鉤子函數,開發者可以利用這個鉤子來對 loaderContext 進行拓展
        compilation.hooks.normalModuleLoader.call(loaderContext, this);
        if (options.loader) {
            Object.assign(loaderContext, options.loader);
        }

        return loaderContext;
  }

  doBuild(options, compilation, resolver, fs, callback) {
    // 創建 loaderContext 上下文
        const loaderContext = this.createLoaderContext(
            resolver,
            options,
            compilation,
            fs
    )
    
    runLoaders(
      {
        resource: this.resource, // 這個模塊的路徑
                loaders: this.loaders, // 模塊所使用的 loaders
                context: loaderContext, // loaderContext 上下文
                readResource: fs.readFile.bind(fs) // 讀取文件的 node api
      },
      (err, result) => {
        // do something
      }
    )
  }
  ...
}

當 loaderContext 初始化完成后,開始調用 runLoaders 方法,這個時候進入到了 loaders 的執行階段。runLoaders 方法是由loader-runner這個獨立的 npm 包提供的方法,那我們就一起來看下 runLoaders 方法內部是如何運行的。

首先根據傳入的參數完成進一步的處理,同時對于 loaderContext 對象上的屬性做進一步的拓展:

exports.runLoaders = function runLoaders(options, callback) {
  // read options
    var resource = options.resource || ""; // 模塊的路徑
    var loaders = options.loaders || []; // 模塊所需要使用的 loaders
    var loaderContext = options.context || {}; // 在 normalModule 里面創建的 loaderContext
    var readResource = options.readResource || readFile;

    var splittedResource = resource && splitQuery(resource);
    var resourcePath = splittedResource ? splittedResource[0] : undefined; // 模塊實際路徑
    var resourceQuery = splittedResource ? splittedResource[1] : undefined; // 模塊路徑 query 參數
    var contextDirectory = resourcePath ? dirname(resourcePath) : null; // 模塊的父路徑

    // execution state
    var requestCacheable = true;
    var fileDependencies = [];
    var contextDependencies = [];

    // prepare loader objects
    loaders = loaders.map(createLoaderObject); // 處理 loaders 

  // 拓展 loaderContext 的屬性
    loaderContext.context = contextDirectory;
    loaderContext.loaderIndex = 0; // 當前正在執行的 loader 索引
    loaderContext.loaders = loaders;
    loaderContext.resourcePath = resourcePath;
    loaderContext.resourceQuery = resourceQuery;
    loaderContext.async = null; // 異步 loader
  loaderContext.callback = null;

  ...

  // 需要被構建的模塊路徑,將 loaderContext.resource -> getter/setter
  // 例如 /abc/resource.js?rrr
  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;
        }
  });

  // 構建這個 module 所有的 loader 及這個模塊的 resouce 所組成的 request 字符串
  // 例如:/abc/loader1.js?xyz!/abc/node_modules/loader2/index.js!/abc/resource.js?rrr
    Object.defineProperty(loaderContext, "request", {
        enumerable: true,
        get: function() {
            return loaderContext.loaders.map(function(o) {
                return o.request;
            }).concat(loaderContext.resource || "").join("!");
        }
  });
  // 在執行 loader 提供的 pitch 函數階段傳入的參數之一,剩下還未被調用的 loader.pitch 所組成的 request 字符串
    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("!");
        }
  });
  // 在執行 loader 提供的 pitch 函數階段傳入的參數之一,包含當前 loader.pitch 所組成的 request 字符串
    Object.defineProperty(loaderContext, "currentRequest", {
        enumerable: true,
        get: function() {
            return loaderContext.loaders.slice(loaderContext.loaderIndex).map(function(o) {
                return o.request;
            }).concat(loaderContext.resource || "").join("!");
        }
  });
  // 在執行 loader 提供的 pitch 函數階段傳入的參數之一,包含已經被執行的 loader.pitch 所組成的 request 字符串
    Object.defineProperty(loaderContext, "previousRequest", {
        enumerable: true,
        get: function() {
            return loaderContext.loaders.slice(0, loaderContext.loaderIndex).map(function(o) {
                return o.request;
            }).join("!");
        }
  });
  // 獲取當前正在執行的 loader 的query參數
  // 如果這個 loader 配置了 options 對象的話,this.query 就指向這個 option 對象
  // 如果 loader 中沒有 options,而是以 query 字符串作為參數調用時,this.query 就是一個以 ? 開頭的字符串
    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;
        }
  });
  // 每個 loader 在 pitch 階段和正常執行階段都可以共享的 data 數據
    Object.defineProperty(loaderContext, "data", {
        enumerable: true,
        get: function() {
            return loaderContext.loaders[loaderContext.loaderIndex].data;
        }
  });
  
  var processOptions = {
        resourceBuffer: null, // module 的內容 buffer
        readResource: readResource
  };
  // 開始執行每個 loader 上的 pitch 函數
    iteratePitchingLoaders(processOptions, loaderContext, function(err, result) {
    // do something...
  });
}

這里稍微總結下就是在 runLoaders 方法的初期會對相關參數進行初始化的操作,特別是將 loaderContext 上的部分屬性改寫為 getter/setter 函數,這樣在不同的 loader 執行的階段可以動態的獲取一些參數。

接下來開始調用 iteratePitchingLoaders 方法執行每個 loader 上提供的 pitch 函數。大家寫過 loader 的話應該都清楚,每個 loader 可以掛載一個 pitch 函數,每個 loader 提供的 pitch 方法和 loader 實際的執行順序正好相反。這塊的內容在 webpack 文檔上也有詳細的說明(請戳我)。

這些 pitch 函數并不是用來實際處理 module 的內容的,主要是可以利用 module 的 request,來做一些攔截處理的工作,從而達到在 loader 處理流程當中的一些定制化的處理需要,有關 pitch 函數具體的實戰可以參見下一篇文檔[Webpack 高手進階-loader 實戰] TODO: 鏈接

function iteratePitchingLoaders() {
  // abort after last loader
    if(loaderContext.loaderIndex >= loaderContext.loaders.length)
        return processResource(options, loaderContext, callback);

  // 根據 loaderIndex 來獲取當前需要執行的 loader
    var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];

  // iterate
  // 如果被執行過,那么直接跳過這個 loader 的 pitch 函數
    if(currentLoaderObject.pitchExecuted) {
        loaderContext.loaderIndex++;
        return iteratePitchingLoaders(options, loaderContext, callback);
    }

    // 加載 loader 模塊
    // load loader module
    loadLoader(currentLoaderObject, function(err) {
        // do something ...
    });
}

每次執行 pitch 函數前,首先根據 loaderIndex 來獲取當前需要執行的 loader (currentLoaderObject),調用 loadLoader 函數來加載這個 loader,loadLoader 內部兼容了 SystemJS,ES Module,CommonJs 這些模塊定義,最終會將 loader 提供的 pitch 方法和普通方法賦值到 currentLoaderObject 上:

// loadLoader.js
module.exports = function (loader, callback) {
  ...
  var module = require(loader.path)
 
  ...
  loader.normal = module

  loader.pitch = module.pitch

  loader.raw = module.raw

  callback()
  ...
}

當 loader 加載完后,開始執行 loadLoader 的回調:

loadLoader(currentLoaderObject, function(err) {
  var fn = currentLoaderObject.pitch; // 獲取 pitch 函數
  currentLoaderObject.pitchExecuted = true;
  if(!fn) return iteratePitchingLoaders(options, loaderContext, callback); // 如果這個 loader 沒有提供 pitch 函數,那么直接跳過

  // 開始執行 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.
      // 根據是否有參數返回來判斷是否向下繼續進行 pitch 函數的執行
      var hasArg = args.some(function(value) {
        return value !== undefined;
      });
      if(hasArg) {
        loaderContext.loaderIndex--;
        iterateNormalLoaders(options, loaderContext, args, callback);
      } else {
        iteratePitchingLoaders(options, loaderContext, callback);
      }
    }
  );
})

這里出現了一個 runSyncOrAsync 方法,放到后文去講,開始執行 pitch 函數,當 pitch 函數執行完后,執行傳入的回調函數。我們看到回調函數里面會判斷接收到的參數的個數,除了第一個 err 參數外,如果還有其他的參數(這些參數是 pitch 函數執行完后傳入回調函數的),那么會直接進入 loader 的 normal 方法執行階段,并且會直接跳過后面的 loader 執行階段。如果 pitch 函數沒有返回值的話,那么進入到下一個 loader 的 pitch 函數的執行階段。讓我們再回到 iteratePitchingLoaders 方法內部,當所有 loader 上面的 pitch 函數都執行完后,即 loaderIndex 索引值 >= loader 數組長度的時候:

function iteratePitchingLoaders () {
  ...

  if(loaderContext.loaderIndex >= loaderContext.loaders.length)
    return processResource(options, loaderContext, callback);

  ...
}

function processResource(options, loaderContext, callback) {
    // set loader index to last 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.resourceBuffer = buffer;
            iterateNormalLoaders(options, loaderContext, [buffer], callback);
        });
    } else {
        iterateNormalLoaders(options, loaderContext, [null], callback);
    }
}

在 processResouce 方法內部調用 node API readResouce 讀取 module 對應路徑的文本內容,調用 iterateNormalLoaders 方法,開始進入 loader normal 方法的執行階段。

function iterateNormalLoaders () {
  if(loaderContext.loaderIndex < 0)
        return callback(null, args);

    var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];

    // iterate
    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 和 utf8 string 之間的轉化
    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);
    });
}

在 iterateNormalLoaders 方法內部就是依照從右到左的順序(正好與 pitch 方法執行順序相反)依次執行每個 loader 上的 normal 方法。loader 不管是 pitch 方法還是 normal 方法的執行可為同步的,也可設為異步的(這里說下 normal 方法的)。一般如果你寫的 loader 里面可能涉及到計算量較大的情況時,可將你的 loader 異步化,在你 loader 方法里面調用this.async方法,返回異步的回調函數,當你 loader 內部實際的內容執行完后,可調用這個異步的回調來進入下一個 loader 的執行。

module.exports = function (content) {
  const callback = this.async()
  someAsyncOperation(content, function(err, result) {
    if (err) return callback(err);
    callback(null, result);
  });
}

除了調用 this.async 來異步化 loader 之外,還有一種方式就是在你的 loader 里面去返回一個 promise,只有當這個 promise 被 resolve 之后,才會調用下一個 loader(具體實現機制見下文):

module.exports = function (content) {
  return new Promise(resolve => {
    someAsyncOpertion(content, function(err, result) {
      if (err) resolve(err)
      resolve(null, result)
    })
  })
}

這里還有一個地方需要注意的就是,上下游 loader 之間的數據傳遞過程中,如果下游的 loader 接收到的參數為一個,那么可以在上一個 loader 執行結束后,如果是同步就直接 return 出去:

module.exports = function (content) {
  // do something
  return content
}

如果是異步就直接調用異步回調傳遞下去(參見上面 loader 異步化)。如果下游 loader 接收的參數多于一個,那么上一個 loader 執行結束后,如果是同步那么就需要調用 loaderContext 提供的 callback 函數:

module.exports = function (content) {
  // do something
  this.callback(null, content, argA, argB)
}

如果是異步的還是繼續調用異步回調函數傳遞下去(參見上面 loader 異步化)。具體的執行機制涉及到上文還沒講到的 runSyncOrAsync 方法,它提供了上下游 loader 調用的接口:

function runSyncOrAsync(fn, context, args, callback) {
    var isSync = true; // 是否為同步
    var isDone = false;
    var isError = false; // internal error
    var reportedError = false;
    // 給 loaderContext 上下文賦值 async 函數,用以將 loader 異步化,并返回異步回調
    context.async = function async() {
        if(isDone) {
            if(reportedError) return; // ignore
            throw new Error("async(): The callback was already called.");
        }
        isSync = false; // 同步標志位置為 false
        return innerCallback;
  };
  // callback 的形式可以向下一個 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 {
        // 開始執行 loader
        var result = (function LOADER_EXECUTION() {
            return fn.apply(context, args);
    }());
    // 如果為同步的執行
        if(isSync) {
      isDone = true;
      // 如果 loader 執行后沒有返回值,執行 callback 開始下一個 loader 執行
            if(result === undefined)
        return callback();
      // loader 返回值為一個 promise 實例,待這個實例被resolve或者reject后執行下一個 loader。這也是 loader 異步化的一種方式
            if(result && typeof result === "object" && typeof result.then === "function") {
                return result.catch(callback).then(function(r) {
                    callback(null, r);
                });
      }
      // 如果 loader 執行后有返回值,執行 callback 開始下一個 loader 執行
            return callback(null, result);
        }
    } catch(e) {
        // do something
    }
}

以上就是對于 module 在構建過程中 loader 執行流程的源碼分析。可能平時在使用 webpack 過程了解相關的 loader 執行規則和策略,再配合這篇對于內部機制的分析,應該會對 webpack loader 的使用有更加深刻的印象。

文章首發于個人github blog: Biu-blog,歡迎大家關注~

文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。

轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/109092.html

相關文章

  • Webpack Loader 高手進階(三)

    摘要:相關的內容為這樣對于一個處理的第二階段也就結束了,通過去攔截不同類型的,并返回新的,跳過后面的的執行,同時在內部會剔除掉,這樣在進入到下一個處理階段的時候,不在使用的范圍之內,因此下一階段便不會經由來處理。 文章首發于個人github blog: Biu-blog,歡迎大家關注~ Webpack 系列文章: Webpack Loader 高手進階(一)Webpack Loader 高手...

    ivydom 評論0 收藏0
  • Webpack Loader 高手進階(一)

    摘要:在一個構建過程中,首先根據的依賴類型例如調用對應的構造函數來創建對應的模塊。 文章首發于個人github blog: Biu-blog,歡迎大家關注~ Webpack 系列文章: Webpack Loader 高手進階(一)Webpack Loader 高手進階(二)Webpack Loader 高手進階(三) Webpack loader 詳解 loader 的配置 Webpack...

    MAX_zuo 評論0 收藏0
  • webpack學習()—— 進階

    摘要:上篇文章中簡單介紹了的最基本用法,且項目結構十分簡單,只有一個頁面一個頁面兩個文件。本文將介紹如何使用對具有較為規范的結構的項目進行構建。這說明監測到了文件的變化,但是我們的加載的文件并沒有變。后續還會更深入地學習,希望和大家一同進步。 上篇文章中簡單介紹了webpack的最基本用法,且項目結構十分簡單,只有一個html頁面、一個css頁面、兩個js文件。本文將介紹如何使用webpac...

    singerye 評論0 收藏0
  • webpack進階loader

    webpack的loaders是一大特色,也是很重要的一部分。這遍博客我將分類講解一些常用的laodershowImg(https://segmentfault.com/img/remote/1460000005742040); 一、loaders之 預處理 css-loader 處理css中路徑引用等問題 style-loader 動態把樣式寫入css sass-loader scss編譯器 ...

    qylost 評論0 收藏0
  • webpack進階之插件篇

    摘要:基本環境搭建就不展開講了一插件篇自動補全前綴官方是這樣說的,也就是說它是一個自動檢測兼容性給各個瀏覽器加個內核前綴的插件。 上一篇博客講解了webpack環境的基本,這一篇講解一些更深入的內容和開發技巧。基本環境搭建就不展開講了showImg(http://static.xiaomo.info/images/webpack.png); 一、插件篇 1. 自動補全css3前綴 autop...

    Tonny 評論0 收藏0

發表評論

0條評論

jackzou

|高級講師

TA的文章

閱讀更多
最新活動
閱讀需要支付1元查看
<