6 个 ES6 代理使用案例

来源:开源中国社区 作者:oschina
  

这就是我?代理好像在ES6的其他特性的大肆宣传下迷失了自我。

这可能是因为代理太慢或者是只限于支持Safari(没有版本支持),Node(v6是第一个支持的),还有转译器(Babel/TypeScript)。同时,代理具有元编译的特性,使用代理的好处,并没有比使用新的语法,比如箭头,解构等其他语法那么明显。

但我对ES6的代理还是非常激动的,因为它们是一个用来调节访问JS应用程序里面对象的简洁语义化的构造。

在这篇文章里,我会尽我所能来解释它们是如何工作的,并且列举出几种你可能会用到的实用的方法。

什么是代理?
 

在现实生活中,一个代理是一个人,他被赋予权限去代表别人。举例来说,很多国家允许代理人去投票,这意味着你可以授权别人代表你去投票。

在技术上,代理是一种通用的范例。你可能听说过代理服务器,他负责所有你的请求/传输,把你引导到其他的目的地,并返回响应给你。使用一个代理服务器是很有用的,比如,当你不想要到别人知道你的来源,就到达你所传输的目的地(e.g. nsa.gov)。这样,一个请求的所有目标服务器都被认为是来自代理服务器。

这就慢慢接近了这篇文章的重点,在应用程序编程上,代理在 设计模式 上是有共通之处的。ES6 代理的目的是要做一种类似的模拟,它包含一个包装类(A)和一个其他类(B)去拦截/控制访问它(A)。

当你想用代理模式时,你可能会想要:

  • 拦截或控制访问一个对象

  • 简化模糊的规则或辅助逻辑方法/类的复杂性

  • 没有验证/准备之前,防止重-资源动作。

ES6 的代理
 

Proxy 构造器在整个全局对象上都可以被访问。用了它你就能于对象跟在其上执行的操作之间拥有一个位置,处在这个位置搜集有关请求的信息,并且返回任何你想要返回的东西。使用这种方式,代理就跟中间件有很多相同点了。

具体而言,代理让你可以拦截通常在一个对象或者其属性上调用的许多的方法, 最常见的就是 get, set, apply (针对函数) 以及 construct (对于函数而言就是用 new 这个关键词)。完整的可以通过代理来拦截的方法列表,可以看看这份文档。代理也可以在任何时候被配置成停止接收请求, 有效的撤销所有针对它们所代理对象的访问操作。这是由一个我稍后会更多的提到的 revoke 方法。

属于

在我们更深入了解以前,有三个词你需要先了解下 : target, handler, 以及 trap。

target 指的是代理所代表的对象。它就是那个你想要节制对其访问的对象。它总是被作为 Proxy构造器的第一个参数传入,并且也会被传入每个 trap 中(更多关于这个的内容在后面两行)。

handler 是一个对象,包含了你想要拦截和处理的操作。它被作为 Proxy 构造器的第二个参数传入。它实现了 Proxy API (例如:get, set, apply 等等)。

trap 是指代 handler 中处理特定方法的一个方法项。因此如果你要拦截对 get 方法的调用,就要定义一个 get 的 trap,诸如此类。

最后一个东西就是: 你还应该对 Reflect API 有所了解, 它也能在整个全局对象上被访问到。这个我会推迟到 MDN 来讲述它是什么,因为它所解释的 Reflect 是简明扼要的,也因为它就是你一看就明白的那种解释之一。相信我。

基本的使用

在我深入讲述哪些我见过的有趣的代理示例之前,先来一个"hello world“的示例。更多有关于使用代理的一步一步的介绍,你可以看看 Nicholas Zakas《理解ES6》的相关章节,在线可以免费阅读。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let dataStore = {  
  name: 'Billy Bob',
  age: 15
};
let handler = {  
  get(target, key, proxy) {
    const today = new Date();
    console.log(`GET request made for ${key} at ${today}`);
    return Reflect.get(target, key, proxy);
  }
}
dataStore = new Proxy(dataStore, handler);
// This will execute our handler, log the request, and set the value of the `name` variable.
const name = dataStore.name;

ES6 Proxy 的使用场景
 

对于如何使用代理,你可以已经有了一些想法。这里有我的想法。

1.抽离验证类的代码

这里有一个简单的讲代理用于验证的示例 -- Zakas 在他的书里提供的 -- 它被用来确保数据存储中所有的属性都是相同的类型。使用下面这样的代码,我们就能确保任何时候当有人尝试往我们的 numericDataStore 设置属性, 最新的值都会是一个数字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let numericDataStore = {  
  count: 0,
  amount: 1234,
  total: 14
};
numericDataStore = new Proxy(numericDataStore, {  
  set(target, key, value, proxy) {
    if (typeof value !== 'number') {
      throw Error("Properties in numericDataStore can only be numbers");
    }
    return Reflect.set(target, key, value, proxy);
  }
});
// this will throw an error
numericDataStore.count = "foo";
// this will set the new value as expected
numericDataStore.count = 333;

这很有趣,但是你会有多频繁的使用类型完全相同的属性来创建对象呢?

如果你想要为一个对象上的某些或者所有属性编写自定义的验证器, 代码就会稍稍变得复杂起来,不过我所喜爱的是代理如何来帮助你将验证的代码从更加核心的逻辑分离。我是那个唯一讨厌将验证代码跟方法和类混在一起的人吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Define a validator that takes custom validators and returns a proxy
function createValidator(target, validator) {  
  return new Proxy(target, {
    _validator: validator,
    set(target, key, value, proxy) {
      if (target.hasOwnProperty(key)) {
        let validator = this._validator[key];
        if (!!validator(value)) {
          return Reflect.set(target, key, value, proxy);
        else {
          throw Error(`Cannot set ${key} to ${value}. Invalid.`);
        }
      else {
        // prevent setting a property that isn't explicitly defined in the validator
        throw Error(`${key} is not a valid property`)
      }
    }
  });
}
// Now, just define validators for each property
const personValidators = {  
  name(val) {
    return typeof val === 'string';
  },
  age(val) {
    return typeof age === 'number' && age > 18;
  }
}
class Person {  
  constructor(name, age) {
    this.name = name;
    this.age = age;
    return createValidator(this, personValidators);
  }
}
const bill = new Person('Bill', 25);
// all of these throw an error
bill.name = 0;  
bill.age = 'Bill';  
bill.age = 15;

这样,你就可以无限扩展你的验证代码,而无须修改你的类/方法。

还有一个跟验证相关的点子。假如说你想要检查将要被传入一个特定方法的值,并且当其实现不正确时记录一些有帮助的警告信息。你可以用代理来做这件事情,而且保持类型检查的代码不会碍什么事。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
let obj = {  
  pickyMethodOne: function(obj, str, num) { /* ... */ },
  pickyMethodTwo: function(num, obj) { /*... */ }
};
const argTypes = {  
  pickyMethodOne: ["object""string""number"],
  pickyMethodTwo: ["number""object"]
};
obj = new Proxy(obj, {  
  get: function(target, key, proxy) {
    var value = target[key];
    return function(...args) {
      var checkArgs = argChecker(key, args, argTypes[key]);
      return Reflect.apply(value, target, args);
    };
  }
});
function argChecker(name, args, checkers) {  
  for (var idx = 0; idx < args.length; idx++) {
    var arg = args[idx];
    var type = checkers[idx];
    if (!arg || typeof arg !== type) {
      console.warn(`You are incorrectly implementing the signature of ${name}. Check param ${idx + 1}`);
    }
  }
}
obj.pickyMethodOne();  
// > You are incorrectly implementing the signature of pickyMethodOne. Check param 1
// > You are incorrectly implementing the signature of pickyMethodOne. Check param 2
// > You are incorrectly implementing the signature of pickyMethodOne. Check param 3
obj.pickyMethodTwo("wopdopadoo", {});  
// > You are incorrectly implementing the signature of pickyMethodTwo. Check param 1
// No warnings logged
obj.pickyMethodOne({}, "a little string", 123);  
obj.pickyMethodOne(123, {});

2. 在 JavaScript中实现真正的私有
 

我曾今共事的一个开发者对JavaScript中没有真正的私有感到相当地恼火。他是Java出身的,在Java中你可以明确地将任何属性设置成私有(只能在类的里面被访问到)或者公有(内部或者外部都能被访问)。

在JavaScript中,约定俗成的办法是在属性前面使用下划线 (或者其它的字符) 和/或在其后面来表示它仅能被内部使用。但是这样并不能阻止某个人进行窥视或者改变规则。

下面就是这样的场景,有一个 apiKey,我们只想要它在api对象里面方法被访问到,而我们并不像它在对象的外面能被访问到。

1
2
3
4
5
6
7
8
9
10
11
12
var api = {  
  _apiKey: '123abc456def',
  /* mock methods that use this._apiKey */
  getUsers: function(){}, 
  getUser: function(userId){}, 
  setUser: function(userId, config){}
};
// logs '123abc456def';
console.log("An apiKey we want to keep private", api._apiKey);
// get and mutate _apiKeys as desired
var apiKey = api._apiKey;  
api._apiKey = '987654321';

有了 ES6 的代理,就能在JavaScript中实现真正完全的私有,有两种方式。

首先,你可以使用一个代理来拦截对于特定属性的请求,然后对其进行限制或者就只是返回undefined。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var api = {  
  _apiKey: '123abc456def',
  /* mock methods that use this._apiKey */
  getUsers: function(){ }, 
  getUser: function(userId){ }, 
  setUser: function(userId, config){ }
};
// Add other restricted properties to this array
const RESTRICTED = ['_apiKey'];
api = new Proxy(api, {  
    get(target, key, proxy) {
        if(RESTRICTED.indexOf(key) > -1) {
            throw Error(`${key} is restricted. Please see api documentation for further info.`);
        }
        return Reflect.get(target, key, proxy);
    },
    set(target, key, value, proxy) {
        if(RESTRICTED.indexOf(key) > -1) {
            throw Error(`${key} is restricted. Please see api documentation for further info.`);
        }
        return Reflect.get(target, key, value, proxy);
    }
});
// throws an error
console.log(api._apiKey);
// throws an error
api._apiKey = '987654321';

你也可以使用 has 这个 trap 来掩盖这个属性存在的事实。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var api = {  
  _apiKey: '123abc456def',
  /* mock methods that use this._apiKey */
  getUsers: function(){ }, 
  getUser: function(userId){ }, 
  setUser: function(userId, config){ }
};
// Add other restricted properties to this array
const RESTRICTED = ['_apiKey'];
api = new Proxy(api, {  
  has(target, key) {
    return (RESTRICTED.indexOf(key) > -1) ?
      false :
      Reflect.has(target, key);
  }
});
// these log false, and `for in` iterators will ignore _apiKey
console.log("_apiKey" in api);
for (var key in api) {  
  if (api.hasOwnProperty(key) && key === "_apiKey") {
    console.log("This will never be logged because the proxy obscures _apiKey...")
  }
}

3. 静默地对象访问日志
 

对于那些耗费资源密集型、缓慢运行,和/或被大量使用的方法和接口,你可能会想要对它们的使用和/或性能表现进行日志记录。代理可以使其得以在后台悄悄地进行。

注意: 不幸的是,你不能就只使用 apply 的 trap 来对方法进行拦截。Axel Rauschmayer 就此进行了更多的描述。基本的一点是,任何时候你要调用一个方法,首先都得获得这个方法。所以如果你想要拦截一次方法调用,就需要拦截这个方法的获取过程,然后才能拦截它的使用过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let api = {  
  _apiKey: '123abc456def',
  getUsers: function() { /* ... */ },
  getUser: function(userId) { /* ... */ },
  setUser: function(userId, config) { /* ... */ }
};
api = new Proxy(api, {  
  get: function(target, key, proxy) {
    var value = target[key];
    return function(...arguments) {
      logMethodAsync(new Date(), key);
      return Reflect.apply(value, target, arguments);
    };
  }
});
// executes apply trap in the background
api.getUsers();
function logMethodAsync(timestamp, method) {  
  setTimeout(function() {
    console.log(`${timestamp} - Logging ${method} request asynchronously.`);
  }, 0)
}

这很酷,因为你可以在不搞乱应用程序代码或者阻碍其执行的前提下对任何类型的东西进行日志记录。要跟踪特定方法随着时间的性能表现应该不会要太多的代码。

4. 提供警告信息或者阻止特定操作的执行

假设你想要阻止任何人删除属性 noDelete, 想要让那些调用 oldMethod 的用户知道它已经被弃用了, 还想要阻止任何人修改 doNotChange 属性。下面就是一种快速的实现办法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
let dataStore = {  
  noDelete: 1235,
  oldMethod: function() {/*...*/ },
  doNotChange: "tried and true"
};
const NODELETE = ['noDelete'];  
const DEPRECATED = ['oldMethod'];  
const NOCHANGE = ['doNotChange'];
dataStore = new Proxy(dataStore, {  
  set(target, key, value, proxy) {
    if (NOCHANGE.includes(key)) {
      throw Error(`Error! ${key} is immutable.`);
    }
    return Reflect.set(target, key, value, proxy);
  },
  deleteProperty(target, key) {
    if (NODELETE.includes(key)) {
      throw Error(`Error! ${key} cannot be deleted.`);
    }
    return Reflect.deleteProperty(target, key);
  },
  get(target, key, proxy) {
    if (DEPRECATED.includes(key)) {
      console.warn(`Warning! ${key} is deprecated.`);
    }
    var val = target[key];
    return typeof val === 'function' ?
      function(...args) {
        Reflect.apply(target[key], target, args);
      } :
      val;
  }
});
// these will throw errors or log warnings, respectively
dataStore.doNotChange = "foo";  
delete dataStore.noDelete;  
dataStore.oldMethod();

5. 阻止非必要的重度资源消耗型操作
 

假设你有一个服务器端点会返回一个非常大的文件。你不想在之前的请求还在进行中,而文件也还在下载中,或者它已经被下载过来一次的时候再次发起请求。代理对此缓冲这种类型的访问以及尽可能获取缓存值方面是一个很好的架构, 而不是让用户去尝试尽可能频繁的发起对端点的调用。这里我将省略大多数代码,但下面所列出来的代码足够让你明白它是如何运作的了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let obj = {  
  getGiantFile: function(fileId) {/*...*/ }
};
obj = new Proxy(obj, {  
  get(target, key, proxy) {
    return function(...args) {
      const id = args[0];
      let isEnroute = checkEnroute(id);
      let isDownloading = checkStatus(id);      
      let cached = getCached(id);
      if (isEnroute || isDownloading) {
        return false;
      }
      if (cached) {
        return cached;
      }
      return Reflect.apply(target[key], target, args);
    }
  }
});

6. 即时撤销对敏感数据的访问

代理支持在任何时候撤销对目标对象的访问。这可能在你想要完全封锁对某些数据和API的访问,这样的场景下会有用(例如安全、认证和性能方面的因素) 。这里有一个基础的示例,使用了 revocable 方法。注意当你在使用它时,并没有调用Proxy上的 new 关键词。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let sensitiveData = {  
  username: 'devbryce'
};
const {sensitiveData, revokeAccess} = Proxy.revocable(sensitiveData, handler);
function handleSuspectedHack(){  
  // Don't panic
  // Breathe
  revokeAccess();
}
// logs 'devbryce'
console.log(sensitiveData.username);
handleSuspectedHack();
// TypeError: Revoked
console.log(sensitiveData.username);

好了,我知道的就这些。我也乐于听到你已经在自己的工作中用到代理时使用的方法。

祝你编写JavaScript愉快,若能在本文中发现某些我需要改正的东西,我也会很高兴!

本文转自:开源中国社区 [http://www.oschina.net]
本文标题:6 个 ES6 代理使用案例
本文地址:
http://www.oschina.net/translate/use-cases-for-es6-proxies
参与翻译:
leoxu, tv_哇, 无若

英文原文:6 compelling use cases for ES6 proxies


时间:2016-07-25 08:39 来源:开源中国社区 作者:oschina 原文链接

好文,顶一下
(0)
0%
文章真差,踩一下
(0)
0%
------分隔线----------------------------


把开源带在你的身边-精美linux小纪念品
无觅相关文章插件,快速提升流量