阅读 195

Re:从零写一个基于JS Proxy的断言库[JavaScript]

什么是断言库,如何使用它们,或是如何写一个自己的断言库。

这篇文章的主要目的是展示构建一个简易JS测试库的过程。该库有着自己的测试函数,也可以传入自定义的测试函数,支持链式调用。我们先来实现库的基本功能,随后会使用js proxy来提高库的性能。

什么是断言库

如果你曾使用过 mocha,chai,assert 或是 jest,断言库对你来说肯定不会陌生。简单来说,我们使用断言库中的断言函数来测试一个值或表达式是否如预期地一样得到正确的结果。

它们如何工作

断言函数的命名也很语义化,我们能很容易地看出下面的代码在进行什么样的测试。

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...');      // 通过测试
复制代码

从零书写断言函数

现在我们开始书写自己地断言库,我们用该库来进行数据地有效性。

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 的值,并且理论上链的调用可以无限长。

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…

关注下面的标签,发现更多相似文章
评论