摘要:如果函數(shù)沒(méi)有返回值的話,那么進(jìn)入到下一個(gè)的函數(shù)的執(zhí)行階段。這也是異步化的一種方式如果執(zhí)行后有返回值,執(zhí)行開(kāi)始下一個(gè)執(zhí)行以上就是對(duì)于在構(gòu)建過(guò)程中執(zhí)行流程的源碼分析。
文章首發(fā)于個(gè)人github blog: Biu-blog,歡迎大家關(guān)注~
Webpack 系列文章:
Webpack Loader 高手進(jìn)階(一)
Webpack Loader 高手進(jìn)階(二)
Webpack Loader 高手進(jìn)階(三)
上篇文章主要講了 loader 的配置,匹配相關(guān)的機(jī)制。這篇主要會(huì)講當(dāng)一個(gè) module 被創(chuàng)建之后,使用 loader 去處理這個(gè) module 內(nèi)容的流程機(jī)制。首先我們來(lái)總體的看下整個(gè)的流程:
在 module 一開(kāi)始構(gòu)建的過(guò)程中,首先會(huì)創(chuàng)建一個(gè) loaderContext 對(duì)象,它和這個(gè) module 是一一對(duì)應(yīng)的關(guān)系,而這個(gè) module 所使用的所有 loaders 都會(huì)共享這個(gè) loaderContext 對(duì)象,每個(gè) loader 執(zhí)行的時(shí)候上下文就是這個(gè) loaderContext 對(duì)象,所以可以在我們寫(xiě)的 loader 里面通過(guò) this 來(lái)訪問(wèn)。
// NormalModule.js const { runLoaders } = require("loader-runner") class NormalModule extends Module { ... createLoaderContext(resolver, options, compilation, fs) { const requestShortener = compilation.runtimeTemplate.requestShortener; // 初始化 loaderContext 對(duì)象,這些初始字段的具體內(nèi)容解釋在文檔上有具體的解釋(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, // 項(xiàng)目的根路徑 webpack: true, sourceMap: !!this.useSourceMap, _module: this, _compilation: compilation, _compiler: compilation.compiler, fs: fs }; // 觸發(fā) normalModuleLoader 的鉤子函數(shù),開(kāi)發(fā)者可以利用這個(gè)鉤子來(lái)對(duì) loaderContext 進(jìn)行拓展 compilation.hooks.normalModuleLoader.call(loaderContext, this); if (options.loader) { Object.assign(loaderContext, options.loader); } return loaderContext; } doBuild(options, compilation, resolver, fs, callback) { // 創(chuàng)建 loaderContext 上下文 const loaderContext = this.createLoaderContext( resolver, options, compilation, fs ) runLoaders( { resource: this.resource, // 這個(gè)模塊的路徑 loaders: this.loaders, // 模塊所使用的 loaders context: loaderContext, // loaderContext 上下文 readResource: fs.readFile.bind(fs) // 讀取文件的 node api }, (err, result) => { // do something } ) } ... }
當(dāng) loaderContext 初始化完成后,開(kāi)始調(diào)用 runLoaders 方法,這個(gè)時(shí)候進(jìn)入到了 loaders 的執(zhí)行階段。runLoaders 方法是由loader-runner這個(gè)獨(dú)立的 npm 包提供的方法,那我們就一起來(lái)看下 runLoaders 方法內(nèi)部是如何運(yùn)行的。
首先根據(jù)傳入的參數(shù)完成進(jìn)一步的處理,同時(shí)對(duì)于 loaderContext 對(duì)象上的屬性做進(jìn)一步的拓展:
exports.runLoaders = function runLoaders(options, callback) { // read options var resource = options.resource || ""; // 模塊的路徑 var loaders = options.loaders || []; // 模塊所需要使用的 loaders var loaderContext = options.context || {}; // 在 normalModule 里面創(chuàng)建的 loaderContext var readResource = options.readResource || readFile; var splittedResource = resource && splitQuery(resource); var resourcePath = splittedResource ? splittedResource[0] : undefined; // 模塊實(shí)際路徑 var resourceQuery = splittedResource ? splittedResource[1] : undefined; // 模塊路徑 query 參數(shù) 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; // 當(dāng)前正在執(zhí)行的 loader 索引 loaderContext.loaders = loaders; loaderContext.resourcePath = resourcePath; loaderContext.resourceQuery = resourceQuery; loaderContext.async = null; // 異步 loader loaderContext.callback = null; ... // 需要被構(gòu)建的模塊路徑,將 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; } }); // 構(gòu)建這個(gè) module 所有的 loader 及這個(gè)模塊的 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("!"); } }); // 在執(zhí)行 loader 提供的 pitch 函數(shù)階段傳入的參數(shù)之一,剩下還未被調(diào)用的 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("!"); } }); // 在執(zhí)行 loader 提供的 pitch 函數(shù)階段傳入的參數(shù)之一,包含當(dāng)前 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("!"); } }); // 在執(zhí)行 loader 提供的 pitch 函數(shù)階段傳入的參數(shù)之一,包含已經(jīng)被執(zhí)行的 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("!"); } }); // 獲取當(dāng)前正在執(zhí)行的 loader 的query參數(shù) // 如果這個(gè) loader 配置了 options 對(duì)象的話,this.query 就指向這個(gè) option 對(duì)象 // 如果 loader 中沒(méi)有 options,而是以 query 字符串作為參數(shù)調(diào)用時(shí),this.query 就是一個(gè)以 ? 開(kāi)頭的字符串 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; } }); // 每個(gè) loader 在 pitch 階段和正常執(zhí)行階段都可以共享的 data 數(shù)據(jù) Object.defineProperty(loaderContext, "data", { enumerable: true, get: function() { return loaderContext.loaders[loaderContext.loaderIndex].data; } }); var processOptions = { resourceBuffer: null, // module 的內(nèi)容 buffer readResource: readResource }; // 開(kāi)始執(zhí)行每個(gè) loader 上的 pitch 函數(shù) iteratePitchingLoaders(processOptions, loaderContext, function(err, result) { // do something... }); }
這里稍微總結(jié)下就是在 runLoaders 方法的初期會(huì)對(duì)相關(guān)參數(shù)進(jìn)行初始化的操作,特別是將 loaderContext 上的部分屬性改寫(xiě)為 getter/setter 函數(shù),這樣在不同的 loader 執(zhí)行的階段可以動(dòng)態(tài)的獲取一些參數(shù)。
接下來(lái)開(kāi)始調(diào)用 iteratePitchingLoaders 方法執(zhí)行每個(gè) loader 上提供的 pitch 函數(shù)。大家寫(xiě)過(guò) loader 的話應(yīng)該都清楚,每個(gè) loader 可以掛載一個(gè) pitch 函數(shù),每個(gè) loader 提供的 pitch 方法和 loader 實(shí)際的執(zhí)行順序正好相反。這塊的內(nèi)容在 webpack 文檔上也有詳細(xì)的說(shuō)明(請(qǐng)戳我)。
這些 pitch 函數(shù)并不是用來(lái)實(shí)際處理 module 的內(nèi)容的,主要是可以利用 module 的 request,來(lái)做一些攔截處理的工作,從而達(dá)到在 loader 處理流程當(dāng)中的一些定制化的處理需要,有關(guān) pitch 函數(shù)具體的實(shí)戰(zhàn)可以參見(jiàn)下一篇文檔[Webpack 高手進(jìn)階-loader 實(shí)戰(zhàn)] TODO: 鏈接
function iteratePitchingLoaders() { // abort after last loader if(loaderContext.loaderIndex >= loaderContext.loaders.length) return processResource(options, loaderContext, callback); // 根據(jù) loaderIndex 來(lái)獲取當(dāng)前需要執(zhí)行的 loader var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex]; // iterate // 如果被執(zhí)行過(guò),那么直接跳過(guò)這個(gè) loader 的 pitch 函數(shù) if(currentLoaderObject.pitchExecuted) { loaderContext.loaderIndex++; return iteratePitchingLoaders(options, loaderContext, callback); } // 加載 loader 模塊 // load loader module loadLoader(currentLoaderObject, function(err) { // do something ... }); }
每次執(zhí)行 pitch 函數(shù)前,首先根據(jù) loaderIndex 來(lái)獲取當(dāng)前需要執(zhí)行的 loader (currentLoaderObject),調(diào)用 loadLoader 函數(shù)來(lái)加載這個(gè) loader,loadLoader 內(nèi)部兼容了 SystemJS,ES Module,CommonJs 這些模塊定義,最終會(huì)將 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() ... }
當(dāng) loader 加載完后,開(kāi)始執(zhí)行 loadLoader 的回調(diào):
loadLoader(currentLoaderObject, function(err) { var fn = currentLoaderObject.pitch; // 獲取 pitch 函數(shù) currentLoaderObject.pitchExecuted = true; if(!fn) return iteratePitchingLoaders(options, loaderContext, callback); // 如果這個(gè) loader 沒(méi)有提供 pitch 函數(shù),那么直接跳過(guò) // 開(kāi)始執(zhí)行 pitch 函數(shù) 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. // 根據(jù)是否有參數(shù)返回來(lái)判斷是否向下繼續(xù)進(jìn)行 pitch 函數(shù)的執(zhí)行 var hasArg = args.some(function(value) { return value !== undefined; }); if(hasArg) { loaderContext.loaderIndex--; iterateNormalLoaders(options, loaderContext, args, callback); } else { iteratePitchingLoaders(options, loaderContext, callback); } } ); })
這里出現(xiàn)了一個(gè) runSyncOrAsync 方法,放到后文去講,開(kāi)始執(zhí)行 pitch 函數(shù),當(dāng) pitch 函數(shù)執(zhí)行完后,執(zhí)行傳入的回調(diào)函數(shù)。我們看到回調(diào)函數(shù)里面會(huì)判斷接收到的參數(shù)的個(gè)數(shù),除了第一個(gè) err 參數(shù)外,如果還有其他的參數(shù)(這些參數(shù)是 pitch 函數(shù)執(zhí)行完后傳入回調(diào)函數(shù)的),那么會(huì)直接進(jìn)入 loader 的 normal 方法執(zhí)行階段,并且會(huì)直接跳過(guò)后面的 loader 執(zhí)行階段。如果 pitch 函數(shù)沒(méi)有返回值的話,那么進(jìn)入到下一個(gè) loader 的 pitch 函數(shù)的執(zhí)行階段。讓我們?cè)倩氐?iteratePitchingLoaders 方法內(nèi)部,當(dāng)所有 loader 上面的 pitch 函數(shù)都執(zhí)行完后,即 loaderIndex 索引值 >= loader 數(shù)組長(zhǎng)度的時(shí)候:
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 方法內(nèi)部調(diào)用 node API readResouce 讀取 module 對(duì)應(yīng)路徑的文本內(nèi)容,調(diào)用 iterateNormalLoaders 方法,開(kāi)始進(jìn)入 loader normal 方法的執(zhí)行階段。
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 之間的轉(zhuǎn)化 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 方法內(nèi)部就是依照從右到左的順序(正好與 pitch 方法執(zhí)行順序相反)依次執(zhí)行每個(gè) loader 上的 normal 方法。loader 不管是 pitch 方法還是 normal 方法的執(zhí)行可為同步的,也可設(shè)為異步的(這里說(shuō)下 normal 方法的)。一般如果你寫(xiě)的 loader 里面可能涉及到計(jì)算量較大的情況時(shí),可將你的 loader 異步化,在你 loader 方法里面調(diào)用this.async方法,返回異步的回調(diào)函數(shù),當(dāng)你 loader 內(nèi)部實(shí)際的內(nèi)容執(zhí)行完后,可調(diào)用這個(gè)異步的回調(diào)來(lái)進(jìn)入下一個(gè) loader 的執(zhí)行。
module.exports = function (content) { const callback = this.async() someAsyncOperation(content, function(err, result) { if (err) return callback(err); callback(null, result); }); }
除了調(diào)用 this.async 來(lái)異步化 loader 之外,還有一種方式就是在你的 loader 里面去返回一個(gè) promise,只有當(dāng)這個(gè) promise 被 resolve 之后,才會(huì)調(diào)用下一個(gè) loader(具體實(shí)現(xiàn)機(jī)制見(jiàn)下文):
module.exports = function (content) { return new Promise(resolve => { someAsyncOpertion(content, function(err, result) { if (err) resolve(err) resolve(null, result) }) }) }
這里還有一個(gè)地方需要注意的就是,上下游 loader 之間的數(shù)據(jù)傳遞過(guò)程中,如果下游的 loader 接收到的參數(shù)為一個(gè),那么可以在上一個(gè) loader 執(zhí)行結(jié)束后,如果是同步就直接 return 出去:
module.exports = function (content) { // do something return content }
如果是異步就直接調(diào)用異步回調(diào)傳遞下去(參見(jiàn)上面 loader 異步化)。如果下游 loader 接收的參數(shù)多于一個(gè),那么上一個(gè) loader 執(zhí)行結(jié)束后,如果是同步那么就需要調(diào)用 loaderContext 提供的 callback 函數(shù):
module.exports = function (content) { // do something this.callback(null, content, argA, argB) }
如果是異步的還是繼續(xù)調(diào)用異步回調(diào)函數(shù)傳遞下去(參見(jiàn)上面 loader 異步化)。具體的執(zhí)行機(jī)制涉及到上文還沒(méi)講到的 runSyncOrAsync 方法,它提供了上下游 loader 調(diào)用的接口:
function runSyncOrAsync(fn, context, args, callback) { var isSync = true; // 是否為同步 var isDone = false; var isError = false; // internal error var reportedError = false; // 給 loaderContext 上下文賦值 async 函數(shù),用以將 loader 異步化,并返回異步回調(diào) context.async = function async() { if(isDone) { if(reportedError) return; // ignore throw new Error("async(): The callback was already called."); } isSync = false; // 同步標(biāo)志位置為 false return innerCallback; }; // callback 的形式可以向下一個(gè) loader 多個(gè)參數(shù) 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 { // 開(kāi)始執(zhí)行 loader var result = (function LOADER_EXECUTION() { return fn.apply(context, args); }()); // 如果為同步的執(zhí)行 if(isSync) { isDone = true; // 如果 loader 執(zhí)行后沒(méi)有返回值,執(zhí)行 callback 開(kāi)始下一個(gè) loader 執(zhí)行 if(result === undefined) return callback(); // loader 返回值為一個(gè) promise 實(shí)例,待這個(gè)實(shí)例被resolve或者reject后執(zhí)行下一個(gè) loader。這也是 loader 異步化的一種方式 if(result && typeof result === "object" && typeof result.then === "function") { return result.catch(callback).then(function(r) { callback(null, r); }); } // 如果 loader 執(zhí)行后有返回值,執(zhí)行 callback 開(kāi)始下一個(gè) loader 執(zhí)行 return callback(null, result); } } catch(e) { // do something } }
以上就是對(duì)于 module 在構(gòu)建過(guò)程中 loader 執(zhí)行流程的源碼分析??赡芷綍r(shí)在使用 webpack 過(guò)程了解相關(guān)的 loader 執(zhí)行規(guī)則和策略,再配合這篇對(duì)于內(nèi)部機(jī)制的分析,應(yīng)該會(huì)對(duì) webpack loader 的使用有更加深刻的印象。
文章首發(fā)于個(gè)人github blog: Biu-blog,歡迎大家關(guān)注~
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/109092.html
摘要:相關(guān)的內(nèi)容為這樣對(duì)于一個(gè)處理的第二階段也就結(jié)束了,通過(guò)去攔截不同類(lèi)型的,并返回新的,跳過(guò)后面的的執(zhí)行,同時(shí)在內(nèi)部會(huì)剔除掉,這樣在進(jìn)入到下一個(gè)處理階段的時(shí)候,不在使用的范圍之內(nèi),因此下一階段便不會(huì)經(jīng)由來(lái)處理。 文章首發(fā)于個(gè)人github blog: Biu-blog,歡迎大家關(guān)注~ Webpack 系列文章: Webpack Loader 高手進(jìn)階(一)Webpack Loader 高手...
摘要:在一個(gè)構(gòu)建過(guò)程中,首先根據(jù)的依賴類(lèi)型例如調(diào)用對(duì)應(yīng)的構(gòu)造函數(shù)來(lái)創(chuàng)建對(duì)應(yīng)的模塊。 文章首發(fā)于個(gè)人github blog: Biu-blog,歡迎大家關(guān)注~ Webpack 系列文章: Webpack Loader 高手進(jìn)階(一)Webpack Loader 高手進(jìn)階(二)Webpack Loader 高手進(jìn)階(三) Webpack loader 詳解 loader 的配置 Webpack...
摘要:上篇文章中簡(jiǎn)單介紹了的最基本用法,且項(xiàng)目結(jié)構(gòu)十分簡(jiǎn)單,只有一個(gè)頁(yè)面一個(gè)頁(yè)面兩個(gè)文件。本文將介紹如何使用對(duì)具有較為規(guī)范的結(jié)構(gòu)的項(xiàng)目進(jìn)行構(gòu)建。這說(shuō)明監(jiān)測(cè)到了文件的變化,但是我們的加載的文件并沒(méi)有變。后續(xù)還會(huì)更深入地學(xué)習(xí),希望和大家一同進(jìn)步。 上篇文章中簡(jiǎn)單介紹了webpack的最基本用法,且項(xiàng)目結(jié)構(gòu)十分簡(jiǎn)單,只有一個(gè)html頁(yè)面、一個(gè)css頁(yè)面、兩個(gè)js文件。本文將介紹如何使用webpac...
webpack的loaders是一大特色,也是很重要的一部分。這遍博客我將分類(lèi)講解一些常用的laodershowImg(https://segmentfault.com/img/remote/1460000005742040); 一、loaders之 預(yù)處理 css-loader 處理css中路徑引用等問(wèn)題 style-loader 動(dòng)態(tài)把樣式寫(xiě)入css sass-loader scss編譯器 ...
摘要:基本環(huán)境搭建就不展開(kāi)講了一插件篇自動(dòng)補(bǔ)全前綴官方是這樣說(shuō)的,也就是說(shuō)它是一個(gè)自動(dòng)檢測(cè)兼容性給各個(gè)瀏覽器加個(gè)內(nèi)核前綴的插件。 上一篇博客講解了webpack環(huán)境的基本,這一篇講解一些更深入的內(nèi)容和開(kāi)發(fā)技巧?;经h(huán)境搭建就不展開(kāi)講了showImg(http://static.xiaomo.info/images/webpack.png); 一、插件篇 1. 自動(dòng)補(bǔ)全css3前綴 autop...
閱讀 2480·2021-10-11 10:57
閱讀 1347·2021-10-09 09:59
閱讀 2055·2019-08-30 15:53
閱讀 3255·2019-08-30 15:53
閱讀 1064·2019-08-30 15:45
閱讀 791·2019-08-30 15:44
閱讀 3500·2019-08-30 14:24
閱讀 998·2019-08-30 14:21