如何實現一個webpack yaml-loader

1、什麼是 loader

loader 和 plugins 是 webpack 系統的兩大重要組成元素。依靠對 loader、plugins 的不一樣組合搭配,咱們能夠靈活定製出高度適配自身業務的打包構建流程。javascript

loader 是 webpack 容納各種資源的一個重要手段,它用於對模塊的源代碼進行轉換,容許你在 import 或加載模塊時預處理文件,利用 loader,咱們能夠將各類類型的資源轉換成 webpack 本質接受的資源類型,如 javascript。前端

2、如何編寫一個 yaml-loader

一、YAML

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 的原理

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

四、利用 AST 做源碼轉換

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。

3、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,每週都有優質文章推送~

相關文章
相關標籤/搜索