系列做者:肖磊javascript
GitHub: github.com/CommanderXLcss
本篇來分析下 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 去處理,而不是走常規的配置,所以可使用這些規則來進行處理。數組
接下來將全部的 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 的回調中。