文章首發於我的github blog: Biu-blog,歡迎你們關注~javascript
Webpack 系列文章:css
Webpack Loader 高手進階(一)
Webpack Loader 高手進階(二)
Webpack Loader 高手進階(三)vue
Webpack 對於一個 module 所使用的 loader 對開發者提供了2種使用方式:java
// webpack.config.js module.exports = { ... module: { rules: [{ test: /.vue$/, loader: 'vue-loader' }, { test: /.scss$/, use: [ 'vue-style-loader', 'css-loader', { loader: 'sass-loader', options: { data: '$color: red;' } } ] }] } ... }
// module import a from 'raw-loader!../../utils.js'
2種不一樣的配置形式,在 webpack 內部有着不一樣的解析方式。此外,不一樣的配置方式也決定了最終在實際加載 module 過程當中不一樣 loader 之間相互的執行順序等。webpack
在講 loader 的匹配過程以前,首先從總體上了解下 loader 在整個 webpack 的 workflow 過程當中出現的時機。git
在一個 module 構建過程當中,首先根據 module 的依賴類型(例如 NormalModuleFactory)調用對應的構造函數來建立對應的模塊。在建立模塊的過程當中(new NormalModuleFactory()
),會根據開發者的 webpack.config 當中的 rules 以及 webpack 內置的 rules 規則實例化 RuleSet 匹配實例,這個 RuleSet 實例在 loader 的匹配過濾過程當中很是的關鍵,具體的源碼解析可參見Webpack Loader Ruleset 匹配規則解析。實例化 RuleSet 後,還會註冊2個鉤子函數:github
class NormalModuleFactory { ... // 內部嵌套 resolver 的鉤子,完成相關的解析後,建立這個 normalModule this.hooks.factory.tap('NormalModuleFactory', () => (result, callback) => { ... }) // 在 hooks.factory 的鉤子內部進行調用,實際的做用爲解析構建一共 module 所須要的 loaders 及這個 module 的相關構建信息(例如獲取 module 的 packge.json等) this.hooks.resolver.tap('NormalModuleFactory', () => (result, callback) => { ... }) ... }
當 NormalModuleFactory 實例化完成後,並在 compilation 內部調用這個實例的 create 方法開始真實開始建立這個 normalModule。首先調用hooks.factory
獲取對應的鉤子函數,接下來就調用 resolver 鉤子(hooks.resolver
)進入到了 resolve 的階段,在真正開始 resolve loader 以前,首先就是須要匹配過濾找到構建這個 module 所須要使用的全部的 loaders。首先進行的是對於 inline loaders 的處理:web
// NormalModuleFactory.js // 是否忽略 preLoader 以及 normalLoader const noPreAutoLoaders = requestWithoutMatchResource.startsWith("-!"); // 是否忽略 normalLoader const noAutoLoaders = noPreAutoLoaders || requestWithoutMatchResource.startsWith("!"); // 忽略全部的 preLoader / normalLoader / postLoader const noPrePostAutoLoaders = requestWithoutMatchResource.startsWith("!!"); // 首先解析出所須要的 loader,這種 loader 爲內聯的 loader let elements = requestWithoutMatchResource .replace(/^-?!+/, "") .replace(/!!+/g, "!") .split("!"); let resource = elements.pop(); // 獲取資源的路徑 elements = elements.map(identToLoaderRequest); // 獲取每一個loader及對應的options配置(將inline loader的寫法變動爲module.rule的寫法)
首先是根據模塊的路徑規則,例如模塊的路徑是以這些符號開頭的 !
/ -!
/ !!
來判斷這個模塊是否只是使用 inline loader,或者剔除掉 preLoader, postLoader 等規則:json
!
忽略 webpack.config 配置當中符合規則的 normalLoader-!
忽略 webpack.config 配置當中符合規則的 preLoader/normalLoader!!
忽略 webpack.config 配置當中符合規則的 postLoader/preLoader/normalLoader這幾個匹配規則主要適用於在 webpack.config 已經配置了對應模塊使用的 loader,可是針對一些特殊的 module,你可能須要單獨的定製化的 loader 去處理,而不是走常規的配置,所以可使用這些規則來進行處理。segmentfault
接下來將全部的 inline loader 轉化爲數組的形式,例如:
import 'style-loader!css-loader!stylus-loader?a=b!../../common.styl'
最終 inline loader 統一格式輸出爲:
[{ loader: 'style-loader', options: undefined }, { loader: 'css-lodaer', options: undefined }, { loader: 'stylus-loader', options: '?a=b' }]
對於 inline loader 的處理即是直接對其進行 resolve,獲取對應 loader 的相關信息:
asyncLib.parallel([ callback => this.resolveRequestArray( contextInfo, context, elements, loaderResolver, callback ), callback => { // 對這個 module 進行 resolve ... callack(null, { resouceResolveData, // 模塊的基礎信息,包含 descriptionFilePath / descriptionFileData 等(即 package.json 等信息) resource // 模塊的絕對路徑 }) } ], (err, results) => { const loaders = results[0] // 全部內聯的 loaders const resourceResolveData = results[1].resourceResolveData; // 獲取模塊的基本信息 resource = results[1].resource; // 模塊的絕對路徑 ... // 接下來就要開始根據引入模塊的路徑開始匹配對應的 loaders let resourcePath = matchResource !== undefined ? matchResource : resource; let resourceQuery = ""; const queryIndex = resourcePath.indexOf("?"); if (queryIndex >= 0) { resourceQuery = resourcePath.substr(queryIndex); resourcePath = resourcePath.substr(0, queryIndex); } // 獲取符合條件配置的 loader,具體的 ruleset 是如何匹配的請參見 ruleset 解析(https://github.com/CommanderXL/Biu-blog/issues/30) const result = this.ruleSet.exec({ resource: resourcePath, // module 的絕對路徑 realResource: matchResource !== undefined ? resource.replace(/\?.*/, "") : resourcePath, resourceQuery, // module 路徑上所帶的 query 參數 issuer: contextInfo.issuer, // 所解析的 module 的發佈者 compiler: contextInfo.compiler }); // result 爲最終根據 module 的路徑及相關匹配規則過濾後獲得的 loaders,爲 webpack.config 進行配置的 // 輸出的數據格式爲: /* [{ type: 'use', value: { loader: 'vue-style-loader', options: {} }, enforce: undefined // 可選值還有 pre/post 分別爲 pre-loader 和 post-loader }, { type: 'use', value: { loader: 'css-loader', options: {} }, enforce: undefined }, { type: 'use', value: { loader: 'stylus-loader', options: { data: '$color red' } }, enforce: undefined }] */ const settings = {}; const useLoadersPost = []; // post loader const useLoaders = []; // normal loader const useLoadersPre = []; // pre loader for (const r of result) { if (r.type === "use") { // postLoader if (r.enforce === "post" && !noPrePostAutoLoaders) { useLoadersPost.push(r.value); } else if ( r.enforce === "pre" && !noPreAutoLoaders && !noPrePostAutoLoaders ) { // preLoader useLoadersPre.push(r.value); } else if ( !r.enforce && !noAutoLoaders && !noPrePostAutoLoaders ) { // normal loader useLoaders.push(r.value); } } else if ( typeof r.value === "object" && r.value !== null && typeof settings[r.type] === "object" && settings[r.type] !== null ) { settings[r.type] = cachedMerge(settings[r.type], r.value); } else { settings[r.type] = r.value; } // 當獲取到 webpack.config 當中配置的 loader 後,再根據 loader 的類型進行分組(enforce 配置類型) // postLoader 存儲到 useLoaders 內部 // preLoader 存儲到 usePreLoaders 內部 // normalLoader 存儲到 useLoaders 內部 // 這些分組最終會決定加載一個 module 時不一樣 loader 之間的調用順序 // 當分組過程進行完以後,即開始 loader 模塊的 resolve 過程 asyncLib.parallel([ [ // resolve postLoader this.resolveRequestArray.bind( this, contextInfo, this.context, useLoadersPost, loaderResolver ), // resove normal loaders this.resolveRequestArray.bind( this, contextInfo, this.context, useLoaders, loaderResolver ), // resolve preLoader this.resolveRequestArray.bind( this, contextInfo, this.context, useLoadersPre, loaderResolver ) ], (err, results) => { ... // results[0] -> postLoader // results[1] -> normalLoader // results[2] -> preLoader // 這裏將構建 module 須要的全部類型的 loaders 按照必定順序組合起來,對應於: // [postLoader, inlineLoader, normalLoader, preLoader] // 最終 loader 所執行的順序對應爲: preLoader -> normalLoader -> inlineLoader -> postLoader // 不一樣類型 loader 上的 pitch 方法執行的順序爲: postLoader.pitch -> inlineLoader.pitch -> normalLoader.pitch -> preLoader.pitch (具體loader內部執行的機制後文會單獨講解) loaders = results[0].concat(loaders, results[1], results[2]); process.nextTick(() => { ... // 執行回調,建立 module }) } ]) } })
簡單總結下匹配的流程就是:
首先處理 inlineLoaders,對其進行解析,獲取對應的 loader 模塊的信息,接下來利用 ruleset 實例上的匹配過濾方法對 webpack.config 中配置的相關 loaders 進行匹配過濾,獲取構建這個 module 所須要的配置的的 loaders,並進行解析,這個過程完成後,便進行全部 loaders 的拼裝工做,並傳入建立 module 的回調中。
文章首發於我的github blog: Biu-blog,歡迎你們關注~