第一步處理rule為字元串,直接返回一個包裝類,很簡單看註釋就好了。 test 然後處理test、include、exclude,如下: checkResourceSource直接看源碼: 這個用於檢測配置來源的唯一性,後面會能看到作用,同樣作用的還有checkUseSource方法。 隨後將三個參 ...
第一步處理rule為字元串,直接返回一個包裝類,很簡單看註釋就好了。
test
然後處理test、include、exclude,如下:
if (rule.test || rule.include || rule.exclude) { // 標記使用參數 checkResourceSource("test + include + exclude"); // 沒有就是undefined condition = { test: rule.test, include: rule.include, exclude: rule.exclude }; // 處理常規參數 try { newRule.resource = RuleSet.normalizeCondition(condition); } catch (error) { throw new Error(RuleSet.buildErrorMessage(condition, error)); } }
checkResourceSource直接看源碼:
let resourceSource; // ... function checkResourceSource(newSource) { // 第一次直接跳到後面賦值 if (resourceSource && resourceSource !== newSource) throw new Error(RuleSet.buildErrorMessage(rule, new Error("Rule can only have one resource source (provided " + newSource + " and " + resourceSource + ")"))); resourceSource = newSource; }
這個用於檢測配置來源的唯一性,後面會能看到作用,同樣作用的還有checkUseSource方法。
隨後將三個參數包裝成一個對象傳入normalizeCondition方法,該方法對常規參數進行函數包裝:
class RuleSet { constructor(rules) { /**/ }; static normalizeCondition(condition) { // 假值報錯 if (!condition) throw new Error("Expected condition but got falsy value"); // 檢測給定字元串是否以這個開頭 if (typeof condition === "string") { return str => str.indexOf(condition) === 0; } // 函數直接返回 if (typeof condition === "function") { return condition; } // 正則表達式返回一個正則的test函數 if (condition instanceof RegExp) { return condition.test.bind(condition); } // 數組map遞歸處理 有一個滿足返回true if (Array.isArray(condition)) { const items = condition.map(c => RuleSet.normalizeCondition(c)); return orMatcher(items); } if (typeof condition !== "object") throw Error("Unexcepted " + typeof condition + " when condition was expected (" + condition + ")"); const matchers = []; // 對象會對每個值進行函數包裝彈入matchers中 Object.keys(condition).forEach(key => { const value = condition[key]; switch (key) { case "or": case "include": case "test": if (value) matchers.push(RuleSet.normalizeCondition(value)); break; case "and": if (value) { const items = value.map(c => RuleSet.normalizeCondition(c)); matchers.push(andMatcher(items)); } break; case "not": case "exclude": if (value) { const matcher = RuleSet.normalizeCondition(value); matchers.push(notMatcher(matcher)); } break; default: throw new Error("Unexcepted property " + key + " in condition"); } }); if (matchers.length === 0) throw new Error("Excepted condition but got " + condition); if (matchers.length === 1) return matchers[0]; return andMatcher(matchers); } }
這裡用js的rules做案例,看這個方法的返回:
class RuleSet { constructor(rules) { /**/ }; /* Example: { test: /\.js$/, loader: 'babel-loader', include: [resolve('src'), resolve('test')] } */ /* condition: { test: /\.js$/, include: ['d:\\workspace\\src', 'd:\\workspace\\test'], exclude: undefined } */ static normalizeCondition(condition) { // include返回類似於 [(str) => str.indexOf('d:\\workspace\\src') === 0,...] 的函數 if (typeof condition === "string") { return str => str.indexOf(condition) === 0; } // test參數返回了 /\.js$/.test 函數 if (condition instanceof RegExp) { return condition.test.bind(condition); } // include為數組 if (Array.isArray(condition)) { const items = condition.map(c => RuleSet.normalizeCondition(c)); return orMatcher(items); } const matchers = []; // 解析出['test','include','exclude'] Object.keys(condition).forEach(key => { const value = condition[key]; switch (key) { // 此value為一個數組 case "include": case "test": if (value) matchers.push(RuleSet.normalizeCondition(value)); break; // undefined跳過 case "exclude": if (value) { /**/ } break; default: throw new Error("Unexcepted property " + key + " in condition"); } }); return andMatcher(matchers); } }
這裡繼續看orMatcher、andMatcher函數的處理:
function orMatcher(items) { // 當一個函數被滿足條件時返回true return function(str) { for (let i = 0; i < items.length; i++) { if (items[i](str)) return true; } return false; }; } function andMatcher(items) { // 當一個條件不滿足時返回false return function(str) { for (let i = 0; i < items.length; i++) { if (!items[i](str)) return false; } return true; }; }
從字面意思也可以理解函數作用,比如說這裡的include被包裝成一個orMatcher函數,傳入的字元串無論是以數組中任何一個元素開頭都會返回true,andMatcher以及未出現的notMatcher同理。
resource
下麵是對resource參數的處理,源碼如下:
if (rule.resource) { // 如果前面檢測到了test || include || exclude參數 這裡會報錯 checkResourceSource("resource"); try { newRule.resource = RuleSet.normalizeCondition(rule.resource); } catch (error) { throw new Error(RuleSet.buildErrorMessage(rule.resource, error)); } }
可以看出這個參數與前面那個是互斥的,應該是老版API,下麵兩種方式實現是一樣的:
/* 方式1: rules:[ { test: /\.js$/, loader: 'babel-loader', include: [resolve('src'), resolve('test')] } ] */ /* 方式2: rules:[ { resource:{ test: /\.js$/, include: [resolve('src'), resolve('test')] exclude: undefined } } ] */
接下來的resourceQuery、compiler、issuer先跳過,腳手架沒有,不知道怎麼寫。
loader
下麵是另一塊主要配置loader(s),這裡loader與loaders作用一樣的,當初還頭疼啥區別:
const loader = rule.loaders || rule.loader; // 單loader情況 if (typeof loader === "string" && !rule.options && !rule.query) { checkUseSource("loader"); newRule.use = RuleSet.normalizeUse(loader.split("!"), ident); } // loader配合options或query出現 else if (typeof loader === "string" && (rule.options || rule.query)) { checkUseSource("loader + options/query"); newRule.use = RuleSet.normalizeUse({ loader: loader, options: rule.options, query: rule.query }, ident); } // options與query同時出現報錯 else if (loader && (rule.options || rule.query)) { throw new Error(RuleSet.buildErrorMessage(rule, new Error("options/query cannot be used with loaders (use options for each array item)"))); } /* 處理這種愚蠢用法時: { test: /\.css$/, loader: [{ loader: 'less-loader' }, { loader: 'css-loader' }] } */ else if (loader) { checkUseSource("loaders"); newRule.use = RuleSet.normalizeUse(loader, ident); } // 單獨出現options或者query報錯 else if (rule.options || rule.query) { throw new Error(RuleSet.buildErrorMessage(rule, new Error("options/query provided without loader (use loader + options)"))); }
之前舉例的babel-loader就是第一種單loader配置,這裡使用vue-loader嵌套的css配置作為示例。
首先normalizeUse方法如下:
static normalizeUse(use, ident) { // 單loader字元串 if (Array.isArray(use)) { return use // 如果是單loader情況這裡會返回[[loader1...],[loader2...]] .map((item, idx) => RuleSet.normalizeUse(item, `${ident}-${idx}`)) // 扁平化後變成[loader1,loader2] .reduce((arr, items) => arr.concat(items), []); } // 對象或字元串 return [RuleSet.normalizeUseItem(use, ident)]; };
先講解有options或者query的模式,這裡會把參數包裝一個對象傳入normalizeUse方法:
loader && (options || query)
// indet => 'ref-' static normalizeUseItem(item, ident) { if (typeof item === "function") return item; if (typeof item === "string") { return RuleSet.normalizeUseItemString(item); } const newItem = {}; if (item.options && item.query) throw new Error("Provided options and query in use"); if (!item.loader) throw new Error("No loader specified"); newItem.options = item.options || item.query; // 防止options:null的情況 if (typeof newItem.options === "object" && newItem.options) { // 這裡只是為了處理ident參數 if (newItem.options.ident) newItem.ident = newItem.options.ident; else newItem.ident = ident; } // 取出loader參數 const keys = Object.keys(item).filter(function(key) { return ["options", "query"].indexOf(key) < 0; }); keys.forEach(function(key) { newItem[key] = item[key]; }); /* newItem = { loader:'原字元串', ident:'ref-', (或自定義) options:{...} } */ return newItem; }
比起對test的處理,這裡就簡單的多,簡述如下:
1、嘗試取出options(query)中的ident參數,賦值給newItem的ident,沒有就賦值為預設的ref-
2、取出對象中不是options、query的鍵,賦值給newItem,這裡傳進來的鍵只有三個,剩下的就是loader,這個filter是逗我???
3、返回newItem對象
總之,不知道為什麼防止什麼意外情況而寫出來的垃圾代碼,這段代碼其實十分簡單。
單loader
第二種情況是單字元串,會對'!'進行切割調用map方法處理,再調用reduce方法扁平化為一個數組,直接看normalizeUseItemString方法:
/* Example: { test: /\.css$/, loader: 'css-loader?{opt:1}!style-loader' } 返回: [{loader:'css-loader',options:{opt:1}}, {loader:'style-loader'}] */ static normalizeUseItemString(useItemString) { // 根據'?'切割獲取loader的參數 const idx = useItemString.indexOf("?"); if (idx >= 0) { return { loader: useItemString.substr(0, idx), // 後面的作為options返回 options: useItemString.substr(idx + 1) }; } return { loader: useItemString }; }
這種就是串列調用loader的處理方式,代碼很簡單。
Tips
這裡有一點要註意,一旦loader使用了串列調用方式,不要傳options或者query參數,不然loader不會被切割解析!!!
這兩種方式只是不同的使用方法,最後的結果都是一樣的。
use
下麵是use參數的解析,估計因為這是老版的API,所以格式要求嚴格,處理比較隨便:
if (rule.use) { checkUseSource("use"); newRule.use = RuleSet.normalizeUse(rule.use, ident); }
如果不用loader(s),可以用use替代,但是需要按照格式寫,比如說上述串列簡寫的loader,在use中就需要這樣寫:
/* { test:/\.css$/, user:['css-loader','style-loader] } */
而對應options或query,需要這樣寫:
/* { test:/\.css$/, user:{ loader:'css-loader', options:'1' } } */
因為基本上不用了,所以這裡簡單看一下。
rules、oneOf
if (rule.rules) newRule.rules = RuleSet.normalizeRules(rule.rules, refs, `${ident}-rules`); if (rule.oneOf) newRule.oneOf = RuleSet.normalizeRules(rule.oneOf, refs, `${ident}-oneOf`);
這兩個用得少,也沒啥難理解的。
下一步是過濾出沒有處理的參數,添加到newRuls上:
const keys = Object.keys(rule).filter((key) => { return ["resource", "resourceQuery", "compiler", "test", "include", "exclude", "issuer", "loader", "options", "query", "loaders", "use", "rules", "oneOf"].indexOf(key) < 0; }); keys.forEach((key) => { newRule[key] = rule[key]; });
基本上用到都是test、loader、options,暫時不知道有啥額外參數。
ident
// 防止rules:[]的情況 if (Array.isArray(newRule.use)) { newRule.use.forEach((item) => { // ident來源於options/query的ident參數 if (item.ident) { refs[item.ident] = item.options; } }); }
最後這個地方是終於用到了傳進來的純凈對象refs。
如果在options中傳了ident參數,會填充這個對象,key為ident值,value為對應的options。
至此,所有rules的規則已經解析完畢,真是配置簡單處理複雜。