自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Webpack 原理系列:如何編寫loader

開發(fā) 前端
Loader 是一個帶有副作用的內(nèi)容轉(zhuǎn)譯器!那么為什么需要做這種轉(zhuǎn)換呢?本篇就帶大家了解相關(guān)內(nèi)容。

[[402465]]

關(guān)于 Webpack Loader,網(wǎng)上已經(jīng)有很多很多的資料,很難講出花來,但是要寫 Webpack 的系列博文又沒辦法繞開這一點,所以我閱讀了超過 20 個開源項目,盡量全面地總結(jié)了一些編寫 Loader 時需要了解的知識和技巧。包含:

那么,我們開始吧。

認識 Loader

  • 如果要做總結(jié)的話,我認為 Loader 是一個帶有副作用的內(nèi)容轉(zhuǎn)譯器!

Webpack Loader 最核心的只能是實現(xiàn)內(nèi)容轉(zhuǎn)換器 —— 將各式各樣的資源轉(zhuǎn)化為標(biāo)準(zhǔn) JavaScript 內(nèi)容格式,例如:

  • css-loader 將 css 轉(zhuǎn)換為 __WEBPACK_DEFAULT_EXPORT__ = ".a{ xxx }"格式
  • html-loader 將 html 轉(zhuǎn)換為 __WEBPACK_DEFAULT_EXPORT__ = "
  • vue-loader 更復(fù)雜一些,會將 .vue 文件轉(zhuǎn)化為多個 JavaScript 函數(shù),分別對應(yīng) template、js、css、custom block

那么為什么需要做這種轉(zhuǎn)換呢?本質(zhì)上是因為 Webpack 只認識符合 JavaScript 規(guī)范的文本(Webpack 5之后增加了其它 parser):在構(gòu)建(make)階段,解析模塊內(nèi)容時會調(diào)用 acorn 將文本轉(zhuǎn)換為 AST 對象,進而分析代碼結(jié)構(gòu),分析模塊依賴;這一套邏輯對圖片、json、Vue SFC等場景就不 work 了,就需要 Loader 介入將資源轉(zhuǎn)化成 Webpack 可以理解的內(nèi)容形態(tài)。

  • Plugin 是 Webpack 另一套擴展機制,功能更強,能夠在各個對象的鉤子中插入特化處理邏輯,它可以覆蓋 Webpack 全生命流程,能力、靈活性、復(fù)雜度都會比 Loader 強很多,我們下次再講。

Loader 基礎(chǔ)

代碼層面,Loader 通常是一個函數(shù),結(jié)構(gòu)如下:

  1. module.exports = function(source, sourceMap?, data?) { 
  2.   // source 為 loader 的輸入,可能是文件內(nèi)容,也可能是上一個 loader 處理結(jié)果 
  3.   return source; 
  4. }; 

Loader 函數(shù)接收三個參數(shù),分別為:

  • source:資源輸入,對于第一個執(zhí)行的 loader 為資源文件的內(nèi)容;后續(xù)執(zhí)行的 loader 則為前一個 loader 的執(zhí)行結(jié)果
  • sourceMap: 可選參數(shù),代碼的 sourcemap 結(jié)構(gòu)
  • data: 可選參數(shù),其它需要在 Loader 鏈中傳遞的信息,比如 posthtml/posthtml-loader 就會通過這個參數(shù)傳遞參數(shù)的 AST 對象

其中 source 是最重要的參數(shù),大多數(shù) Loader 要做的事情就是將 source 轉(zhuǎn)譯為另一種形式的 output ,比如 webpack-contrib/raw-loader 的核心源碼:

  1. //...  
  2. export default function rawLoader(source) { 
  3.   // ... 
  4.  
  5.   const json = JSON.stringify(source) 
  6.     .replace(/\u2028/g, '\\u2028'
  7.     .replace(/\u2029/g, '\\u2029'); 
  8.  
  9.   const esModule = 
  10.     typeof options.esModule !== 'undefined' ? options.esModule : true
  11.  
  12.   return `${esModule ? 'export default' : 'module.exports ='} ${json};`; 

這段代碼的作用是將文本內(nèi)容包裹成 JavaScript 模塊,例如:

  1. // source 
  2. I am Tecvan 
  3.  
  4. // output 
  5. module.exports = "I am Tecvan" 

經(jīng)過模塊化包裝之后,這段文本內(nèi)容轉(zhuǎn)身變成 Webpack 可以處理的資源模塊,其它 module 也就能引用、使用它了。

返回多個結(jié)果

上例通過 return 語句返回處理結(jié)果,除此之外 Loader 還可以以 callback 方式返回更多信息,供下游 Loader 或者 Webpack 本身使用,例如在 webpack-contrib/eslint-loader 中:

  1. export default function loader(content, map) { 
  2.   // ... 
  3.   linter.printOutput(linter.lint(content)); 
  4.   this.callback(null, content, map); 

通過 this.callback(null, content, map) 語句同時返回轉(zhuǎn)譯后的內(nèi)容與 sourcemap 內(nèi)容。callback 的完整簽名如下:

  1. this.callback( 
  2.     // 異常信息,Loader 正常運行時傳遞 null 值即可 
  3.     err: Error | null
  4.     // 轉(zhuǎn)譯結(jié)果 
  5.     content: string | Buffer, 
  6.     // 源碼的 sourcemap 信息 
  7.     sourceMap?: SourceMap, 
  8.     // 任意需要在 Loader 間傳遞的值 
  9.     // 經(jīng)常用來傳遞 ast 對象,避免重復(fù)解析 
  10.     data?: any 
  11. ); 

異步處理涉

及到異步或 CPU 密集操作時,Loader 中還可以以異步形式返回處理結(jié)果,例如 webpack-contrib/less-loader 的核心邏輯:

  1. import less from "less"
  2.  
  3. async function lessLoader(source) { 
  4.   // 1. 獲取異步回調(diào)函數(shù) 
  5.   const callback = this.async(); 
  6.   // ... 
  7.  
  8.   let result; 
  9.  
  10.   try { 
  11.     // 2. 調(diào)用less 將模塊內(nèi)容轉(zhuǎn)譯為 css 
  12.     result = await (options.implementation || less).render(data, lessOptions); 
  13.   } catch (error) { 
  14.     // ... 
  15.   } 
  16.  
  17.   const { css, imports } = result; 
  18.  
  19.   // ... 
  20.  
  21.   // 3. 轉(zhuǎn)譯結(jié)束,返回結(jié)果 
  22.   callback(null, css, map); 
  23.  
  24. export default lessLoader; 

在 less-loader 中,邏輯分三步:

  • 調(diào)用 this.async 獲取異步回調(diào)函數(shù),此時 Webpack 會將該 Loader 標(biāo)記為異步加載器,會掛起當(dāng)前執(zhí)行隊列直到 callback 被觸發(fā)
  • 調(diào)用 less 庫將 less 資源轉(zhuǎn)譯為標(biāo)準(zhǔn) css
  • 調(diào)用異步回調(diào) callback 返回處理結(jié)果

this.async 返回的異步回調(diào)函數(shù)簽名與上一節(jié)介紹的 this.callback 相同,此處不再贅述。

緩存

Loader 為開發(fā)者提供了一種便捷的擴展方法,但在 Loader 中執(zhí)行的各種資源內(nèi)容轉(zhuǎn)譯操作通常都是 CPU 密集型 —— 這放在單線程的 Node 場景下可能導(dǎo)致性能問題;又或者異步 Loader 會掛起后續(xù)的加載器隊列直到異步 Loader 觸發(fā)回調(diào),稍微不注意就可能導(dǎo)致整個加載器鏈條的執(zhí)行時間過長。

為此,默認情況下 Webpack 會緩存 Loader 的執(zhí)行結(jié)果直到資源或資源依賴發(fā)生變化,開發(fā)者需要對此有個基本的理解,必要時可以通過 this.cachable 顯式聲明不作緩存,例如:

  1. module.exports = function(source) { 
  2.   this.cacheable(false); 
  3.   // ... 
  4.   return output
  5. }; 

上下文與 Side Effect

除了作為內(nèi)容轉(zhuǎn)換器外,Loader 運行過程還可以通過一些上下文接口,有限制地影響 Webpack 編譯過程,從而產(chǎn)生內(nèi)容轉(zhuǎn)換之外的副作用。

上下文信息可通過 this 獲取,this 對象由 NormolModule.createLoaderContext 函數(shù)在調(diào)用 Loader 前創(chuàng)建,常用的接口包括:

  1. const loaderContext = { 
  2.     // 獲取當(dāng)前 Loader 的配置信息 
  3.     getOptions: schema => {}, 
  4.     // 添加警告 
  5.     emitWarning: warning => {}, 
  6.     // 添加錯誤信息,注意這不會中斷 Webpack 運行 
  7.     emitError: error => {}, 
  8.     // 解析資源文件的具體路徑 
  9.     resolve(context, request, callback) {}, 
  10.     // 直接提交文件,提交的文件不會經(jīng)過后續(xù)的chunk、module處理,直接輸出到 fs 
  11.     emitFile: (name, content, sourceMap, assetInfo) => {}, 
  12.     // 添加額外的依賴文件 
  13.     // watch 模式下,依賴文件發(fā)生變化時會觸發(fā)資源重新編譯 
  14.     addDependency(dep) {}, 
  15. }; 

其中,addDependency、emitFile 、emitError、emitWarning 都會對后續(xù)編譯流程產(chǎn)生副作用,例如 less-loader 中包含這樣一段代碼:

  1. try { 
  2.   result = await (options.implementation || less).render(data, lessOptions); 
  3. } catch (error) { 
  4.   // ... 
  5.  
  6. const { css, imports } = result; 
  7.  
  8. imports.forEach((item) => { 
  9.   // ... 
  10.   this.addDependency(path.normalize(item)); 
  11. }); 

解釋一下,代碼中首先調(diào)用 less 編譯文件內(nèi)容,之后遍歷所有 import 語句,也就是上例 result.imports 數(shù)組,一一調(diào)用 this.addDependency 函數(shù)將 import 到的其它資源都注冊為依賴,之后這些其它資源文件發(fā)生變化時都會觸發(fā)重新編譯。

Loader 鏈?zhǔn)秸{(diào)用

使用上,可以為某種資源文件配置多個 Loader,Loader 之間按照配置的順序從前到后(pitch),再從后到前依次執(zhí)行,從而形成一套內(nèi)容轉(zhuǎn)譯工作流,例如對于下面的配置:

  1. module.exports = { 
  2.   module: { 
  3.     rules: [ 
  4.       { 
  5.         test: /\.less$/i, 
  6.         use: [ 
  7.           "style-loader"
  8.           "css-loader"
  9.           "less-loader"
  10.         ], 
  11.       }, 
  12.     ], 
  13.   }, 
  14. }; 

這是一個典型的 less 處理場景,針對 .less 后綴的文件設(shè)定了:less、css、style 三個 loader 協(xié)作處理資源文件,按照定義的順序,Webpack 解析 less 文件內(nèi)容后先傳入 less-loader;less-loader 返回的結(jié)果再傳入 css-loader 處理;css-loader 的結(jié)果再傳入 style-loader;最終以 style-loader 的處理結(jié)果為準(zhǔn),流程簡化后如:

上述示例中,三個 Loader 分別起如下作用:

  • less-loader:實現(xiàn) less => css 的轉(zhuǎn)換,輸出 css 內(nèi)容,無法被直接應(yīng)用在 Webpack 體系下
  • css-loader:將 css 內(nèi)容包裝成類似 module.exports = "${css}" 的內(nèi)容,包裝后的內(nèi)容符合 JavaScript 語法
  • style-loader:做的事情非常簡單,就是將 css 模塊包進 require 語句,并在運行時調(diào)用 injectStyle 等函數(shù)將內(nèi)容注入到頁面的 style 標(biāo)簽

三個 Loader 分別完成內(nèi)容轉(zhuǎn)化工作的一部分,形成從右到左的調(diào)用鏈條。鏈?zhǔn)秸{(diào)用這種設(shè)計有兩個好處,一是保持單個 Loader 的單一職責(zé),一定程度上降低代碼的復(fù)雜度;二是細粒度的功能能夠被組裝成復(fù)雜而靈活的處理鏈條,提升單個 Loader 的可復(fù)用性。

不過,這只是鏈?zhǔn)秸{(diào)用的一部分,這里面有兩個問題:

  • Loader 鏈條一旦啟動之后,需要所有 Loader 都執(zhí)行完畢才會結(jié)束,沒有中斷的機會 —— 除非顯式拋出異常
  • 某些場景下并不需要關(guān)心資源的具體內(nèi)容,但 Loader 需要在 source 內(nèi)容被讀取出來之后才會執(zhí)行

為了解決這兩個問題,Webpack 在 loader 基礎(chǔ)上疊加了 pitch 的概念。

Loader Pitch

網(wǎng)絡(luò)上關(guān)于 Loader 的文章已經(jīng)有非常非常多,但多數(shù)并沒有對 pitch 這一重要特性做足夠深入的介紹,沒有講清楚為什么要設(shè)計 pitch 這個功能,pitch 有哪些常見用例等。

在這一節(jié),我會從 what、how、why 三個維度展開聊聊 loader pitch 這一特性。

什么是 pitch

Webpack 允許在這個函數(shù)上掛載名為 pitch 的函數(shù),運行時 pitch 會比 Loader 本身更早執(zhí)行,例如:

  1. const loader = function (source){ 
  2.     console.log('后執(zhí)行'
  3.     return source; 
  4.  
  5. loader.pitch = function(requestString) { 
  6.     console.log('先執(zhí)行'
  7.  
  8. module.exports = loader 

Pitch 函數(shù)的完整簽名:

  1. function pitch( 
  2.     remainingRequest: string, previousRequest: string, data = {} 
  3. ): void { 

包含三個參數(shù):

  • remainingRequest : 當(dāng)前 loader 之后的資源請求字符串
  • previousRequest : 在執(zhí)行當(dāng)前 loader 之前經(jīng)歷過的 loader 列表
  • data : 與 Loader 函數(shù)的 data 相同,用于傳遞需要在 Loader 傳播的信息

這些參數(shù)不復(fù)雜,但與 requestString 緊密相關(guān),我們看個例子加深了解:

  1. module.exports = { 
  2.   module: { 
  3.     rules: [ 
  4.       { 
  5.         test: /\.less$/i, 
  6.         use: [ 
  7.           "style-loader""css-loader""less-loader" 
  8.         ], 
  9.       }, 
  10.     ], 
  11.   }, 
  12. }; 

css-loader.pitch 中拿到的參數(shù)依次為:

  1. // css-loader 之后的 loader 列表及資源路徑 
  2. remainingRequest = less-loader!./xxx.less 
  3. // css-loader 之前的 loader 列表 
  4. previousRequest = style-loader 
  5. // 默認值 
  6. data = {} 

調(diào)度邏輯

Pitch 翻譯成中文是拋、球場、力度、事物最高點等,我覺得 pitch 特性之所以被忽略完全是這個名字的鍋,它背后折射的是一整套 Loader 被執(zhí)行的生命周期概念。

實現(xiàn)上,Loader 鏈條執(zhí)行過程分三個階段:pitch、解析資源、執(zhí)行,設(shè)計上與 DOM 的事件模型非常相似,pitch 對應(yīng)到捕獲階段;執(zhí)行對應(yīng)到冒泡階段;而兩個階段之間 Webpack 會執(zhí)行資源內(nèi)容的讀取、解析操作,對應(yīng) DOM 事件模型的 AT_TARGET 階段:


pitch 階段按配置順序從左到右逐個執(zhí)行 loader.pitch 函數(shù)(如果有的話),開發(fā)者可以在 pitch 返回任意值中斷后續(xù)的鏈路的執(zhí)行:


那么為什么要設(shè)計 pitch 這一特性呢?在分析了 style-loader、vue-loader、to-string-loader 等開源項目之后,我個人總結(jié)出兩個字:「阻斷」!

示例:style-loader

先回顧一下前面提到過的 less 加載鏈條:

  • less-loader :將 less 規(guī)格的內(nèi)容轉(zhuǎn)換為標(biāo)準(zhǔn) css
  • css-loader :將 css 內(nèi)容包裹為 JavaScript 模塊
  • style-loader :將 JavaScript 模塊的導(dǎo)出結(jié)果以 link 、style 標(biāo)簽等方式掛載到 html 中,讓 css 代碼能夠正確運行在瀏覽器上

實際上, style-loader 只是負責(zé)讓 css 能夠在瀏覽器環(huán)境下跑起來,本質(zhì)上并不需要關(guān)心具體內(nèi)容,很適合用 pitch 來處理,核心代碼:

  1. // ... 
  2. // Loader 本身不作任何處理 
  3. const loaderApi = () => {}; 
  4.  
  5. // pitch 中根據(jù)參數(shù)拼接模塊代碼 
  6. loaderApi.pitch = function loader(remainingRequest) { 
  7.   //... 
  8.  
  9.   switch (injectType) { 
  10.     case 'linkTag': { 
  11.       return `${ 
  12.         esModule 
  13.           ? `...` 
  14.           // 引入 runtime 模塊 
  15.           : `var api = require(${loaderUtils.stringifyRequest( 
  16.               this, 
  17.               `!${path.join(__dirname, 'runtime/injectStylesIntoLinkTag.js')}` 
  18.             )}); 
  19.             // 引入 css 模塊 
  20.             var content = require(${loaderUtils.stringifyRequest( 
  21.               this, 
  22.               `!!${remainingRequest}` 
  23.             )}); 
  24.  
  25.             content = content.__esModule ? content.default : content;` 
  26.       } // ...`; 
  27.     } 
  28.  
  29.     case 'lazyStyleTag'
  30.     case 'lazySingletonStyleTag': { 
  31.         //... 
  32.     } 
  33.  
  34.     case 'styleTag'
  35.     case 'singletonStyleTag'
  36.     default: { 
  37.         // ... 
  38.     } 
  39.   } 
  40. }; 
  41.  
  42. export default loaderApi; 

關(guān)鍵點:

  • loaderApi 為空函數(shù),不做任何處理
  • loaderApi.pitch 中拼接結(jié)果,導(dǎo)出的代碼包含:

引入運行時模塊 runtime/injectStylesIntoLinkTag.js復(fù)用 remainingRequest 參數(shù),重新引入 css 文件

運行結(jié)果大致如:

  1. var api = require('xxx/style-loader/lib/runtime/injectStylesIntoLinkTag.js'
  2. var content = require('!!css-loader!less-loader!./xxx.less'); 

注意了,到這里 style-loader 的 pitch 函數(shù)返回這一段內(nèi)容,后續(xù)的 Loader 就不會繼續(xù)執(zhí)行,當(dāng)前調(diào)用鏈條中斷了:

之后,Webpack 繼續(xù)解析、構(gòu)建 style-loader 返回的結(jié)果,遇到 inline loader 語句:

  1. var content = require('!!css-loader!less-loader!./xxx.less'); 

所以從 Webpack 的角度看,實際上對同一個文件調(diào)用了兩次 loader 鏈,第一次在 style-loader 的 pitch 中斷,第二次根據(jù) inline loader 的內(nèi)容跳過了 style-loader。

相似的技巧在其它倉庫也有出現(xiàn),比如 vue-loader,感興趣的同學(xué)可以查看我之前發(fā)在 ByteFE 公眾號上的文章《Webpack 案例 ——vue-loader 原理分析》,這里就不展開講了。

進階技巧

開發(fā)工具

Webpack 為 Loader 開發(fā)者提供了兩個實用工具,在諸多開源 Loader 中出現(xiàn)頻率極高:

webpack/loader-utils:提供了一系列諸如讀取配置、requestString 序列化與反序列化、計算 hash 值之類的工具函數(shù)

webpack/schema-utils:參數(shù)校驗工具

這些工具的具體接口在相應(yīng)的 readme 上已經(jīng)有明確的說明,不贅述,這里總結(jié)一些編寫 Loader 時經(jīng)常用到的樣例:如何獲取并校驗用戶配置;如何拼接輸出文件名。

獲取并校驗配置

Loader 通常都提供了一些配置項,供開發(fā)者定制運行行為,用戶可以通過 Webpack 配置文件的 use.options 屬性設(shè)定配置,例如:

  1. module.exports = { 
  2.   module: { 
  3.     rules: [{ 
  4.       test: /\.less$/i, 
  5.       use: [ 
  6.         { 
  7.           loader: "less-loader"
  8.           options: { 
  9.             cacheDirectory: false 
  10.           } 
  11.         }, 
  12.       ], 
  13.     }], 
  14.   }, 
  15. }; 

在 Loader 內(nèi)部,需要使用 loader-utils 庫的 getOptions 函數(shù)獲取用戶配置,用 schema-utils 庫的 validate 函數(shù)校驗參數(shù)合法性,例如 css-loader:

  1. // css-loader/src/index.js 
  2. import { getOptions } from "loader-utils"
  3. import { validate } from "schema-utils"
  4. import schema from "./options.json"
  5.  
  6.  
  7. export default async function loader(content, map, meta) { 
  8.   const rawOptions = getOptions(this); 
  9.  
  10.   validate(schema, rawOptions, { 
  11.     name"CSS Loader"
  12.     baseDataPath: "options"
  13.   }); 
  14.   // ... 

使用 schema-utils 做校驗時需要提前聲明配置模板,通常會處理成一個額外的 json 文件,例如上例中的 "./options.json"。

拼接輸出文件名

Webpack 支持以類似 [path]/[name]-[hash].js 方式設(shè)定 output.filename即輸出文件的命名,這一層規(guī)則通常不需要關(guān)注,但某些場景例如 webpack-contrib/file-loader 需要根據(jù) asset 的文件名拼接結(jié)果。

file-loader 支持在 JS 模塊中引入諸如 png、jpg、svg 等文本或二進制文件,并將文件寫出到輸出目錄,這里面有一個問題:假如文件叫 a.jpg ,經(jīng)過 Webpack 處理后輸出為 [hash].jpg ,怎么對應(yīng)上呢?此時就可以使用 loader-utils 提供的 interpolateName 在 file-loader 中獲取資源寫出的路徑及名稱,源碼:

  1. import { getOptions, interpolateName } from 'loader-utils'
  2.  
  3. export default function loader(content) { 
  4.   const context = options.context || this.rootContext; 
  5.   const name = options.name || '[contenthash].[ext]'
  6.  
  7.   // 拼接最終輸出的名稱 
  8.   const url = interpolateName(this, name, { 
  9.     context, 
  10.     content, 
  11.     regExp: options.regExp, 
  12.   }); 
  13.  
  14.   let outputPath = url; 
  15.   // ... 
  16.  
  17.   let publicPath = `__webpack_public_path__ + ${JSON.stringify(outputPath)}`; 
  18.   // ... 
  19.  
  20.   if (typeof options.emitFile === 'undefined' || options.emitFile) { 
  21.     // ... 
  22.  
  23.     // 提交、寫出文件 
  24.     this.emitFile(outputPath, content, null, assetInfo); 
  25.   } 
  26.   // ... 
  27.  
  28.   const esModule = 
  29.     typeof options.esModule !== 'undefined' ? options.esModule : true
  30.  
  31.   // 返回模塊化內(nèi)容 
  32.   return `${esModule ? 'export default' : 'module.exports ='} ${publicPath};`; 
  33.  
  34. export const raw = true

代碼的核心邏輯:

  1. 根據(jù) Loader 配置,調(diào)用 interpolateName 方法拼接目標(biāo)文件的完整路徑
  2. 調(diào)用上下文 this.emitFile 接口,寫出文件
  3. 返回 module.exports = ${publicPath} ,其它模塊可以引用到該文件路徑

除 file-loader 外,css-loader、eslint-loader 都有用到該接口,感興趣的同學(xué)請自行前往查閱源碼。

單元測試

在 Loader 中編寫單元測試收益非常高,一方面對開發(fā)者來說不用去怎么寫 demo,怎么搭建測試環(huán)境;一方面對于最終用戶來說,帶有一定測試覆蓋率的項目通常意味著更高、更穩(wěn)定的質(zhì)量。

閱讀了超過 20 個開源項目后,我總結(jié)了一套 Webpack Loader 場景下常用的單元測試流程,以 Jest · 🃏 Delightful JavaScript Testing 為例:

  1. 創(chuàng)建在 Webpack 實例,并運行 Loader
  2. 獲取 Loader 執(zhí)行結(jié)果,比對、分析判斷是否符合預(yù)期
  3. 判斷執(zhí)行過程中是否出錯

如何運行 Loader

有兩種辦法,一是在 node 環(huán)境下運行調(diào)用 Webpack 接口,用代碼而非命令行執(zhí)行編譯,很多框架都會采用這種方式,例如 vue-loader、stylus-loader、babel-loader 等,優(yōu)點的運行效果最接近最終用戶,缺點是運行效率相對較低(可以忽略)。

以 posthtml/posthtml-loader 為例,它會在啟動測試之前創(chuàng)建并運行 Webpack 實例:

  1. // posthtml-loader/test/helpers/compiler.js 文件 
  2. module.exports = function (fixture, config, options) { 
  3.   config = { /*...*/ } 
  4.  
  5.   options = Object.assign({ outputfalse }, options) 
  6.  
  7.   // 創(chuàng)建 Webpack 實例 
  8.   const compiler = webpack(config) 
  9.  
  10.   // 以 MemoryFS 方式輸出構(gòu)建結(jié)果,避免寫磁盤 
  11.   if (!options.output) compiler.outputFileSystem = new MemoryFS() 
  12.  
  13.   // 執(zhí)行,并以 promise 方式返回結(jié)果 
  14.   return new Promise((resolve, reject) => compiler.run((err, stats) => { 
  15.     if (err) reject(err) 
  16.     // 異步返回執(zhí)行結(jié)果 
  17.     resolve(stats) 
  18.   })) 
  • 小技巧:如上例所示,用 compiler.outputFileSystem = new MemoryFS()語句將 Webpack 設(shè)定成輸出到內(nèi)存,能避免寫盤操作,提升編譯速度。

另外一種方法是編寫一系列 mock 方法,搭建起一個模擬的 Webpack 運行環(huán)境,例如 emaphp/underscore-template-loader ,優(yōu)點的運行速度更快,缺點是開發(fā)工作量大通用性低,了解了解即可。

比對結(jié)果

上例運行結(jié)束之后會以 resolve(stats) 方式返回執(zhí)行結(jié)果,stats 對象中幾乎包含了編譯過程所有信息,包括耗時、產(chǎn)物、模塊、chunks、errors、warnings 等等,我在之前的文章 分享幾個 Webpack 實用分析工具 對此已經(jīng)做了較深入的介紹,感興趣的同學(xué)可以前往閱讀。

在測試場景下,可以從 stats 對象中讀取編譯最終輸出的產(chǎn)物,例如 style-loader 的實現(xiàn):

  1. // style-loader/src/test/helpers/readAsset.js 文件 
  2. function readAsset(compiler, stats, assets) => { 
  3.   const usedFs = compiler.outputFileSystem 
  4.   const outputPath = stats.compilation.outputOptions.path 
  5.   const queryStringIdx = targetFile.indexOf('?'
  6.  
  7.   if (queryStringIdx >= 0) { 
  8.     // 解析出輸出文件路徑 
  9.     asset = asset.substr(0, queryStringIdx) 
  10.   } 
  11.  
  12.   // 讀文件內(nèi)容 
  13.   return usedFs.readFileSync(path.join(outputPath, targetFile)).toString() 

解釋一下,這段代碼首先計算 asset 輸出的文件路徑,之后調(diào)用 outputFileSystem 的 readFile 方法讀取文件內(nèi)容。

接下來,有兩種分析內(nèi)容的方法:

  • 調(diào)用 Jest 的 expect(xxx).toMatchSnapshot() 斷言判斷當(dāng)前運行結(jié)果是否與之前的運行結(jié)果一致,從而確保多次修改的結(jié)果一致性,很多框架都大量用了這種方法
  • 解讀資源內(nèi)容,判斷是否符合預(yù)期,例如 less-loader 的單元測試中會對同一份代碼跑兩次 less 編譯,一次由 Webpack 執(zhí)行,一次直接調(diào)用 less 庫,之后分析兩次運行結(jié)果是否相同

對此有興趣的同學(xué),強烈建議看看 less-loader 的 test 目錄。

異常判斷

最后,還需要判斷編譯過程是否出現(xiàn)異常,同樣可以從 stats 對象解析:

  1. export default getErrors = (stats) => { 
  2.   const errors = stats.compilation.errors.sort() 
  3.   return errors.map( 
  4.     e => e.toString() 
  5.   ) 

大多數(shù)情況下都希望編譯沒有錯誤,此時只要判斷結(jié)果數(shù)組是否為空即可。某些情況下可能需要判斷是否拋出特定異常,此時可以 expect(xxx).toMatchSnapshot() 斷言,用快照對比更新前后的結(jié)果。

調(diào)試

開發(fā) Loader 的過程中,有一些小技巧能夠提升調(diào)試效率,包括:

  • 使用 ndb 工具實現(xiàn)斷點調(diào)試
  • 使用 npm link 將 Loader 模塊鏈接到測試項目
  • 使用 resolveLoader 配置項將 Loader 所在的目錄加入到測試項目中,如:
  1. // webpack.config.js 
  2. module.exports = { 
  3.   resolveLoader:{ 
  4.     modules: ['node_modules','./loaders/'], 
  5.   } 

 【編輯推薦】

 

責(zé)任編輯:姜華 來源: Tecvan
相關(guān)推薦

2021-12-17 00:02:28

Webpack資源加載

2021-04-30 08:28:15

WebpackLoaderPlugin

2021-09-13 09:40:35

Webpack 前端HMR 原理

2021-08-26 10:30:29

WebpackTree-Shakin前端

2021-06-28 05:59:17

Webpack 前端打包與工程化

2020-10-30 15:04:16

開發(fā)技能代碼

2020-08-05 08:21:41

Webpack

2021-12-16 22:02:28

webpack原理模塊化

2021-12-24 08:01:44

Webpack優(yōu)化打包

2021-08-12 09:48:21

Webpack Loa工具Webpack

2021-12-19 07:21:48

Webpack 前端插件機制

2021-06-22 10:43:03

Webpack loader plugin

2021-12-20 00:03:38

Webpack運行機制

2021-12-25 22:29:04

WebpackRollup 前端

2021-11-09 09:57:46

Webpack 前端分包優(yōu)化

2016-10-08 20:58:50

awkLinux編寫腳本

2021-04-19 10:45:52

Webpack熱更新前端

2021-11-15 09:44:49

Webpack 前端 Scope Hois

2021-12-15 09:21:59

Webpack 前端Sourcemap

2021-10-25 10:23:49

Webpack 前端Tree shakin
點贊
收藏

51CTO技術(shù)棧公眾號