loader 和 plugins 是 webpack 系統的兩大重要組成元素。依靠對 loader、plugins 的不一樣組合搭配,咱們能夠靈活定製出高度適配自身業務的打包構建流程。javascript
loader 是 webpack 容納各種資源的一個重要手段,它用於對模塊的源代碼進行轉換,容許你在 import 或加載模塊時預處理文件,利用 loader,咱們能夠將各類類型的資源轉換成 webpack 本質接受的資源類型,如 javascript。前端
yaml 語言多用於編寫配置文件,結構與 JSON 相似,但語法格式比 JSON 更加方便簡潔。yaml 支持註釋,大小寫敏感,使用縮進來表示層級關係:vue
#對象
version: 1.2.4
#數組
author:
- Mike
- Hankle
#常量
name: "my project" #定義一個字符串
limit: 30 #定義一個數值
es6: true #定義一個布爾值
openkey: Null #定義一個null
#錨點引用
server:
base: &base
port: 8005
dev:
ip: 120.168.117.21
<<: *base
gamma:
ip: 120.168.117.22
<<: *base
複製代碼
等同於:java
{
"version": "1.2.4",
"author": ["Mike", "Hankle"],
"name": "my project",
"limit": 30,
"es6": true,
"openkey": null,
"server": {
"base": {
"port": 8005
},
"dev": {
"ip": "120.168.117.21",
"port": 8005
},
"gamma": {
"ip": "120.168.117.22",
"port": 8005
}
}
}
複製代碼
在基於 webpack 構建的應用中,若是但願可以引用 yaml 文件中的數據,就須要一個 yaml-loader 來支持編譯。通常狀況下,你都能在 npm 上找到可用的 loader,但若是萬一沒有對應的支持,或者你但願有一些自定義的轉換,那麼就須要本身編寫一個 webpack loader 了。node
loader 是一個 node 模塊,它導出爲一個函數,用於在轉換資源時調用。該函數接收一個 String/Buffer 類型的入參,並返回一個 String/Buffer 類型的返回值。一個最簡單的 loader 是這樣的:webpack
// loaders/yaml-loader.js
module.exports = function(source) {
return source;
};
複製代碼
loader 支持管道式傳遞,對同一類型的文件,咱們可使用多個 loader 進行處理,這批 loader 將按照「從下到上、從右到左」的順序執行,並之前一個 loader 的返回值做爲後一個 loader 的入參。這個機制無非是但願咱們在編寫 loader 的時候可以儘可能避免重複造輪子,只關注須要實現的核心功能。所以配置的時候,咱們能夠引入 json-loader:es6
// webpack.config.js
const path = require("path");
module.exports = {
// ...
module: {
rules: [
{
test: /\.yml$/,
use: [
{
loader: "json-loader"
},
{
loader: path.resolve(__dirname, "./loaders/yaml-loader.js")
}
]
}
]
}
};
複製代碼
這樣一來,咱們須要的 yaml-loader,就只作一件事情:將 yaml 的數據轉化成爲一個 JSON 字符串。所以,咱們能夠很簡單地實現這樣一個 yaml-loader:web
var yaml = require("js-yaml");
module.exports = function(source) {
this.cacheable && this.cacheable();
try {
var res = yaml.safeLoad(source);
return JSON.stringify(res, undefined, "\t");
} catch (err) {
this.emitError(err);
return null;
}
};
複製代碼
就是這麼簡單。可是可能有朋友會問,這裏是由於有個現成的模塊 js-yaml,能夠直接將 yaml 轉換成 JavaScript 對象,萬一沒有這個模塊,該怎麼作呢?是的,loader 的核心工做其實就是字符串的處理,這是個至關噁心的活兒,尤爲是在這類語法轉換的場景上,對源代碼的字符串處理將變得極其複雜。這個狀況下,咱們能夠考慮另一種解法,藉助 AST 語法樹,來協助咱們更加便捷地操做轉換。npm
yaml-ast-parser 是一個將 yaml 轉換成 AST 語法樹的 node 模塊,咱們把字符串解析的工做交給了 AST parser,而操做 AST 語法樹遠比操做字符串要簡單、方便得多:json
const yaml = require("yaml-ast-parser");
class YamlParser {
constructor(source) {
this.data = yaml.load(source);
this.parse();
}
parse() {
// parse ast into javascript object
}
}
module.exports = function(source) {
this.cacheable && this.cacheable();
try {
const parser = new YamlParser(source);
return JSON.stringify(parser.data, undefined, "\t");
} catch (err) {
this.emitError(err);
return null;
}
};
複製代碼
這裏咱們能夠利用 AST parser 提供的方法直接轉化出 json,若是沒有或者有所定製,也能夠手動實現一下 parse 的過程,僅僅只是一個樹結構的迭代遍歷而已,關鍵步驟是對 AST 語法樹的各種型節點分別進行處理:
const yaml = require("yaml-ast-parser");
const types = yaml.Kind;
class YamlParser {
// ...
parse() {
this.data = this.traverse(this.data);
}
traverse(node) {
const type = types[node.kind];
switch (type) {
// 對象
case "MAP": {
const ret = {};
node.mappings.forEach(mapping => {
Object.assign(ret, this.traverse(mapping));
});
return ret;
}
// 鍵值對
case "MAPPING": {
let ret = {};
// 驗證
const keyValid =
yaml.determineScalarType(node.key) == yaml.ScalarType.string;
if (!keyValid) {
throw Error("鍵值非法");
}
if (node.key.value == "<<" && types[node.value.kind] === "ANCHOR_REF") {
// 引用合併
ret = this.traverse(node.value);
} else {
ret[node.key.value] = this.traverse(node.value);
}
return ret;
}
// 常量
case "SCALAR": {
return node.valueObject !== undefined ? node.valueObject : node.value;
}
// 數組
case "SEQ": {
const ret = [];
node.items.forEach(item => {
ret.push(this.traverse(item));
});
return ret;
}
// 錨點引用
case "ANCHOR_REF": {
return this.traverse(node.value);
}
default:
throw Error("unvalid node");
}
}
}
// ...
複製代碼
固然這樣的實現略爲粗糙,正常來講,一些完備的 AST parser 通常都會自帶遍歷方法(traverse),這樣的方法都是有作過優化的,咱們能夠直接調用,儘可能避免本身手動實現。
按照相同的作法,你還能夠實現一個 markdown-loader,甚至更爲複雜的 vue-loader。
只作一件事情,作好一件事情。loader 的管道(pipeline)設計正是但願可以將任務拆解並獨立成一個個子任務,由多個 loader 分別處理,以此來保證每一個 loader 的可複用性。所以咱們在開發 loader 前必定要先給 loader 一個準確的功能定位,從通用的角度出發去設計,避免作多餘的事。
loader 應該是不保存狀態的。這樣的好處一方面是使咱們 loader 中的數據流簡單清晰,另外一方面是保證 loader 具備良好可測性。所以咱們的 loader 每次運行都不該該依賴於自身以前的編譯結果,也不該該經過除出入參外的其餘方式與其餘編譯模塊進行數據交流。固然,這並不表明 loader 必須是一個無任何反作用的純函數,loader 支持異步,所以是能夠在 loader 中有 I/O 操做的。
在開發時,loader 可能會被不斷地執行,合理的緩存可以下降重複編譯帶來的成本。loader 執行時默認是開啓緩存的,這樣一來, webpack 在編譯過程當中執行到判斷是否須要重編譯 loader 實例的時候,會直接跳過 rebuild 環節,節省沒必要要重建帶來的開銷。
當且僅當有你的 loader 有其餘不穩定的外部依賴(如 I/O 接口依賴)時,能夠關閉緩存:
this.cacheable && this.cacheable(false);
複製代碼
若是你以爲這篇內容對你有價值,歡迎點贊並關注咱們前端團隊的 官網 和咱們的微信公衆號 WecTeam,每週都有優質文章推送~