从概念到前后端实战
1. 为什么需要代理?(问题导向)
在工程实践中,我们常常需要在不修改原有业务代码 的前提下,为对象透明地 加上缓存、鉴权、限流、重试、日志、懒加载等横切能力。直接在业务类里加,会让代码变得臃肿、难维护、难测试------这就是 Proxy 代理模式 大显身手的地方。
一句话 :代理模式为某个对象提供一个"替身(Proxy)",由代理在调用前/后做额外处理,再把请求转发给真实对象(RealSubject)。
2. 定义与角色
-
意图 :为其他对象提供一种代理以控制对这个对象的访问。
-
核心角色:
Subject
:抽象主题(接口/抽象类)。RealSubject
:真实主题(被代理者)。Proxy
:代理对象,持有RealSubject
的引用,负责访问控制/增强。
3. 常见分类与应用举例
- 远程代理(Remote Proxy):本地类代理远端服务调用(RPC、微服务)。
- 虚拟代理(Virtual Proxy):按需/懒加载大对象(例如图片懒加载、复杂图表)。
- 保护代理(Protection Proxy):基于权限角色控制访问(RBAC)。
- 缓存/智能引用代理(Smart Reference):透明加缓存、引用计数、日志、性能埋点。
- 防护代理:限流、熔断、降级、重试、超时控制。
这些都能在"不动核心业务"的情况下,由代理进行横切增强。
4. 何时使用?(Checklist)
- 需要在不修改原类的前提下,新增通用能力(缓存/鉴权/日志/限流)。
- 真实对象创建或调用开销大 ,希望懒加载 或按需。
- 访问需要前置校验(登录、角色、白名单)。
- 需要统一 的调用链打点 、调用重试 、熔断/超时。
5. 和相近模式怎么区分?
模式 | 关注点 | 是否改变接口 | 典型用途 |
---|---|---|---|
代理(Proxy) | 控制访问 & 透明增强 | 不变(对 Client 透明) | 鉴权、缓存、远程、懒加载 |
装饰器(Decorator) | 叠加新职责 | 不变 | 功能扩展(如为组件"加特效") |
适配器(Adapter) | 接口不兼容的转换 | 改变为适配接口 | 旧接口适配新系统 |
外观(Facade) | 提供统一入口 | 通常不暴露原接口 | 简化复杂子系统调用 |
6. 静态代理(TypeScript 入门示例)
适用于接口稳定、代理逻辑较少的场景。优点是类型清晰;缺点是类多、扩展性一般。
ts
// Subject
export interface UserService {
getProfile(userId: string): Promise<{ id: string; name: string }>;
}
// RealSubject
export class RemoteUserService implements UserService {
async getProfile(userId: string) {
// 模拟远程调用
await new Promise(r => setTimeout(r, 50));
return { id: userId, name: "Alice" };
}
}
// Proxy:增加鉴权与日志
export class UserServiceProxy implements UserService {
constructor(private real: UserService, private currentRole: "guest" | "admin") {}
async getProfile(userId: string) {
const start = performance.now();
if (this.currentRole === "guest") {
throw new Error("Permission denied");
}
const result = await this.real.getProfile(userId);
const cost = Math.round(performance.now() - start);
console.info(`[UserService.getProfile] cost=${cost}ms`);
return result;
}
}
// 使用
const service = new UserServiceProxy(new RemoteUserService(), "admin");
service.getProfile("1001").then(console.log);
7. 动态代理(JavaScript/TypeScript 的 ES6 Proxy
)
JS 原生
Proxy
能动态拦截 任意对象 的属性读取、设置、函数调用,是前端/Node 最常用的"代理模式"载体。
7.1 通用 AOP 代理工厂
ts
type AnyFn = (...args: any[]) => any;
interface Hooks {
before?: (prop: string, args: unknown[]) => void;
after?: (prop: string, ret: unknown) => void;
error?: (prop: string, e: unknown) => void;
}
export function createAopProxy<T extends object>(target: T, hooks: Hooks): T {
return new Proxy(target, {
get(t, p, r) {
const prop = String(p);
const val = Reflect.get(t, p, r);
if (typeof val !== "function") return val;
return new Proxy(val as AnyFn, {
apply(fn, thisArg, args) {
try {
hooks.before?.(prop, args);
const ret = Reflect.apply(fn, t, args);
if (ret instanceof Promise) {
return ret
.then(x => { hooks.after?.(prop, x); return x; })
.catch(e => { hooks.error?.(prop, e); throw e; });
}
hooks.after?.(prop, ret);
return ret;
} catch (e) {
hooks.error?.(prop, e);
throw e;
}
}
});
}
});
}
7.2 组合能力:缓存 + 限流 + 重试 + 超时
ts
// 简易 TTL 缓存
class TTLCache<K, V> {
private m = new Map<K, { v: V; exp: number }>();
constructor(private ttl = 5_000) {}
get(k: K) {
const x = this.m.get(k);
if (!x) return undefined;
if (Date.now() > x.exp) { this.m.delete(k); return undefined; }
return x.v;
}
set(k: K, v: V) { this.m.set(k, { v, exp: Date.now() + this.ttl }); }
}
// 简易令牌桶限流
class TokenBucket {
private tokens: number; private last = Date.now();
constructor(private ratePerSec: number, private capacity = ratePerSec) { this.tokens = capacity; }
allow() {
const now = Date.now();
const refill = ((now - this.last) / 1000) * this.ratePerSec;
this.tokens = Math.min(this.capacity, this.tokens + refill);
this.last = now;
if (this.tokens >= 1) { this.tokens -= 1; return true; }
return false;
}
}
// 工具:带超时与重试
async function withTimeout<T>(p: Promise<T>, ms: number) {
return Promise.race([
p,
new Promise<T>((_, rej) => setTimeout(() => rej(new Error(`Timeout ${ms}ms`)), ms))
]);
}
async function withRetry<T>(fn: () => Promise<T>, times = 2, backoff = 200): Promise<T> {
let last: unknown;
for (let i = 0; i <= times; i++) {
try { return await fn(); } catch (e) { last = e; await new Promise(r => setTimeout(r, backoff * Math.pow(2, i))); }
}
throw last;
}
// 为"远程服务"生成防护代理
function protectRemote<T extends object>(svc: T, opt: { ttl?: number; rps?: number; timeout?: number; retry?: number }) {
const cache = new TTLCache<string, unknown>(opt.ttl ?? 3000);
const bucket = new TokenBucket(opt.rps ?? 5);
return createAopProxy(svc, {
before(prop, args) {
const key = `${prop}:${JSON.stringify(args)}`;
const cached = cache.get(key);
if (cached !== undefined) throw { __cached: true, key, cached };
if (!bucket.allow()) throw new Error("Rate limit exceeded");
},
async after(prop, ret) {
if (ret instanceof Promise) return; // 上面已在 AOP 工厂处理
const key = `${prop}:` + "??"; // 简化演示
cache.set(key, ret);
},
error(prop, e) {
if ((e as any).__cached) return; // 通过异常短路返回缓存(也可改用线程局部变量)
}
});
}
说明:真实生产会把"缓存命中"改为正常返回,这里用抛错短路是为了演示 Hook 流程。你也可以在
get
里直接返回包装函数。
8. 前端实战
8.1 虚拟代理:图片懒加载(IntersectionObserver)
ts
// HTML: <img class="lazy" data-src="https://...big.jpg" />
const io = new IntersectionObserver(entries => {
entries.forEach(e => {
if (e.isIntersecting) {
const img = e.target as HTMLImageElement;
img.src = img.dataset.src || "";
io.unobserve(img);
}
});
});
document.querySelectorAll("img.lazy").forEach(img => io.observe(img));
8.2 保护代理:防止重复提交
ts
function once<T extends (...a: any[]) => Promise<any>>(fn: T): T {
let pending = false;
return (async function (...args: any[]) {
if (pending) return; pending = true;
try { return await fn(...args); } finally { pending = false; }
}) as T;
}
const submit = once(async () => {
// 调用真实提交
await fetch("/api/submit", { method: "POST" });
});
8.3 请求代理:自动附带 Token + 刷新
ts
interface Http {
get<T>(url: string, init?: RequestInit): Promise<T>;
}
class FetchImpl implements Http {
async get<T>(url: string, init?: RequestInit) {
const res = await fetch(url, init);
if (!res.ok) throw new Error(res.statusText);
return res.json();
}
}
function authProxy(http: Http, getToken: () => Promise<string>, refresh: () => Promise<void>): Http {
return new Proxy(http, {
get(t, p, r) {
const val = Reflect.get(t, p, r);
if (typeof val !== "function") return val;
return async function (this: any, ...args: any[]) {
const token = await getToken();
try {
return await (val as Function).apply(t, [args[0], { ...(args[1]||{}), headers: { Authorization: `Bearer ${token}` } }]);
} catch (e: any) {
if (e.message.includes("401")) { await refresh(); }
throw e;
}
}
}
});
}
9. Node/后端实战:API 代理(限流 + 熔断 + 缓存)
以 Express 为例,在 BFF(Backend for Frontend)层做代理:
ts
import express from "express";
import fetch from "node-fetch";
const app = express();
let circuitOpen = false; let failCount = 0;
const cache = new Map<string, { data: any; exp: number }>();
app.get("/api/weather", async (req, res) => {
const key = "weather:tw:taipei";
const now = Date.now();
const hit = cache.get(key);
if (hit && hit.exp > now) return res.json({ cached: true, ...hit.data });
if (circuitOpen) return res.status(503).json({ message: "circuit open" });
try {
const r = await Promise.race([
fetch("https://third.party/weather?city=taipei"),
new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), 1500))
]);
// @ts-ignore
const data = await r.json();
cache.set(key, { data, exp: now + 10_000 });
failCount = 0;
res.json(data);
} catch (e) {
if (++failCount >= 3) { circuitOpen = true; setTimeout(() => { circuitOpen = false; failCount = 0; }, 10_000); }
res.status(502).json({ message: "upstream error", detail: String(e) });
}
});
app.listen(3000);
10. Java 动态代理(JDK Proxy)
在 Java 里,
java.lang.reflect.Proxy
+InvocationHandler
可在运行时为接口生成代理类(AOP 的基础之一)。
java
public interface OrderService { String place(String sku, int num); }
public class OrderServiceImpl implements OrderService {
@Override public String place(String sku, int num) { return "ok:" + sku + ":" + num; }
}
import java.lang.reflect.*;
public class LogProxy implements InvocationHandler {
private final Object target;
public LogProxy(Object target) { this.target = target; }
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long s = System.currentTimeMillis();
try {
Object ret = method.invoke(target, args);
System.out.println(method.getName() + " ok cost=" + (System.currentTimeMillis()-s));
return ret;
} catch (InvocationTargetException e) {
System.out.println(method.getName() + " error: " + e.getTargetException().getMessage());
throw e.getTargetException();
}
}
@SuppressWarnings("unchecked")
public static <T> T of(T target) {
return (T) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new LogProxy(target)
);
}
}
// 使用
OrderService s = LogProxy.of(new OrderServiceImpl());
System.out.println(s.place("SKU-1", 2));
提示:JDK 动态代理只能代理接口 ;如需代理类可用 CGLIB(基于继承/字节码增强)。
11. 在 Umi/React 项目中的落地建议
- 请求层 :给
request
/fetch
封一个 代理工厂(统一加 token、重试、超时、熔断、缓存)。 - 组件层 :对高代价可视化(大图表、地图)用虚拟代理 懒加载;交互按钮用保护代理防重复提交。
- BFF 层 :在 Node/Express/Nest 架一层 反向代理 ,实现限流/缓存/熔断,保护后端微服务。
- 日志与监控:用 AOP 代理统一埋点(method 入参/耗时/异常)。
12. 常见坑与最佳实践
- this 绑定 :JS
Proxy
里转发时优先Reflect.apply
或Function.prototype.apply.call
,保证this
与原对象一致。 - 性能 :代理引入额外开销------对高频/极短执行方法谨慎使用;将部分逻辑下沉到框架层(中间件/拦截器)。
- 错误处理 :异步链路要统一处理异常,避免"吞错"。
- 可观测性 :给代理增加日志 TraceId 、指标,否则问题难排查。
- 序列化 :
Proxy
对象不能直接被结构化克隆(如postMessage
),需要转原始对象或 DTO。
13. 测试建议
-
对
Proxy
行为写单测:- 鉴权失败是否拦截;
- 缓存命中是否减少真实调用;
- 限流/熔断是否生效;
- 日志/埋点是否被调用(可用 spy/mock)。
14. 小结(带走这张速查表)
-
要点:代理 = 透明访问控制 + 横切增强;不改原接口,专注"调用前/后"的能力。
-
常用组合拳:鉴权 + 缓存 + 限流 + 重试 + 超时 + 熔断 + 日志(AOP)。
-
落地路径:
- 先抽象
Subject
接口; - 用静态或动态代理封装横切能力;
- 在请求层/组件层/BFF 层按需插拔;
- 加上可观测性与单测,闭环迭代。
- 先抽象
如果你正在做性能与稳定性治理,优先把"代理化"到请求与服务边界 ------ 成本最低,收益最大。
15. 延伸阅读(关键词)
- JS 原生
Proxy
、Reflect
API; - Java
Proxy
/InvocationHandler
、CGLIB; - AOP(面向切面编程)、拦截器(Interceptor)、中间件(Middleware);
- 断路器(Circuit Breaker)、令牌桶/漏桶限流、幂等;
- 虚拟代理(懒加载)、保护代理(RBAC)。