Loader(加載器) 是 webpack 的核心之一。它用於將不一樣類型的文件轉換爲 webpack 可識別的模塊。本文將嘗試深刻探索 webpack 中的 loader,揭祕它的工做原理,以及如何開發一個 loader。javascript
webpack 只能直接處理 javascript 格式的代碼。任何非 js 文件都必須被預先處理轉換爲 js 代碼,才能夠參與打包。loader(加載器)就是這樣一個代碼轉換器。它由 webpack 的 loader runner
執行調用,接收原始資源數據做爲參數(當多個加載器聯合使用時,上一個loader的結果會傳入下一個loader),最終輸出 javascript 代碼(和可選的 source map)給 webpack 作進一步編譯。css
pre > normal > inline > post
。從右到左,從下到上
。內聯 loader 能夠經過添加不一樣前綴,跳過其餘類型 loader。html
!
跳過 normal loader。-!
跳過 pre 和 normal loader。!!
跳過 pre、 normal 和 post loader。這些前綴在不少場景下很是有用。java
loader 是一個導出一個函數的 node 模塊。node
當只有一個 loader 應用於資源文件時,它接收源碼做爲參數,輸出轉換後的 js 代碼。webpack
// loaders/simple-loader.js module.exports = function loader (source) { console.log('simple-loader is working'); return source; }
這就是一個最簡單的 loader 了,這個 loader 啥也沒幹,就是接收源碼,而後原樣返回,爲了證實這個loader被調用了,我在裏面打印了一句話‘simple-loader is working’。git
測試這個 loader:
須要先配置 loader 路徑
如果使用 npm 安裝的第三方 loader,直接寫 loader 的名字就能夠了。可是如今用的是本身開發的本地 loader,須要咱們手動配置路徑,告訴 webpack 這些 loader 在哪裏。github
// webpack.config.js const path = require('path'); module.exports = { entry: {...}, output: {...}, module: { rules: [ { test: /\.js$/, // 直接指明 loader 的絕對路徑 use: path.resolve(__dirname, 'loaders/simple-loader') } ] } }
若是以爲這樣配置本地 loader 並不優雅,能夠在 webpack配置本地loader的四種方法 中挑一個你喜歡的。
執行webpack編譯
能夠看到,控制檯輸出 ‘simple-loader is working’。說明 loader 成功被調用。web
pitch
是 loader 上的一個方法,它的做用是阻斷 loader 鏈。shell
// loaders/simple-loader-with-pitch.js module.exports = function (source) { console.log('normal excution'); return source; } // loader上的pitch方法,非必須 module.exports.pitch = function() { console.log('pitching graph'); // todo }
pitch 方法不是必須的。若是有 pitch,loader 的執行則會分爲兩個階段:pitch
階段 和 normal execution
階段。webpack 會先從左到右執行 loader 鏈中的每一個 loader 上的 pitch 方法(若是有),而後再從右到左執行 loader 鏈中的每一個 loader 上的普通 loader 方法。
假如配置了以下 loader 鏈:
use: ['loader1', 'loader2', 'loader3']
真實的 loader 執行過程是:
在這個過程當中若是任何 pitch 有返回值,則 loader 鏈被阻斷。webpack 會跳事後面全部的的 pitch 和 loader,直接進入上一個 loader 的 normal execution
。
假設在 loader2 的 pitch 中返回了一個字符串,此時 loader 鏈發生阻斷:
style-loader 一般不會獨自使用,而是跟 css-loader 連用。css-loader 的返回值是一個 js 模塊,大體長這樣:
// 打印 css-loader 的返回值 // Imports var ___CSS_LOADER_API_IMPORT___ = require("../node_modules/css-loader/dist/runtime/api.js"); exports = ___CSS_LOADER_API_IMPORT___(false); // Module exports.push([module.id, "\nbody {\n background: yellow;\n}\n", ""]); // Exports module.exports = exports;
這個模塊在運行時上下文中執行後返回 css
代碼 "\nbody {\n background: yellow;\n}\n"
。
style-loader 的做用就是將這段 css
代碼轉成 style
標籤插入到 html
的 head
中。
js
腳本:在腳本中建立一個 style
標籤,將 css
代碼賦給 style
標籤,再將這個 style
標籤插入 html
的 head
中。css
代碼,由於 css-loader 的返回值只能在運行時的上下文中執行,而執行 loader 是在編譯階段。換句話說,css-loader 的返回值在 style-loader 裏派不上用場。css
代碼的表達式,在運行時再獲取 css (相似 require('css-loader!index.css')
)。inline loader
require css
文件,會產生循環執行 loader 的問題,因此咱們須要利用 pitch
方法,讓 style-loader 在 pitch
階段返回腳本,跳過剩下的 loader,同時還須要內聯前綴 !!
的加持。注:pitch 方法有3個參數:
!
做爲鏈接符組成的字符串。!
做爲鏈接符組成的字符串。
能夠利用
remainingRequest
參數獲取 loader 鏈的剩餘部分。
// loaders/simple-style-loader.js const loaderUtils = require('loader-utils'); module.exports = function(source) { // do nothing } module.exports.pitch = function(remainingRequest) { console.log('simple-style-loader is working'); // 在 pitch 階段返回腳本 return ( ` // 建立 style 標籤 let style = document.createElement('style'); /** * 利用 remainingRequest 參數獲取 loader 鏈的剩餘部分 * 利用 ‘!!’ 前綴跳過其餘 loader * 利用 loaderUtils 的 stringifyRequest 方法將模塊的絕對路徑轉爲相對路徑 * 將獲取 css 的 require 表達式賦給 style 標籤 */ style.innerHTML = require(${loaderUtils.stringifyRequest(this, '!!' + remainingRequest)}); // 將 style 標籤插入 head document.head.appendChild(style); ` ) }
一個簡易的 style-loader 就完成了。
webpack 配置
// webpack.config.js const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: {...}, output: {...}, // 手動配置 loader 路徑 resolveLoader: { modules: [path.resolve(__dirname, 'loaders'), 'node_modules'] }, module: { rules: [ { // 配置處理 css 的 loader test: /\.css$/, use: ['simple-style-loader', 'css-loader'] } ] }, plugins: [ // 渲染首頁 new HtmlWebpackPlugin({ template: './src/index.html' }) ] }
在 index.js 中引入一個 css 樣式文件
// src/index.js require('./index.css'); console.log('Brovo!');
樣式文件中將 body 的背景色設置爲黃色
// src/index.css body { background-color: yellow; }
執行webpack
npm run build
能夠看到命令行控制檯打印了 'simple-style-loader is working',說明 webpack 成功調用了咱們編寫的 loader。
在瀏覽器打開 dist 下的 index.html 頁面,能夠看到樣式生效,並且成功插入到了頁面頭部!
說明咱們編寫的 loader 發揮做用了。
成功!
開發 loader 必備:
1. loader-utils
這個模塊中經常使用的幾個方法:
2. schema-utils
這個模塊能夠幫你驗證 loader option 配置的合法性。
用法:
// loaders/simple-loader-with-validate.js const loaderUtils = require('loader-utils'); const validate = require('schema-utils'); module.exports = function(source) { // 獲取 loader 配置項 let options = loaderUtils.getOptions(this) || {}; // 定義配置項結構和類型 let schema = { type: 'object', properties: { name: { type: 'string' } } } // 驗證配置項是否符合要求 validate(schema, options); return source; }
當配置項不符合要求,編譯就會中斷並在控制檯打印錯誤信息:
異步 loader 的開發(例如裏面有一些須要讀取文件的操做的時候),須要經過 this.async() 獲取異步回調,而後手動調用它。
用法:
// loaders/simple-async-loader.js module.exports = function(source) { console.log('async loader'); let cb = this.async(); setTimeout(() => { console.log('ok'); // 在異步回調中手動調用 cb 返回處理結果 cb(null, source); }, 3000); }
注: 異步回調 cb() 的第一個參數是
error
,要返回的結果放在第二個參數。
若是是處理圖片、字體等資源的 loader,須要將 loader 上的 raw 屬性設置爲 true,讓 loader 支持二進制格式資源(webpack默認是以 utf-8
的格式讀取文件內容給 loader)。
用法:
// loaders/simple-raw-loader.js module.exports = function(source) { // 將輸出 buffer 類型的二進制數據 console.log(source); // todo handle source let result = 'results of processing source' return ` module.exports = '${result}' `; } // 告訴 wepack 這個 loader 須要接收的是二進制格式的數據 module.exports.raw = true;
注:一般 raw 屬性會在有文件輸出需求的 loader 中使用。
在開發一些處理資源文件(好比圖片、字體等)的 loader 中,須要拷貝或者生成新的文件,可使用內部的 this.emitFile()
方法.
用法:
// loaders/simple-file-loader.js const loaderUtils = require('loader-utils'); module.exports = function(source) { // 獲取 loader 的配置項 let options = loaderUtils.getOptions(this) || {}; // 獲取用戶設置的文件名或者製做新的文件名 // 注意第三個參數,是計算 contenthash 的依據 let url = loaderUtils.interpolateName(this, options.filename || '[contenthash].[ext]', {content: source}); // 輸出文件 this.emitFile(url, source); // 返回導出文件地址的模塊腳本 return `module.exports = '${JSON.stringify(url)}'`; } module.exports.raw = true;
在這個例子中,loader 讀取圖片內容(buffer),將其重命名,而後調用this.emitFile()
輸出到指定目錄,最後返回一個模塊,這個模塊導出重命名後的圖片地址。因而當require
圖片的時候,就至關於 require 了一個模塊,從而獲得最終的圖片路徑。(這就是 file-loader 的基本原理)
爲了讓咱們的 loader 具備更高的質量和複用性,記得保持簡單。也就是儘可能保持讓一個 loader 專一一件事情,若是發現你寫的 loader 比較龐大,能夠試着將其拆成幾個 loader 。
在 webpack 社區,有一份 loader 開發準則,咱們能夠去參考它來指導咱們的 loader 設計:
文章源碼獲取:https://github.com/yc111/webp...
歡迎交流~
Happy New Year!
--
參考
https://webpack.js.org/concep...
https://webpack.js.org/api/lo...
https://webpack.js.org/contri...
https://github.com/webpack/we...
https://github.com/webpack-co...
https://www.npmjs.com/package...
https://www.npmjs.com/package...
歡迎轉載,轉載請註明出處:
https://champyin.com/2020/01/...