前端开发中的代理模式——从 Vue 3 响应式到日常开发

Vue 3 的 reactive(),配置 Axios 拦截器、webpack-dev-server 的反向代理这些大家肯定都不陌生。它们的背后都是同一个设计模式------------代理模式。代理模式的核心是为一个对象提供一个替身,以控制对这个对象的访问。在实际开发中,代理模式能够帮我们实现权限验证、缓存、性能优化、请求拦截等额外逻辑,而无需修改原始对象。本文从 Vue 3 响应式系统、缓存代理、拦截器、虚拟代理、保护代理、反向代理等真实场景出发,带你彻底掌握代理模式在现代前端中的落地方式。

一、代理模式的核心思想

当客户不方便直接访问一个对象,或者需要额外的控制时,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。

代理模式由三部分组成:

  • 目标对象(Target):实际提供功能的对象。
  • 代理对象(Proxy):控制对目标对象访问的替身。
  • 客户端(Client):通过代理对象间接使用目标对象。

在 JavaScript 中,ES6 原生提供的 Proxy 构造函数让我们可以方便地创建代理对象,拦截并自定义对目标对象的各种操作(读取、赋值、函数调用等)。

生活中的代理随处可见:

  • 代购:你无法直接访问国外商店,代购作为代理帮你完成购买。
  • 缓存代理:浏览器缓存静态资源,下次访问时直接返回缓存,避免重复下载。
  • 身份验证代理:登录时,代理先验证用户身份,通过后才将请求转发给后端。
  • 金融代理:投资经纪人作为代理帮你买卖股票。

对应到前端开发中,代理模式最常见的应用包括:Vue 3 响应式系统、Axios 拦截器、图片懒加载、表单校验、webpack-dev-server 反向代理等。

二、Vue 3 响应式系统------代理模式的巅峰应用

Vue 3 的响应式核心就是用 Proxy 实现的。它代理了整个数据对象,拦截属性的读取和写入,从而实现依赖收集和派发更新。

javascript 复制代码
function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      track(target, key);  // 依赖收集
      const result = Reflect.get(target, key, receiver);
      if (typeof result === 'object' && result !== null) {
        return reactive(result); // 惰性递归代理
      }
      return result;
    },
    set(target, key, value, receiver) {
      const oldVal = target[key];
      const success = Reflect.set(target, key, value, receiver);
      if (oldVal !== value) trigger(target, key); // 派发更新
      return success;
    }
  });
}

为什么 Vue 3 抛弃了 Object.defineProperty

  • 自动追踪新增/删除属性,不再需要 Vue.set
  • 数组处理更优雅,不再需要重写七个变异方法。
  • 惰性代理,只在访问深层对象时才创建代理,性能更好。

这正是代理模式最核心的价值:在不修改原对象的前提下,控制对它的访问。

三、缓存代理------为昂贵的操作提速

代理模式可以用来缓存计算结果或网络请求结果,避免重复执行。下面是一个计算缓存代理的例子:

javascript 复制代码
class Calculator {
  multiply(a, b) {
    console.log(`Calculating ${a} * ${b}`);
    return a * b;
  }
}

class CachingProxy {
  constructor() {
    this.calculator = new Calculator();
    this.cache = {};
  }

  multiply(a, b) {
    const key = `${a}*${b}`;
    if (this.cache[key] === undefined) {
      this.cache[key] = this.calculator.multiply(a, b);
    }
    return this.cache[key];
  }
}

const proxy = new CachingProxy();
console.log(proxy.multiply(2, 3)); // 第一次计算
console.log(proxy.multiply(2, 3)); // 从缓存获取

同样,我们可以用 Proxy 为任意函数添加缓存能力:

javascript 复制代码
function createCachedAPI(apiFn) {
  const cache = new Map();
  return new Proxy(apiFn, {
    apply(target, thisArg, args) {
      const key = JSON.stringify(args);
      if (cache.has(key)) {
        return cache.get(key);
      }
      const result = Reflect.apply(target, thisArg, args);
      cache.set(key, result);
      return result;
    }
  });
}

// 原函数
async function fetchStats(type) {
  return fetch(`/api/stats?type=${type}`).then(r => r.json());
}

const cachedFetch = createCachedAPI(fetchStats);
await cachedFetch('user'); // 发起请求
await cachedFetch('user'); // 命中缓存

在实际项目中,这种缓存代理可以显著减少重复请求,优化用户体验。

四、拦截器代理------Axios 的精髓

Axios 的请求和响应拦截器本质上就是代理模式的应用。我们通过拦截器在请求发送前和响应返回后添加统一逻辑,而不修改原始的 fetch 或 XHR。

javascript 复制代码
// 请求拦截器
axios.interceptors.request.use(
  (config) => {
    config.headers.Authorization = `author ${getToken()}`;
    return config;
  },
  (error) => Promise.reject(error)
);

// 响应拦截器
axios.interceptors.response.use(
  (response) => {
    if (response.data.code !== 0) {
      message.error(response.data.message);
    }
    return response.data;
  },
  (error) => {
    message.error('网络异常');
    return Promise.reject(error);
  }
);

在这里,axios 实例相当于代理对象,它拦截了所有请求和响应,在原始的网络请求前后插入了额外的处理步骤,而业务代码无需关心这些细节。这正是代理模式中"控制访问"的体现。

五、虚拟代理------按需加载图片

虚拟代理可以延迟加载资源,直到真正需要时才加载。图片懒加载是一个典型场景:页面上的图片先用占位图,只有当图片进入视口时才加载真实图片。

我们可以用 Proxy 来控制 <img> 元素的 src 属性,实现简单的懒加载:

javascript 复制代码
function createLazyImage(src, placeholder) {
  const img = new Image();
  img.src = placeholder; // 占位图

  const observer = new IntersectionObserver(([entry]) => {
    if (entry.isIntersecting) {
      img.src = src; // 真正加载
      observer.disconnect();
    }
  });
  observer.observe(img);
  return img;
}

// 也可以结合 Proxy 监控 src 修改
function createImageProxy(src, placeholder) {
  const img = new Image();
  img.src = placeholder;
  const observer = new IntersectionObserver(([entry]) => {
    if (entry.isIntersecting) {
      img.src = src;
      observer.disconnect();
    }
  });
  observer.observe(img);

  return new Proxy(img, {
    set(target, key, value) {
      if (key === 'src' && value === src) {
        // 当尝试设置真实 src 时,先检查视口
        if (!target.complete) { // 还没加载完成
          return Reflect.set(target, key, placeholder);
        }
      }
      return Reflect.set(target, key, value);
    }
  });
}

通过虚拟代理,我们将昂贵的图片加载操作延迟到了合适的时机,大大提升了页面的首屏渲染速度。


六、保护代理------数据校验与只读保护

代理对象可以用于控制对目标对象的访问权限,比如表单输入校验、只读保护等。

1. 表单数据实时校验

javascript 复制代码
function createValidator(data, rules) {
  return new Proxy(data, {
    set(target, key, value) {
      const rule = rules[key];
      if (rule && !rule(value)) {
        console.error(`${String(key)} 校验失败`);
        return false; // 阻止赋值
      }
      return Reflect.set(target, key, value);
    }
  });
}

const rules = {
  age: (v) => v > 0 && v < 150,
  email: (v) => /^\S+@\S+\.\S+$/.test(v),
};

const form = createValidator({ age: 0, email: '' }, rules);
form.age = -5; //  age 校验失败,赋值被阻止
form.age = 25; //  成功

2. 只读保护

javascript 复制代码
function createReadOnly(obj, name = '对象') {
  return new Proxy(obj, {
    set() {
      console.warn(`${name} 是只读的,不允许修改`);
      return false;
    },
    deleteProperty() {
      console.warn(`${name} 是只读的,不允许删除`);
      return false;
    }
  });
}

const config = createReadOnly({ apiUrl: '/api' }, '应用配置');
config.apiUrl = '/v2'; // 阻止修改并给出友好提示

保护代理让我们的数据更安全,并且能提供比 Object.freeze 更友好的错误提示。

七、反向代理------前端开发环境必备

在前端开发中,我们经常需要解决跨域问题。webpack-dev-server 或 Vite 提供的代理功能,本质上就是反向代理。

javascript 复制代码
// vite.config.js
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://backend-server.com',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
}

在这里,本地的开发服务器充当了代理服务器的角色。浏览器向本地开发服务器发送 /api 开头的请求,代理服务器将这些请求转发到真实的后端服务器,并将响应返回给浏览器。对于浏览器来说,它只知道自己访问的是本地服务器,真实的后端地址被隐藏了。

这种反向代理模式的好处是:

  • 解决跨域问题,浏览器不需要直接访问不同域的后端。
  • 隐藏真实的后端服务器,提高安全性。
  • 可以在代理层添加日志、修改请求头等额外操作。

八、总结

代理模式的核心是为一个对象提供替身,以控制对这个对象的访问 。在 JavaScript 中,ES6 的 ProxyReflect 让代理模式的实现变得非常简洁。代理模式需要注意性能开销和调试复杂性,但它在前端框架和工程化中的价值无可替代。当你需要给现有功能增加缓存、校验、权限控制时,Proxy 往往是最优雅的解决方案。

相关推荐
咖啡八杯1 小时前
GoF设计模式——状态模式
java·面试·架构
Li.map1 小时前
CesiumJS 地图主题系统:用着色器打造实时滤镜
javascript·arcgis·cesium·着色器
用户298698530141 小时前
在 React 中通过 JavaScript 实现 Excel 单元格、行与列的锁定保护
javascript·react.js·excel
এ慕ོ冬℘゜1 小时前
深入理解 JavaScript 事件对象:从 target 到 preventDefault 的实战指南
开发语言·前端·javascript
Listen·Rain2 小时前
Vue概述
前端·javascript·vue.js
尘世中一位迷途小书童2 小时前
Cesium 涟漪扩散点:自定义材质,参数调对了才好看
前端·面试
ttwuai2 小时前
Go 后台富文本图片上传失败排查:前端限制、接口和存储目录要一起看
前端·golang·状态模式
小雪_Snow2 小时前
JS 数组的遍历 —— 两种方式详解
前端·javascript
禅思院2 小时前
AI对话前端从入门到崩溃:一个长对话引发的五层优化战争【五】
前端·架构·前端框架