在ES6中,Proxy构造器是一种可访问的全局对象,使用它,你可以在对象与各种操作对象的行为之间收集有关请求操作的各种信息,并返回任何你想做的。在这点上,代理Proxy与中间件有很多共同点。也就是说,代理Proxy会代理你要操作的对象,那为什么要代理原对象呢?
Proxy代理能够让你截取原对象的各种操作方法,最普通的是get set apply和construct等方法,看这个规定有很多被拦截的方法.。Proxy代理也能配置为任何时候停止拦截请求,有效地撤销他们所服务的对象的所有访问代理,这是通过revoke方法实现的。
首先解释一下代理中使用到的技术名词:
target是指代理的原对象,它是你需要拦截访问的原始对象,它总是作为Proxy构造器的第一个方法,也可以传递到每个trap中。
handler是一个包含你要进行拦截和处理的对象,也就是你拦截了原始对象以后想干什么?主要的代理内容在这里,是作为Proxy构造器的第二个方法参数传统,它是实现Proxy API。
trap用来规定对于指定什么方法进行拦截处理,如果你想拦截get方法的调用,那么你要定义一个get trap。
基本用法
这里是一个Hello world用法的代码:
let dataStore = { name: 'Billy Bob', age: 15 };
let handler = { get(target, key, proxy) { //get 的trap 拦截get方法 const today = new Date(); console.log(`GET request made for ${key} at ${today}`); return Reflect.get(target, key, proxy); //拦截get方法 } }
//构造一个代理,第一个参数是原始对象,第二个参数是代理处理器 dataStore = new Proxy(dataStore, handler);
//这里将会执行我们的handler, 记录请求,设置name值。 const name = dataStore.name;
下面总结一下Proxy具体应用用法模式:
1.剥离校验代码
我们经常有对数字进行校验的代码,现在可以将这部分校验代码放入Proxy中。
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); }
}); //这会抛出错误 numericDataStore.count = "foo"; //这会正常设置新值 numericDataStore.count = 333;
如果你想对对象的所有属性都定制一个校验器,代码可能会复杂一些:
//定义一个校验器validator对象 返回一个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`) } } }); }
// 现在为每个属性定义校验器 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); //返回的是代理对象,不是真正Person原始对象 } } const bill = new Person('Bill', 25);
//下面都会抛错 bill.name = 0; bill.age = 'Bill'; bill.age = 15;
这种校验方法无需修改原来的代码,因为修改原来稳定的代码可能会引入新的Bug。
下面代码是检查传入每个执行方法的参数,以校验参数类型签名是否和要求的一致?
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[hey]; 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, {});
实现真正的Javascript私有
我们会使用下划线定义私有变量,但是你无法阻碍别人会偷偷读取它,下面apiKey是只能在方法中访问,不想被外面访问:
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);
// 会获得_apiKeys值 var apiKey = api._apiKey; api._apiKey = '987654321'; //修改了
使用代理你能拦截某个属性访问并限制返回undefined:
var api = { _apiKey: '123abc456def', /* mock methods that use this._apiKey */ getUsers: function(){ }, getUser: function(userId){ }, setUser: function(userId, config){ } }; // 增加限制访问的属性在这个数组中 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进行拦截,与上面相同代码不再列出:
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...") }}
安静记录对象的访问
一些方法可能是资源密集型,运行缓慢或时间比较长,我们需要记录它们的性能,Proxy代理可以在后台安静地记录。
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) }上面使用异步方法记录了你的代码可能会堵塞的地方,花不了多少代码就能时刻跟踪某个方法的性能。
警告或防止某些操作
如果你想阻止某人删除属性noDelete,你要让调用者知道oldMethod已经丢弃了,你要阻止任何人改变doNotChange属性:
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();
阻止不必要的重资源操作
假设你有一个服务器端点会返回一个非常大的文件,当之前有一个请求没有完成,或者文件正在下载,Proxy是一个好的缓冲机制,可以阻断用户对这个端点的重复请求。
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); } } });立即撤销访问敏感数据
Proxy任何时候提供取消对目标原始对象的访问,这是在安全和性能保护时有用的,如下案例,使用revocable方法,注意当你使用它时,你不能在Proxy是new关键词。
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);