您好,登錄后才能下訂單哦!
1.主要目的為稍微梳理從配置到裝載的流程。另外詳解當然要加點源碼提升格調(本人菜鳥,有錯還請友善指正)
2.被的WebPack打包的文件,都被轉化為一個模塊,比如import './xxx/x.jpg'或require('./xxx/x.js')。至于具體實際怎么轉化,交由裝載機處理
3.下文會使用打字稿(勸退警告?)以方便說明有哪些選項和各個選項的值類型
模塊屬性
module.exports = { ... module: { noParse: /jquery/, rules: [ { test: /\.js/, exclude: /node_modules/, use:[ { loader: './loader1.js?num=1', options: {myoptions:false}, }, "./loader2.js?num=2", ] }, { test: /\.js/, include: /src/, loader: './loader1.js!./loader2.js', }, ] } }
上述是展示常見的配置寫法.webpack為其選項都編寫了打字稿聲明,這個模塊屬性的聲明在的WebPack /聲明中可見:
export interface ModuleOptions { // 一般下面這兩個 noParse?: RegExp[] | RegExp | Function | string[] | string; rules?: RuleSetRules; // 這些...已被廢棄,即將被刪除,不用看 defaultRules?: RuleSetRules; exprContextCritical?: boolean; exprContextRecursive?: boolean; exprContextRegExp?: boolean | RegExp; exprContextRequest?: string; strictExportPresence?: boolean; strictThisContextOnImports?: boolean; unknownContextCritical?: boolean; unknownContextRecursive?: boolean; unknownContextRegExp?: boolean | RegExp; unknownContextRequest?: string; unsafeCache?: boolean | Function; wrappedContextCritical?: boolean; wrappedContextRecursive?: boolean; wrappedContextRegExp?: RegExp; }
noParse 用于讓的WebPack跳過對這些文件的轉化,也就是他們不會被加載程序所處理(但還是會被打包并輸出到DIST目錄)
rules 核心配置,見下文
module.rules屬性
module.rules類型是RuleSetRule[],請繼續的WebPack /聲明查看其打字稿,有哪些屬性,屬性類型一目了然。
注意RuleSetConditionsRecursive這個東西在另外一個文件聲明,是interface RuleSetConditionsRecursive extends Array<import("./declarations/WebpackOptions").RuleSetCondition> {},其實就是export type RuleSetConditionsRecursive = RuleSetCondition[];,代表一個RuleSetCondition數組
意義直接貼中文文檔:模塊。
好了,上面基本是搬運打字稿聲明,結合文檔基本能知道有哪些屬性,屬性的類型和含義。下面結合源碼對文檔一些難以理解的地方補充說明。
規則集
規則的規范化(類型收斂)
由上可知一個規則對象,其屬性類型有多種可能,所以應該對其規范化,底層減少代碼的大量typeof等判斷。這是由RuleSet.js進行規范化的。下面是經過規則集處理后的一個規則對象大致形式:
// rule 對象規范化后的形狀應該是: { resource: function(), resourceQuery: function(), compiler: function(), issuer: function(), use: [ { loader: string, options: string | object, // 源碼的注釋可能是歷史遺留原因,options也可為object類型 <any>: <any> } // 下文稱呼這個為use數組的單個元素為 loader對象,規范化后它一般只有loader和options屬性 ], rules: [<rule>], oneOf: [<rule>], <any>: <any>, }
rules狀語從句:oneOf的英文用來嵌套的,里面的也是規范過的規則對象。
它這里的四個函數是的WebPack用來判斷是否需要把文件內容交給裝載器處理的。如的WebPack遇到了import './a.js',那么rule.resource('f:/a.js')===true時會才把文件交由規則中指定的裝載機去處理,resourceQuery等同理。
的這里的傳入參數'f:/a.js'就是官網所說的
條件已經兩個輸入值:
資源:請求文件的絕對路徑。它已經根據resolve規則解析。issuer :被請求資源(請求的資源)的模塊文件的絕對路徑。是導入時的位置。
首先要做的是把Rule.loader, ,Rule.options(Rule.query已廢棄,但尚未刪除),移動全部到Rule.use數組元素的對象里。主要這由static normalizeRule(rule, refs, ident)函數處理,代碼主要是處理各種“簡寫”,把值搬運到裝載器對象,做一些報錯處理,難度不大看一下即可,下面挑它里面的“條件函數”規范化來說一說。
Rule.resource規范化
由上可知這是一個“條件函數”,它是根據我們的配置中的test,include,exclude,resource規范化而生成的源碼180多行中:
if (rule.test || rule.include || rule.exclude) { checkResourceSource("test + include + exclude"); 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)); } } if (rule.resource) { checkResourceSource("resource"); try { newRule.resource = RuleSet.normalizeCondition(rule.resource); } catch (error) { throw new Error(RuleSet.buildErrorMessage(rule.resource, error)); } }
中文檔說Rule.test的英文Rule.resource.test的簡寫,實際就是這串代碼。
checkResourceSource用來檢查是否重復配置,即文檔中提到的:你如果提供了一個Rule.test選項對話,就不能再提供Rule.resource
求最后RuleSet.normalizeCondition生成一個“條件函數”,如下:
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; } if (condition instanceof RegExp) { return condition.test.bind(condition); } 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 = []; 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); }
這串代碼主要就是根據字符串,正則表達式,對象,功能類型來生成不同的“條件函數”,難度不大。
notMatcher,orMatcher,andMatcher這三個是輔助函數,看名字就知道了,實現上非常簡單,不貼源碼了。有什么不明白的邏輯,代入進去跑一跑就知道了
規則使用規范化
我們接下來要把Rule.use給規范分類中翻譯上面提到的那種形式,即讓裝載機只對象保留loader狀語從句:options這兩個屬性(當然,并不是它一定只有這兩個屬性)源碼如下:
static normalizeUse(use, ident) { if (typeof use === "function") { return data => RuleSet.normalizeUse(use(data), ident); } if (Array.isArray(use)) { return use .map((item, idx) => RuleSet.normalizeUse(item, `${ident}-${idx}`)) .reduce((arr, items) => arr.concat(items), []); } return [RuleSet.normalizeUseItem(use, ident)]; } static normalizeUseItemString(useItemString) { const idx = useItemString.indexOf("?"); if (idx >= 0) { return { loader: useItemString.substr(0, idx), options: useItemString.substr(idx + 1) }; } return { loader: useItemString, options: undefined }; } static normalizeUseItem(item, ident) { 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; if (typeof newItem.options === "object" && newItem.options) { if (newItem.options.ident) { newItem.ident = newItem.options.ident; } else { newItem.ident = ident; } } const keys = Object.keys(item).filter(function(key) { return !["options", "query"].includes(key); }); for (const key of keys) { newItem[key] = item[key]; } return newItem; }
這幾個函數比較繞,但總體來說難度不大。
這里再稍微總結幾點現象:
1.loader: './loader1!./loader2',如果在Rule.loader指明了兩個以以上裝載機,那么不可設置Rule.options,因為不知道該把這個選項傳給哪個裝載機,直接報錯
2.-loader不可省略,如babel!./loader的英文非法的,因為在webpack/lib/NormalModuleFactory.js440行左右,已經不再支持這種寫法,直接報錯叫你寫成babel-loader
3.loader: './loader1?num1=1&num2=2'將被處理成{loader: './loader', options: 'num=1&num=2'},以?進行了字符串分割,最終處理成規范化裝載機對象
規則集規范化到此結束,有興趣的可以繼續圍觀源碼的高管方法和構造函數
接下來算是番外,討論各種裝載機如何讀取我們配置的對象。
**屬性在的WebPack的傳遞與處理選項**
首先一個裝載機就是簡單的導出一個函數即可,比如上面舉例用到的
loader1.js: module.exports = function (content){ console.log(this) console.log(content) return content }
這個函數里面的這個被綁定到一個loaderContext(loader上下文)中,官方api:loader API。
直接把這個loader1.js加入到配置文件webpack.config.js里面即可,在編譯時他就會打印出一些東西。
簡單而言,就是在裝載機中,可以我們通過this.query來訪問到規范化裝載機對象options屬性。比如{loader: './loader1.js', options: 'num1=1&num=2'},那么this.query === '?num1=1&num=2'。
問題來了,這個問號哪里來的如果它是一個對象?
的WebPack通過裝載機的領先者來執行裝載機,這個問題可以去loader-runner/lib/LoaderRunner.js,在createLoaderObject函數中有這么一段:
if (obj.options === null) obj.query = ""; else if (obj.options === undefined) obj.query = ""; else if (typeof obj.options === "string") obj.query = "?" + obj.options; else if (obj.ident) { obj.query = "??" + obj.ident; } else if (typeof obj.options === "object" && obj.options.ident) obj.query = "??" + obj.options.ident; else obj.query = "?" + JSON.stringify(obj.options);
在以及runLoaders函數里面的這段:
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; } });
總結來說,當選項存在且是一個對象時,那么this.query就是這個對象;如果選項是一個字符串,那么this.query等于一個問號+這個字符串
多數裝載機讀取選項的方法
const loaderUtils=require('loader-utils') module.exports = function (content){ console.log(loaderUtils.getOptions(this)) return content }
借助架utils的讀取那么接下來走進loaderUtils.getOptions看看:
const query = loaderContext.query; if (typeof query === 'string' && query !== '') { return parseQuery(loaderContext.query); } if (!query || typeof query !== 'object') { return null; } return query;
這里只復制了關鍵代碼,它主要是做一些簡單判斷,對字符串的核心轉換在parseQuery上,接著看:
const JSON5 = require('json5'); function parseQuery(query) { if (query.substr(0, 1) !== '?') { throw new Error( "A valid query string passed to parseQuery should begin with '?'" ); } query = query.substr(1); if (!query) { return {}; } if (query.substr(0, 1) === '{' && query.substr(-1) === '}') { return JSON5.parse(query); } const queryArgs = query.split(/[,&]/g); const result = {}; queryArgs.forEach((arg) => { const idx = arg.indexOf('='); if (idx >= 0) { let name = arg.substr(0, idx); let value = decodeURIComponent(arg.substr(idx + 1)); if (specialValues.hasOwnProperty(value)) { value = specialValues[value]; } if (name.substr(-2) === '[]') { name = decodeURIComponent(name.substr(0, name.length - 2)); if (!Array.isArray(result[name])) { result[name] = []; } result[name].push(value); } else { name = decodeURIComponent(name); result[name] = value; } } else { if (arg.substr(0, 1) === '-') { result[decodeURIComponent(arg.substr(1))] = false; } else if (arg.substr(0, 1) === '+') { result[decodeURIComponent(arg.substr(1))] = true; } else { result[decodeURIComponent(arg)] = true; } } }); return result; }
使用了json5庫,以及自己的一套參數的轉換。
總結來說,只要你能確保自己使用的裝載器是通過loader-utils來獲取選項對象的,那么你可以直接給選項寫成如下字符串(inline loader中常用,如import 'loader1?a=1&b=2!./a.js'):
options: "{a: '1', b: '2'}" // 非json,是json5格式字符串,略有出入,請右轉百度 options: "list[]=1&list=2[]&a=1&b=2" // http請求中常見的url參數部分
更多示例可在的WebPack /架utils的中查看
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。