80%的Web开发者都不知道的代理API的8个主要使用场景!
Proxy API 非常强大,非常有用。在这篇文章中,我将介绍它的 8 种使用场景。
?在日常工作中,相信很多开发者都使用过Web调试代理工具,比如Fiddler或者Charles,通过使用Web调试代理工具,我们可以拦截HTTP/HTTPS协议请求,手动修改请求参数和响应结果,不仅如此,在调试在线问题时,使用Web调试代理工具,还可以将在线压缩和混淆的JavaScript文件映射到本地未压缩和混淆的JavaScript文件。
在简单介绍Web调试代理工具的基本功能后,让我们来看看使用Web调试代理工具的HTTP请求过程:
从上图可以看出,使用Web Proxy工具后,我们发起的HTTP请求会通过Web Proxy进行转发和处理,添加了Web Proxy代理层,可以让我们更好的控制HTTP请求的流,对于单页应用,从服务器获取数据后,我们会读取相应的数据,并显示在页面上:
上面的过程类似于浏览器直接从服务器获取数据:
为了能够灵活控制HTTP请求的流向,我们增加了Web Proxy层,那么我们可以控制数据对象的读取过程吗?答案是可以的,我们可以使用Web API,比如?Object.defineProperty
?或者?Proxy
??API,引入Web API后,数据访问过程如下图所示:
接下来,我将重点介绍??Proxy
??API,它是 Vue3 实现数据响应背后的“英雄”。
?Proxy
?对象用于创建对象的代理,支持基本操作的拦截和定制(如属性查找、赋值、枚举、函数调用等)。
?Proxy
?的构造函数语法是:
const p = new Proxy(target, handler)
target
?:要用 Proxy 包装的目标对象(可以是任何类型的对象,包括数组、函数,甚至是另一个 Proxy)。
handler
?:一个对象,定义哪些操作将被拦截以及如何重新定义被拦截的操作。
在介绍完 Proxy 构造函数之后,让我们举一个简单的例子:
const man = {
name: "Bytefer",
};
const proxy = new Proxy(man, {
get(target, property, receiver) {
console.log(`Accessing the ${property} property`);
return target[property];
},
});
console.log(proxy.name);
console.log(proxy.age);
在上面的例子中,我们使用了?Proxy
?构造函数为?man
?对象创建了一个代理对象,在创建代理对象时,我们定义了一个 get 陷阱来捕获属性读操作,这个陷阱的功能是拦截用户在目标对象上的相关操作,在这些操作传播到目标对象之前,会先调用相应的 trap 函数,从而拦截和修改相应的行为。
上述代码成功执行后,将输出如下结果:
Accessing the name property
Bytefer
Accessing the age property
undefined
基于上面的输出结果,我们可以发现get陷阱不仅可以拦截已知属性的读操作,还可以拦截未知属性的读操作。在创建?Proxy
?对象时,除了定义get陷阱外,还可以定义其他陷阱,如has
、set
、delete
、apply
?或?ownKeys
?等。
handler对象支持13种陷阱,这里我只列出以下5种常用陷阱:
handler.get
?:是一个获取属性值的陷阱。
handler.set
?:是一个用于设置属性值的陷阱。
handler.has
?:是?in
?操作符的陷阱。
handler.deleteProperty
?:是 delete 操作符的陷阱。
handler.ownKeys
?:是?Reflect.ownKeys()
?的陷阱。
注意所有的陷阱都是可选的,如果没有定义陷阱,则保留源对象的默认行为。在阅读了上面的陷阱介绍后,你是否认为?Proxy
??API非常强大?
function enhancedArray(arr) {
return new Proxy(arr, {
get(target, property, receiver) {
const range = getRange(property);
const indices = range ? range : getIndices(property);
const values = indices.map((index) => {
const key = index < 0 ? target.length + index : index;
return Reflect.get(target, key, receiver);
});
return values.length === 1 ? values[0] : values;
},
});
?
function getRange(str) {
var [start, end] = str.split(":").map(Number);
if (typeof end === "undefined") return false;
?
let range = [];
for (let i = start; i < end; i++) {
range = range.concat(i);
}
return range;
}
?
function getIndices(str) {
return str.split(",").map(Number);
}
}
在上面的代码中,除了使用了Proxy API,我们还使用了Reflect API,一旦我们有了?enhancedArray
?函数,我们可以像这样使用它:
const arr = enhancedArray([10, 6, 8, 5, 2]);
console.log(arr[-1]); // 2
console.log(arr[[2, 4]]); // [ 8, 2 ]
console.log(arr[[2, -2, 1]]); // [ 8, 5, 6 ]
console.log(arr["2:4"]); // [ 8, 5 ]
console.log(arr["-2:3"]); // [ 5, 2, 10, 6, 8 ]
从上面的输出结果可以看出,增强后的数组对象可以支持负索引和片段索引等功能,除了增强数组之外,我们还可以使用?Proxy
?API 增强普通对象。
const enhancedObject = (target) =>
new Proxy(target, {
get(target, property) {
if (property in target) {
return target[property];
} else {
return getPropertyValue(property, target);
}
},
});
?
let value;
function getPropertyValue(property, target) {
value = null;
for (const key of Object.keys(target)) {
if (typeof target[key] === "object") {
getPropertyValue(property, target[key]);
} else if (typeof target[property] !== "undefined") {
value = target[property];
break;
}
}
return value;
}
一旦我们有了?enhancedObject
?函数,我们就可以像这样使用它:
const data = enhancedObject({
user: {
name: "Bytefer",
settings: {
theme: "light",
},
},
});
console.log(data.user.settings.theme); // light
console.log(data.theme); // light
console.log(data.address); // null
从上面的输出结果可以看出,我们可以通过使用?enhancedObject
?函数处理的对象轻松访问普通对象内部的深层属性。
const man = { name: "Bytefer" };
?
function freezeObject(obj) {
return new Proxy(obj, {
set() {
return true;
},
deleteProperty() {
return false;
},
defineProperty() {
return true;
},
setPrototypeOf() {
return true;
},
});
}
?
const freezedMan = freezeObject(man);
定义了 freeze 函数之后,让我们来测试一下它的功能:
console.log(freezedMan.name); // Bytefer
freezedMan.name = "Lolo";
delete freezedMan.man;
freezedMan.age = 30;
console.log(freezedMan); // { name: 'Bytefer' }
function traceMethodCall(obj) {
const handler = {
get(target, propKey, receiver) {
const propValue = target[propKey]; // Get the original method
return typeof propValue !== "function"
? propValue
: function (...args) {
const result = propValue.apply(this, args);
console.log(
`Call ${propKey} method -> ${JSON.stringify(result)}`
);
return result;
};
},
};
return new Proxy(obj, handler);
}
有了?traceMethodCall
?函数,我们可以用它来跟踪指定对象的方法调用:
const man = {
name: "Bytefer",
say(msg) {
return `${this.name} says: ${msg}`;
},
};
const tracedObj = traceMethodCall(man);
tracedObj.say("Hello Proxy API");
// Call say method -> "Bytefer says: Hello Proxy API"
事实上,除了能够跟踪方法调用外,我们还可以跟踪对象中属性的访问。
function tracePropertyAccess(obj, propKeys) {
const propKeySet = new Set(propKeys);
return new Proxy(obj, {
get(target, propKey, receiver) {
if (propKeySet.has(propKey)) {
console.log("GET " + propKey);
}
return Reflect.get(target, propKey, receiver);
},
set(target, propKey, value, receiver) {
if (propKeySet.has(propKey)) {
console.log("SET " + propKey + "=" + value);
}
return Reflect.set(target, propKey, value, receiver);
},
});
}
有了?tracePropertyAccess
?函数,我们可以用它来跟踪指定对象的属性访问:
const man = {
name: "Bytefer",
};
const tracedMan = tracePropertyAccess(man, ["name"]);
console.log(tracedMan.name); // GET name; Bytefer
console.log(tracedMan.age); // undefined
tracedMan.name = "Lolo"; // SET name=Lolo
在上面的例子中,我们定义了一个?tracePropertyAccess
?函数,它接收两个参数:obj和propKeys,分别代表要跟踪的目标和要跟踪的属性列表,在调用?tracePropertyAccess
?函数后,将返回一个代理对象,当我们访问被跟踪的属性时,控制台将输出相应的访问日志。
function hideProperty(target, prefix = "_") {
return new Proxy(target, {
has: (obj, prop) => !prop.startsWith(prefix) && prop in obj,
ownKeys: (obj) =>
Reflect.ownKeys(obj).filter(
(prop) => typeof prop !== "string" || !prop.startsWith(prefix)
),
get: (obj, prop, rec) => (prop in rec ? obj[prop] : undefined),
});
}
使用?hideProperty
?函数,我们可以隐藏以??_
?(下划线)开头的属性:
const man = {
name: "Bytefer",
_pwd: "ProxyAPI",
};
console.log(safeMan._pwd); // undefined
console.log("_pwd" in safeMan); // false
console.log(Object.keys(safeMan)); // [ 'name' ]
对于 JavaScript 来说,沙箱并不是传统意义上的沙箱,它只是在沙箱中运行一些不可信代码的一种安全机制,这样它就无法访问沙箱之外的代码。
function sandbox(code) {
code = "with (sandbox) {" + code + "}";
const fn = new Function("sandbox", code);
?
return function (sandbox) {
const sandboxProxy = new Proxy(sandbox, {
has(target, key) {
return true;
},
get(target, key) {
if (key === Symbol.unscopables) return undefined;
return target[key];
},
});
return fn(sandboxProxy);
};
}
使用?sandbox
?函数,让我们验证一下它的功能:
const man = {
name: "Bytefer",
log() {
console.log("Hello Proxy API");
},
};
let code = "log();console.log(name)";
sandbox(code)(man);
在浏览器中运行上述代码时,控制台会抛出以下错误消息:
构建器模式将复杂对象分解为相对简单的部分,然后根据不同的需求分别创建它们,最后构建复杂对象。
使用?Proxy
??API,我们可以实现一个?Builder
?函数,这样它包装的对象就支持构造函数模式来构造对象。
function Builder(typeOrTemplate, template) {
let type;
if (typeOrTemplate instanceof Function) {
type = typeOrTemplate;
} else {
template = typeOrTemplate;
}
const built = template ? Object.assign({}, template) : {};
const builder = new Proxy(
{},
{
get(target, prop) {
if ("build" === prop) {
if (type) {
// A class name (identified by the constructor) was passed. Instantiate it with props.
const obj = new type();
return () => Object.assign(obj, Object.assign({}, built));
} else {
// No type information - just return the object.
return () => built;
}
}
return (...args) => {
// If no arguments passed return current value.
if (0 === args.length) {
return built[prop.toString()];
}
built[prop.toString()] = args[0];
return builder;
};
},
}
);
return builder;
}
对于?Builder
?函数,让我们看一下它的两种使用方法。第一种方法是处理普通对象:
const defaultUserInfo = {
id: 1,
userName: "Bytefer",
email: "bytefer@gmail.com",
};
?
const bytefer = Builder(defaultUserInfo).id(2).build();
console.log(bytefer);
第二种方法是处理类:
class User {
constructor() {}
}
const lolo = Builder(User, defaultUserInfo);
console.log(lolo.id(3).userName("Lolo").build());
如果你知道代理API的其他使用场景,欢迎给我留言。
?欢迎关注公众号:文本魔术,了解更多