文:小 boy(滬江網校Web前端工程師)css
本文原創,轉載請註明做者及出處html
常常逛 webpack 官網的同窗應該會很眼熟上面的圖。正如它宣傳的同樣,webpack 能把左側各類類型的文件(webpack 把它們叫做「模塊」)統一打包爲右邊被通用瀏覽器支持的文件。webpack 就像是魔術師的帽子,放進去一條絲巾,變出來一隻白鴿。那這個「魔術」的過程是如何實現的呢?今天咱們從 webpack 的核心概念之一 —— loader 來尋找答案,並着手實現這個「魔術」。看完本文,你能夠:前端
在擼一個 loader 前,咱們須要先知道它究竟是什麼。本質上來講,loader 就是一個 node 模塊,這很符合 webpack 中「萬物皆模塊」的思路。既然是 node 模塊,那就必定會導出點什麼。在 webpack 的定義中,loader 導出一個函數,loader 會在轉換源模塊(resource)的時候調用該函數。在這個函數內部,咱們能夠經過傳入 this
上下文給 Loader API 來使用它們。回顧一下頭圖左邊的那些模塊,他們就是所謂的源模塊,會被 loader 轉化爲右邊的通用文件,所以咱們也能夠歸納一下 loader 的功能:把源模塊轉換成通用模塊。node
知道它的強大功能之後,咱們要怎麼使用 loader 呢?webpack
既然 loader 是 webpack 模塊,若是咱們要使其生效,確定離不開配置。我這裏收集了三種配置方法,任你挑選。git
增長 config.module.rules
數組中的規則對象(rule object)。github
let webpackConfig = { //... module: { rules: [{ test: /\.js$/, use: [{ //這裏寫 loader 的路徑 loader: path.resolve(__dirname, 'loaders/a-loader.js'), options: {/* ... */} }] }] } }
增長 config.module.rules
數組中的規則對象以及 config.resolveLoader
。web
let webpackConfig = { //... module: { rules: [{ test: /\.js$/, use: [{ //這裏寫 loader 名便可 loader: 'a-loader', options: {/* ... */} }, { loader: 'b-loader', options: {/* ... */} }] }] }, resolveLoader: { // 告訴 webpack 該去那個目錄下找 loader 模塊 modules: ['node_modules', path.resolve(__dirname, 'loaders')] } }
也能夠經過 npm link
鏈接到你的項目裏,這個方式相似 node CLI 工具開發,非 loader 模塊專用,本文就很少討論了。npm
配置完成後,當你在 webpack 項目中引入模塊時,匹配到 rule (例如上面的 /\.js$/
)就會啓用對應的 loader (例如上面的 a-loader 和 b-loader)。這時,假設咱們是 a-loader 的開發者,a-loader 會導出一個函數,這個函數接受的惟一參數是一個包含源文件內容的字符串。咱們暫且稱它爲「source」。json
接着咱們在函數中處理 source 的轉化,最終返回處理好的值。固然返回值的數量和返回方式依據 a-loader 的需求來定。通常狀況下能夠經過 return
返回一個值,也就是轉化後的值。若是須要返回多個參數,則須調用 this.callback(err, values...)
來返回。在異步 loader 中你能夠經過拋錯來處理異常狀況。Webpack 建議咱們返回 1 至 2 個參數,第一個參數是轉化後的 source,能夠是 string 或 buffer。第二個參數可選,是用來看成 SourceMap 的對象。
一般咱們處理一類源文件的時候,單一的 loader是不夠用的(loader 的設計原則咱們稍後講到)。通常咱們會將多個 loader 串聯使用,相似工廠流水線,一個位置的工人(或機器)只幹一種類型的活。既然是串聯,那確定有順序的問題,webpack 規定 use 數組中 loader 的執行順序是從最後一個到第一個,它們符合下面這些規則:
咱們舉個例子:
webpack.config.js
{ test: /\.js/, use: [ 'bar-loader', 'mid-loader', 'foo-loader' ] }
在上面的配置中:
瞭解了基本模式後,咱們先不急着開發。所謂磨刀不誤砍柴工,咱們先看看開發一個 loader 須要注意些什麼,這樣能夠少走彎路,提升開發質量。下面是 webpack 提供的幾點指南,它們按重要程度排序,注意其中有些點只適用特定狀況。
一個 loader 只作一件事,這樣不只可讓 loader 的維護變得簡單,還能讓 loader 以不一樣的串聯方式組合出符合場景需求的搭配。
這一點是第一點的延伸。好好利用 loader 的鏈式組合的特型,能夠收穫意想不到的效果。具體來講,寫一個能一次幹 5 件事情的 loader ,不如細分紅 5 個只能幹一件事情的 loader,也許其中幾個能用在其餘你暫時還沒想到的場景。下面咱們來舉個例子。
假設如今咱們要實現經過 loader 的配置和 query 參數來渲染模版的功能。咱們在 「apply-loader」 裏面實現這個功能,它負責編譯源模版,最終輸出一個導出 HTML 字符串的模塊。根據鏈式組合的規則,咱們能夠結合另外兩個開源 loader:
jade-loader
把模版源文件轉化爲導出一個函數的模塊。apply-loader
把 loader options 傳給上面的函數並執行,返回 HTML 文本。html-loader
接收 HTMl 文本文件,轉化爲可被引用的 JS 模塊。事實上串聯組合中的 loader 並不必定要返回 JS 代碼。只要下游的 loader 能有效處理上游 loader 的輸出,那麼上游的 loader 能夠返回任意類型的模塊。
保證 loader 是模塊化的。loader 生成模塊須要遵循和普通模塊同樣的設計原則。
在屢次模塊的轉化之間,咱們不該該在 loader 中保留狀態。每一個 loader 運行時應該確保與其餘編譯好的模塊保持獨立,一樣也應該與前幾個 loader 對相同模塊的編譯結果保持獨立。
請好好利用 loader-utils
包,它提供了不少有用的工具,最經常使用的一個就是獲取傳入 loader 的 options。除了 loader-utils
以外包還有 schema-utils
包,咱們能夠用 schema-utils
提供的工具,獲取用於校驗 options 的 JSON Schema 常量,從而校驗 loader 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'); // 在這裏寫轉換 source 的邏輯 ... return `export default ${ JSON.stringify(source) }`; };
若是咱們在 loader 中用到了外部資源(也就是從文件系統中讀取的資源),咱們必須聲明這些外部資源的信息。這些信息用於在監控模式(watch mode)下驗證可緩存的 loder 以及從新編譯。下面這個例子簡要地說明了怎麼使用 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 至關於異步版的 return callback(null, header + "\n" + source); }); };
不一樣的模塊會以不一樣的形式指定依賴。好比在 CSS 中咱們使用 @import
和 url(...)
聲明來完成指定,而咱們應該讓模塊系統解析這些依賴。
如何讓模塊系統解析不一樣聲明方式的依賴呢?下面有兩種方法:
require
聲明。this.resolve
函數來解析路徑。對於第一種方式,有一個很好的例子就是 css-loader
。它把 @import
聲明轉化爲 require
樣式表文件,把 url(...)
聲明轉化爲 require
被引用文件。
而對於第二種方式,則須要參考一下 less-loader
。因爲要追蹤 less 中的變量和 mixin,咱們須要把全部的 .less
文件一次編譯完畢,因此不能把每一個 @import
轉爲 require
。所以,less-loader
用自定義路徑解析邏輯拓展了 less 編譯器。這種方式運用了咱們剛纔提到的第二種方式 —— this.resolve
經過 webpack 來解析依賴。
若是某種語言只支持相對路徑(例如
url(file)
指向./file
)。你能夠用~
將相對路徑指向某個已經安裝好的目錄(例如node_modules
)下,所以,拿url
舉例,它看起來會變成這樣:url(~some-library/image.jpg)
。
避免在多個 loader 裏面初始化一樣的代碼,請把這些共用代碼提取到一個運行時文件裏,而後經過 require
把它引進每一個 loader。
不要在 loader 模塊裏寫絕對路徑,由於當項目根路徑變了,這些路徑會干擾 webpack 計算 hash(把 module 的路徑轉化爲 module 的引用 id)。loader-utils
裏有一個 stringifyRequest
方法,它能夠把絕對路徑轉化爲相對路徑。
若是你開發的 loader 只是簡單包裝另一個包,那麼你應該在 package.json 中將這個包設爲同伴依賴(peerDependency)。這可讓應用開發者知道該指定哪一個具體的版本。
舉個例子,以下所示 sass-loader
將 node-sass
指定爲同伴依賴:
"peerDependencies": { "node-sass": "^4.0.0" }
以上咱們已經爲砍柴磨好了刀,接下來,咱們動手開發一個 loader。
若是咱們要在項目開發中引用模版文件,那麼壓縮 html 是十分常見的需求。分解以上需求,解析模版、壓縮模版其實能夠拆分給兩給 loader 來作(單一職責),前者較爲複雜,咱們就引入開源包 html-loader
,然後者,咱們就拿來練手。首先,咱們給它取個響亮的名字 —— html-minify-loader
。
接下來,按照以前介紹的步驟,首先,咱們應該配置 webpack.config.js
,讓 webpack 能識別咱們的 loader。固然,最最開始,咱們要建立 loader 的 文件 —— src/loaders/html-minify-loader.js
。
因而,咱們在配置文件中這樣處理:
webpack.config.js
module: { rules: [{ test: /\.html$/, use: ['html-loader', 'html-minify-loader'] // 處理順序 html-minify-loader => html-loader => webpack }] }, resolveLoader: { // 由於 html-loader 是開源 npm 包,因此這裏要添加 'node_modules' 目錄 modules: [path.join(__dirname, './src/loaders'), 'node_modules'] }
接下來,咱們提供示例 html 和 js 來測試 loader:
src/example.html
:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> </body> </html>
src/app.js
:
var html = require('./expamle.html'); console.log(html);
好了,如今咱們着手處理 src/loaders/html-minify-loader.js
。前面咱們說過,loader 也是一個 node 模塊,它導出一個函數,該函數的參數是 require 的源模塊,處理 source 後把返回值交給下一個 loader。因此它的 「模版」 應該是這樣的:
module.exports = function (source) { // 處理 source ... return handledSource; }
或
module.exports = function (source) { // 處理 source ... this.callback(null, handledSource) return handledSource; }
注意:若是是處理順序排在最後一個的 loader,那麼它的返回值將最終交給 webpack 的
require
,換句話說,它必定是一段可執行的 JS 腳本 (用字符串來存儲),更準確來講,是一個 node 模塊的 JS 腳本,咱們來看下面的例子。
// 處理順序排在最後的 loader module.exports = function (source) { // 這個 loader 的功能是把源模塊轉化爲字符串交給 require 的調用方 return 'module.exports = ' + JSON.stringify(source); }
整個過程至關於這個 loader 把源文件
這裏是 source 模塊
轉化爲
// example.js module.exports = '這裏是 source 模塊';
而後交給 require 調用方:
// applySomeModule.js var source = require('example.js'); console.log(source); // 這裏是 source 模塊
而咱們本次串聯的兩個 loader 中,解析 html 、轉化爲 JS 執行腳本的任務已經交給 html-loader
了,咱們來處理 html 壓縮問題。
做爲普通 node 模塊的 loader 能夠垂手可得地引用第三方庫。咱們使用 minimize
這個庫來完成核心的壓縮功能:
// src/loaders/html-minify-loader.js var Minimize = require('minimize'); module.exports = function(source) { var minimize = new Minimize(); return minimize.parse(source); };
固然, minimize 庫支持一系列的壓縮參數,好比 comments 參數指定是否須要保留註釋。咱們確定不能在 loader 裏寫死這些配置。那麼 loader-utils
就該發揮做用了:
// src/loaders/html-minify-loader.js var loaderUtils = require('loader-utils'); var Minimize = require('minimize'); module.exports = function(source) { var options = loaderUtils.getOptions(this) || {}; //這裏拿到 webpack.config.js 的 loader 配置 var minimize = new Minimize(options); return minimize.parse(source); };
這樣,咱們能夠在 webpack.config.js 中設置壓縮後是否須要保留註釋:
module: { rules: [{ test: /\.html$/, use: ['html-loader', { loader: 'html-minify-loader', options: { comments: false } }] }] }, resolveLoader: { // 由於 html-loader 是開源 npm 包,因此這裏要添加 'node_modules' 目錄 modules: [path.join(__dirname, './src/loaders'), 'node_modules'] }
固然,你還能夠把咱們的 loader 寫成異步的方式,這樣不會阻塞其餘編譯進度:
var Minimize = require('minimize'); var loaderUtils = require('loader-utils'); module.exports = function(source) { var callback = this.async(); if (this.cacheable) { this.cacheable(); } var opts = loaderUtils.getOptions(this) || {}; var minimize = new Minimize(opts); minimize.parse(source, callback); };
你能夠在這個倉庫查看相關代碼,npm start
之後能夠去 http://localhost:9000
打開控制檯查看 loader 處理後的內容。
到這裏,對於「如何開發一個 loader」,我相信你已經有了本身的答案。總結一下,一個 loader 在咱們項目中 work 須要經歷如下步驟:
require
指定類型文件時,咱們能讓處理流通過指定 laoder。最後,Talk is cheep,趕忙動手擼一個 loader 耍耍吧~