代理模式(Proxy Pattern)
1)从一个“需求/问题”引入:为什么我需要代理?
你可能遇到过这些前端场景:
- 图片很多的列表页,首屏加载慢:能不能“先占位,滚动到可视区再加载真实图片”?
- 点击按钮会触发昂贵请求:能不能“相同参数的请求别重复打,直接复用结果”?
- 有些操作需要权限校验/登录态:能不能“先拦一下,不符合条件就不让继续执行”?
- 表单输入频繁触发查询:能不能“别每个字符都请求,做防抖/节流”?
这些诉求背后都有共同点:我想在真正执行某个对象/函数的核心逻辑之前或之后,加一层控制。
这就是代理模式非常擅长解决的问题。
2)代理模式的关键:它到底是什么?
代理模式(Proxy Pattern):为目标对象/函数提供一个“代理”,由代理来控制对目标的访问。
你可以把它理解成:
- 目标(Real Subject):真正干活的人/对象
- 代理(Proxy):中间人/门卫/缓存层,负责“拦截、增强、限制、转发”
- 调用方(Client):只跟代理打交道(也可以无感)
代理模式的核心价值(前端视角)
- 访问控制:权限、登录态、黑白名单、环境判断
- 性能优化:缓存、懒加载、合并请求、节流/防抖
- 解耦增强:不改目标代码,在外面加日志、埋点、错误处理
一句话抓重点:
不改变原对象核心逻辑,通过代理添加“控制与增强”。
3)简单代码例子:用“缓存代理”避免重复请求
下面用一个非常常见的场景:同一个请求参数在短时间内重复触发(比如重复点击、多个组件请求同一资源)。我们用代理做缓存。
// 目标函数:真正的请求(这里用 setTimeout 模拟网络请求)
function fetchUserFromServer(userId) {
console.log("[server] fetching user:", userId);
return new Promise((resolve) => {
setTimeout(() => resolve({ id: userId, name: "User_" + userId }), 300);
});
}
// 代理:增加缓存能力(memoization)
function createCachedFetch(fetchFn) {
const cache = new Map(); // key -> Promise
return function cachedFetch(key) {
if (cache.has(key)) {
console.log("[proxy] hit cache:", key);
return cache.get(key);
}
console.log("[proxy] miss cache, request:", key);
const promise = fetchFn(key).catch((err) => {
// 注意:失败的请求一般不应缓存,避免“永久失败”
cache.delete(key);
throw err;
});
cache.set(key, promise);
return promise;
};
}
const fetchUser = createCachedFetch(fetchUserFromServer);
// demo:连续调用两次同一个 userId,只会发一次“真实请求”
fetchUser(1).then(console.log);
fetchUser(1).then(console.log);
fetchUser(2).then(console.log);
你会看到输出大概是:
- 第一次
fetchUser(1):代理 miss,走真实请求 - 第二次
fetchUser(1):代理 hit,直接复用第一次的 Promise fetchUser(2):新参数,新请求
这就是典型的代理增强:调用方无需知道缓存细节。
4)前端常见应用场景(很实用)
4.1 虚拟代理:图片懒加载 / 占位符
- 先展示占位图,进入可视区再加载真实资源
- 常见于瀑布流、长列表、详情页多图
4.2 缓存代理:请求结果缓存 / 计算结果缓存
- 相同参数直接返回缓存
- 适合:字典数据、配置、幂等查询、昂贵计算(如大 JSON 解析)
4.3 保护代理:权限控制 / 登录校验
- 调用前先判断是否登录/是否有权限
- 适合:支付、下单、查看敏感信息、管理后台按钮
4.4 代理作为“统一切面”:日志、埋点、错误处理
- 在函数调用前后统一加埋点和耗时统计
- 在请求层统一做错误兜底、重试、熔断
4.5 函数代理:防抖/节流(本质上也是“控制访问频率”)
- 输入框联想:防抖
- 滚动监听:节流
5)注意事项:代理模式不是“万能中间层”
-
别滥用导致调用链过深
代理层太多会让调试困难:到底是哪里改了参数、哪里吞了错误? -
缓存要考虑失效策略
- 什么时候过期(TTL)?
- 是否需要手动清理?
- 是否区分用户、环境、权限(避免串数据)?
-
Promise 缓存要特别注意失败缓存
通常失败不缓存,否则一次失败可能导致后续一直失败(上面的示例用catch删除缓存就是为此)。 -
代理里的副作用要可控
例如埋点、日志、重试等,不要影响原本的业务语义(比如改变返回值类型、吞异常)。 -
代理不等于 ES6 的
Proxy语法糖
设计模式讲的是思想:你可以用闭包/高阶函数/类来实现。ES6Proxy只是其中一种实现手段。
小结
- 代理模式解决的是:在不改目标逻辑的前提下,控制访问并进行增强。
- 前端最常用:缓存、懒加载、权限控制、埋点、错误处理、防抖节流。
- 用得好能显著提升性能与可维护性;用得过度会让系统“黑盒化”。