所謂的加載器其實就是一個導出函數的node模塊。若是有資源須要該加載器處理,webpack就會自動調用該加載器。這個返回的函數經過上下文提供的this
能夠訪問Loader API.javascript
在咱們深刻了解不一樣種類的加載器以及他們的用法、相關案例以前,咱們先來看看在本地開發和測試加載器的三種方式吧。
想要測試一個加載器,你能夠在rules
對象裏面用path
去resolve
一個本地文件:css
{ test: /\.js$/ use: [ { loader: path.resolve('path/to/loader.js'), options: {/* ... */} } ] }
想要測試多個加載器,你能夠經過resolveLoader.modules
配置項告訴webpack應該去哪兒尋找加載器。
好比項目裏面有個目錄/loaders
:html
resolveLoader: { modules: [ 'node_modules', path.resolve(__dirname, 'loaders') ] }
最後,若是你以前已經單獨爲加載器建立了一個倉庫,不要忘了使用npm link
將加載器連接到你當前要測試的項目。java
加載器處理資源的時候只是傳入一個字符串參數,也就是那個資源文件裏面的內容。
同步加載器能夠簡單地返回一個表明被處理資源的值。不過更復雜狀況下,也能夠經過this.callback(err, values...)
返回任意數量的值。錯誤(Errors)要麼被傳入那個this.callback
函數裏面,要麼就在同步加載器裏面拋出。
加載器應該返回1個或者2個值。第一個應該是String或者Buffer類型值,表明處理中的JS代碼;第二個是一個JS對象,就是所謂的SourceMap
.node
多個加載器鏈式調用時,值得注意的就是,他們的執行順序是倒敘的---根據數組格式寫法,要麼從右向左,要麼從下向上。webpack
source map
.所以,下面的例子中,foo-loader會首先被調用,處理原始資源,而後返回值傳給bar-loader.bar-loader執行完後返回最終的JS結果,有必要的話還會返回Source map.web
{ test: /\.js/, use: [ 'bar-loader', 'foo-loader' ] }
加載器編寫須要遵循如下原則。根據重要性排列以下。其中有些只在特定場景下使用。想了解更多能夠查看隨後的詳情介紹。npm
加載器應該只作某個單一的事情。這不但使得維護更容易,同時也容許在更多場景下去鏈式使用加載器。(其實就是功能比較單一的加載器在複雜的場景下能夠被鏈式的組合使用)json
充分利用加載器能夠鏈式調用這一特性。好比,咱們應該編寫5個加載器,讓每一個加載器去處理一項任務,而不是編寫一個處理5項任務的加載器。這種分離不但使得加載器極其簡單,並且有時候還能在你原先沒有想到場景下使用。
考慮一下。使用加載器options選項或者query參數提供的數據去渲染一個模板文件的場景。這個加載器會首先讀取源模板文件,執行,並最終返回一個包含所有html代碼的字符串。然而爲了符合上面準則(簡單,單一原則),apply-loader
能夠用來簡單的連接其餘開源加載器。api
jade-loader
:將模板轉換爲一個導出函數的模塊。apply-loader
:使用加載器options執行那個函數,並返回原生的html。html-loader
:傳入html,並返回一個有效的JS模塊。事實上加載器能夠連接也就意味着他們不必定都返回JS代碼。只要加載器執行對列下一個加載器可以處理,加載器就能返回任意類型的模塊。
保持模塊化輸出。加載器產生的模塊應該遵循一樣的設計原則。
保證加載器在模塊轉換的時候不要保持狀態。買一次執行加載器都不該該收到其餘已編譯或者同一個模塊往次編譯加過的影響。
使用加載器工具包loader-utils
。它提供了不少有用的工具,尤爲重要的是獲取傳入加載器的options的工具。同時使用schema-utils
包能夠被用來保證加載器options校驗的一致性。下面有一個簡短的案列。
import { getOptions } from 'loader-utils'; import validateOptions from 'schema-utils'; const schema = { type: 'object', properties: { test: { type: 'string' } } } export default function(source) { const options = getOptions(this); validateOptions(schema, options, 'Example Loader'); // Apply some transformations to the source... return `export default ${ JSON.stringify(source) }`; };
若是加載器使用了外部資源(好比,從文件系統讀入),就必需要指明。在watch模式下,這個信息能夠用來清除失效的加載器緩存,並從新編譯。下面是使用addDependency
方法實現這一功能的一個簡短案例:
loader.js:
import path from 'path'; export default function(source) { var callback = this.async(); var headerPath = path.resolve('header.js'); this.addDependency(headerPath); fs.readFile(headerPath, 'utf-8', function(err, header) { if(err) return callback(err); callback(null, header + "\n" + source); }); };
因爲模塊的類型各類各樣,因此指定模塊依賴的方式也各有不一樣。好比在css中,咱們就使用@import
和url(....)
來指定相關依賴的。模塊系統會解析這些依賴的。
下面的兩種方法能夠完成:
require
語句。this.resolve
函數解析路徑。css-loader
加載器就是第一種方案很好的一個案例。它把樣式表文件裏面的@import和url所有轉換爲require語句去引入相關資源了。
而less-loader
因爲要處理複雜的變量和mixins,因此根本無法把每個@import給轉換爲require。所以,less-loader
在less編譯器的基礎上擴展了自定義的路徑解析邏輯。而後它利用上述第二種方案裏面的this.resolve
去解析相關依賴。
注意:若是語言只接受相對路徑的url,你可使用`~`去引用已安裝的模塊(好比`node_modules`裏的模塊)。這種狀況下看起來是這樣子的`url('~some-library/image.jpg')`.
爲了不在加載器處理的模塊裏面生成重複的代碼,應該在加載器裏面生成一個運行時文件放在獨立的模塊裏面,而後讓每個加載器處理的模塊require那個共享的運行時文件。
不要在代碼裏面使用絕對路徑,不然一旦項目根目錄遷移,之前的哈希名稱什麼的會破壞掉。loader-utils
裏面的stringifyRequest
方法能夠把絕對路徑轉換爲相對路徑。
若是你的加載器只是在別的包上封裝而成的。你應該把那個包指定爲一個peerDependency
.這樣容許開發者在package.json
裏面指定準確的版本號。
好比。sass-loader
指定node-sass
做爲peer dependency
:
"peerDependencies": { "node-sass": "^4.0.0" }
如今你已經根據上面的原則編寫好本身的加載器了,並在本地運行起來了,接下來幹嗎呢?我們運行一個簡單的單元測試以確保加載器符合咱們的預期吧。這兒咱們使用Jest
這個框架。爲了使用import / export
和async / await
咱們把babel-jest
以及一些babel預設都給安裝上。如今開始安裝並把他們保存爲devDependencies
。
npm install --save-dev jest babel-jest babel-preset-env
.babelrc
{ "presets": [[ "env", { "targets": { "node": "4" } } ]] }
咱們的加載器會處理txt格式的文件,而且僅僅只是使用傳入加載器options裏面的name
參數去替換txt文件裏面的[name]
.而後就會輸出轉換好的JS代碼咯。
src/loader.js
import { getOptions } from 'loader-utils'; export default function loader(source) { const options = getOptions(this); source = source.replace(/\[name\]/g, options.name); return `export default ${ JSON.stringify(source) }`; };
而後使用這個加載器去處理下面這個文件:
test/example.txt
Hey [name]!
請注意接下來的這一步哦,咱們要使用Node API和memory-fs
來執行webpack
。這樣就避免了把輸出結果輸出到硬盤上了,經過訪問stats
數據,咱們就能夠拿到轉換好了的模塊了。
npm install --save-dev webpack memory-fs
test/compiler.js
import path from 'path'; import webpack from 'webpack'; import memoryfs from 'memory-fs'; export default (fixture, options = {}) => { const compiler = webpack({ context: __dirname, entry: `./${fixture}`, output: { path: path.resolve(__dirname), filename: 'bundle.js', }, module: { rules: [{ test: /\.txt$/, use: { loader: path.resolve(__dirname, '../src/loader.js'), options: { name: 'Alice' } } }] } }); compiler.outputFileSystem = new memoryfs(); return new Promise((resolve, reject) => { compiler.run((err, stats) => { if (err) reject(err); resolve(stats); }); }); }
上面,咱們內聯了webpack的配置項。可是你依然可讓導出的函數接受一個webpack配置項做爲參數,這樣就可使用同一個編譯器模塊去測試不一樣的設置了。
如今,咱們就能夠編寫測試案例,並添加npm腳本,跑起來咯:
test/loader.test.js
import compiler from './compiler.js'; test('Inserts name and outputs JavaScript', async () => { const stats = await compiler('example.txt'); const output = stats.toJson().modules[0].source; expect(output).toBe(`export default "Hey Alice!\\n"`); });
package.json
"scripts": { "test": "jest" }
一切就緒,如今就能夠跑起來看看咱們的加載器測試是否經過咯。
PASS test/loader.test.js ✓ Inserts name and outputs JavaScript (229ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 1.853s, estimated 2s Ran all test suites.
成功了!如今你就能夠去開發,測試,部署你本身的加載器咯。期待你的分享!