Re:從零寫一個基於JS Proxy的斷言庫[JavaScript]

什麼是斷言庫,如何使用它們,或是如何寫一個本身的斷言庫。javascript

這篇文章的主要目的是展現構建一個簡易JS測試庫的過程。該庫有着本身的測試函數,也能夠傳入自定義的測試函數,支持鏈式調用。咱們先來實現庫的基本功能,隨後會使用js proxy來提升庫的性能。java

什麼是斷言庫

若是你曾使用過 mocha,chai,assert 或是 jest,斷言庫對你來講確定不會陌生。簡單來講,咱們使用斷言庫中的斷言函數來測試一個值或表達式是否如預期地同樣獲得正確的結果。git

它們如何工做

斷言函數的命名也很語義化,咱們能很容易地看出下面的代碼在進行什麼樣的測試。github

expect(true).to.equal(false)   // 拋出錯誤
expect(1+1).to.equal(2)        // 經過測試
複製代碼

若是沒有錯誤拋出,那代碼只是靜靜地跑了一遍。在 jest 中,沒有提供回調地斷言測試默認爲經過測試。瀏覽器

it('should equal true', () => {
  expect(true).toBe(true);
});
it('nothing to test here...');      // 經過測試
複製代碼

從零書寫斷言函數

如今咱們開始書寫本身地斷言庫,咱們用該庫來進行數據地有效性。app

enforce(2).largerThan(0).smallerThan(5);

enforce(description).anyOf({
  isEmpty: true,
  largetThan: 20
});
複製代碼

咱們向 enforce 函數中傳入待測試地值或表達式,隨後用一系列斷言函數來驗證數據是否符合咱們地要求。函數

整理思路

咱們須要完成的有如下幾點:性能

  1. 接受待測試的值😒
  2. 支持鏈式調用
  3. 數據不符合要求是拋出錯誤
  4. 可接受自定義斷言函數

咱們先來建立幾個驗證函數測試

const rules = {
  isArray: (value) => Array.isArray(value),
  largerThan: (value, compare) => value > compare,
  smallerThan: (value, compare) => value < compare,
  isEmpty: (value) => !value || Object.keys(value).length === 0
};
複製代碼

在 enforce(value) 的返回值中,須要能訪問到 value 的值。enforce(value) 後接的斷言函數的返回值也須要能訪問到 value 的值,而且理論上鍊的調用能夠無限長。ui

class Enforce {
  constructor(custom = {}) {
    this.rules = Object.assign({}, rules, custom);
    return this.enforce;
  }
  enforce = (value) => {
    this.value = value;
    return this.rules;
  }
}
const enforce = new Enforce();
enforce()
// {isArray: ƒ, largerThan: ƒ, smallerThan: ƒ, isEmpty: ƒ}
複製代碼

此時 enforce 的返回值中的函數還不能訪問 value 的值,也不能鏈式調用咱們須要返回一個 object,該 object 的鍵爲 rules 的名稱,值爲函數引用,這樣在enforce 返回的 object 中,enforce().largerThan會返回函數引用,咱們只須要提供參數即可以運行該函數。

function ruleRunner(rule, wrapped, value, ...args) {
  const result = rule(value, ...args);
  if (result !== true) {
    throw Error(`Validation failed for ${rule.name}`);
  }
  return wrapped;
}

// 接受待測試的值
function enforce(value) {
  const wrapped = {};
  Object.keys(rules).forEach( fnName => {
    // 遍歷 rules 中的規則,塞進 object 中,鍵即爲函數名,值爲一個函數
    // 函數中已經傳入了須要運行的測試函數 rules[fnName]
    // 爲了能夠無限鏈式調用,ruleRunner的返回值 wrapped 也被傳入
    // 固然還有 value 
    // args 是爲了如 largerThan 或自定義測試函數等 除了value值外,另外須要的參照值
    wrapped[fnName] = (...args) => ruleRunner(rules[fnName], wrapped, value, ...args);
  } );
  return wrapped;
}

enforce();
// {isArray: f, largerThan: f, smallerThan: f, isEmpty: f}
// 此時,enforce 的返回值爲 object
// enforce(1).largetThan 返回函數引用,咱們只須要給予參數,函數就會運行

enforce(55).largetThan(11);
// {isArray: f, largerThan: f, smallerThan: f, isEmpty: f}

enforce(55).largetThan(11).smallerThan(99);
// {isArray: f, largerThan: f, smallerThan: f, isEmpty: f}

enforce(true).isArray();
// Uncaught Error: Validation failed for isArray
複製代碼

咱們來討論一下效率問題。 每次運行 enforce 時,咱們都會遍歷一遍 rules [對rules特攻],放到另外一個函數中。若是咱們的 rules 中的規則越多,效率將會越低。

Proxy

什麼是Proxy

Proxy對象用於定義基本操做的自定義行爲。

栗子

const orig = {
  a: 'get',
  b: 'moving'
};
const handle = {
  // 在使用 new Proxy()以後,target 指代被代理的對象 即orig
  // key 爲調用對象,運行 proxy.a 時 key 的值爲 a
  get: (target, key) => {
    if ( key === 'b' ) {
      // 經過代理訪問 b 屬性將會返回另外的值
      return 'schwifty';
    } else if ( target.hasOwnProperty(key) ) {
      // 經過代理訪問 b 之外的屬性直接返回本該返回的值
      return target[key];
    } else {
      console.error('Not sure what you are looking for...');
    }
  }
};
// 用 handle 中的 get 爲 orig 進行代理
const proxy = new Proxy(orig, handle);

// 使用原 object 和 proxy 的不一樣
console.log(`${orig.a} ${orig.b}`);
// get moving

console.log(`${proxy.a} ${proxy.b}`);
// get schwifty

orig.c
// undefined

proxy.c
// Not sure what you are looking for...
複製代碼

在 enforce 中使用 proxy

class Enforce {
  constructor(custom = {}) {
    const allRules = Object.assign({}, rules, custom);
    this.allRules = allRules;
    return this.enforce.bind(this);
  }
  enforce(value) {
    const proxy = new Proxy(this.allRules, {
      // allRules 爲 this.allRules 
      get: (allRules, rule) => {
        // 判斷自己的rules和自定義規則cuntom中是否存在所要運行的rule
        // 若無 返回 undefined
        if (!allRules.hasOwnProperty(rule)){
          return allRules[rule];
        }
        // 如有 返回一個函數,幹函數接受參數並運行該 rule
        return (...args) => this.runRule(proxy, value, rule, ...args);
      }
    });
    // enforce 的返回值爲 proxy 
    // 鏈上的全部運行的 rule 都經過代理來運行
    return proxy;
  }
  runRule(proxy, value, rule, ...args) {
    const isValid = this.allRules[rule].call(proxy, value, args);
    if(isValid === true){
    // 符合預期 則返回 proxy 可繼續鏈式調用
      return proxy;
    }
    throw new Error(`${this.allRules[rule].name}: You shall not pass!`);
  }
}
const enforce = new Enforce();

enforce(55);
// Proxy {isArray: ƒ, largerThan: ƒ, smallerThan: ƒ, isEmpty: ƒ}

enforce(55).largerThan(20);
// Proxy {isArray: ƒ, largerThan: ƒ, smallerThan: ƒ, isEmpty: ƒ}

enforce(55).largerThan(200);
// Uncaught Error: largerThan: YOU SHALL NOT PASS!

enforce(55).largerThan(20).smallerThan(200)
// enforce(55).largerThan(20).smallerThan(200)

enforce(55).largerThan(20).smallerThan(200).isArray();
// Uncaught Error: isArray: YOU SHALL NOT PASS!


const enforce = new Enforce({
  hasDog: (value) => /dog/.test(value)
});

enforce('dog').hasDog();
// Proxy {isArray: ƒ, largerThan: ƒ, smallerThan: ƒ, isEmpty: ƒ, hasDog: ƒ}

enforce('cat').hasDog();
// Uncaught Error: hasDog: YOU SHALL NOT PASS!
複製代碼

目前的 Enforce 類可使用自定義規則來測試數據,而且能夠鏈式調用。

Proxy 真的有那麼好嗎

性能測試

下面是使用 benchmark.js 在兩個版本的 enforce 之間的性能對比,能夠看出Proxy版本enforce的運行時間是非Proxy版本的五分之一。

proxy-benchmark.png

兼容性

Proxy 是 ES2015 中的特性,並不兼容老版本的瀏覽器。

proxy-compatibility.png
若想要兼容老版本瀏覽器,也可使用 GoogleChrome/proxy-polyfill。proxy-polyfill 容許 IE 9+ 和 Safari 6+ 兼容 Proxy。

原文:medium.com/fiverr-engi…

相關文章
相關標籤/搜索