设计模式-代理模式

从概念到前后端实战

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.applyFunction.prototype.apply.call,保证 this 与原对象一致。
  • 性能 :代理引入额外开销------对高频/极短执行方法谨慎使用;将部分逻辑下沉到框架层(中间件/拦截器)。
  • 错误处理 :异步链路要统一处理异常,避免"吞错"。
  • 可观测性 :给代理增加日志 TraceId指标,否则问题难排查。
  • 序列化Proxy 对象不能直接被结构化克隆(如 postMessage),需要转原始对象或 DTO。

13. 测试建议

  • Proxy 行为写单测

    • 鉴权失败是否拦截;
    • 缓存命中是否减少真实调用;
    • 限流/熔断是否生效;
    • 日志/埋点是否被调用(可用 spy/mock)。

14. 小结(带走这张速查表)

  • 要点:代理 = 透明访问控制 + 横切增强;不改原接口,专注"调用前/后"的能力。

  • 常用组合拳:鉴权 + 缓存 + 限流 + 重试 + 超时 + 熔断 + 日志(AOP)。

  • 落地路径

    1. 先抽象 Subject 接口;
    2. 用静态或动态代理封装横切能力;
    3. 在请求层/组件层/BFF 层按需插拔;
    4. 加上可观测性与单测,闭环迭代。

如果你正在做性能与稳定性治理,优先把"代理化"到请求与服务边界 ------ 成本最低,收益最大。


15. 延伸阅读(关键词)

  • JS 原生 ProxyReflect API;
  • Java Proxy / InvocationHandler、CGLIB;
  • AOP(面向切面编程)、拦截器(Interceptor)、中间件(Middleware);
  • 断路器(Circuit Breaker)、令牌桶/漏桶限流、幂等;
  • 虚拟代理(懒加载)、保护代理(RBAC)。

相关推荐
刘一说11 分钟前
TypeScript 与 JavaScript:现代前端开发的双子星
javascript·ubuntu·typescript
GISer_Jing15 分钟前
AI Agent 目标设定与异常处理
人工智能·设计模式·aigc
孟无岐34 分钟前
【Laya】Component 使用说明
typescript·游戏引擎·游戏程序·laya
蔺太微44 分钟前
组合模式(Composite Pattern)
设计模式·组合模式
EndingCoder1 小时前
类的继承和多态
linux·运维·前端·javascript·ubuntu·typescript
鱼跃鹰飞3 小时前
DDD中的防腐层
java·设计模式·架构
码界奇点3 小时前
基于Vue3与TypeScript的后台管理系统设计与实现
前端·javascript·typescript·vue·毕业设计·源代码管理
会员果汁4 小时前
15.设计模式-组合模式
设计模式·组合模式
YUEchn5 小时前
无处不在的Agent
设计模式·llm·agent
Wect5 小时前
LeetCode 274. H 指数:两种高效解法全解析
算法·typescript