文:小 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 耍耍吧~
2019年,iKcamp原創新書《Koa與Node.js開發實戰》已在京東、天貓、亞馬遜、噹噹開售啦!