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 的 Proxy 和 Reflect 让代理模式的实现变得非常简洁。代理模式需要注意性能开销和调试复杂性,但它在前端框架和工程化中的价值无可替代。当你需要给现有功能增加缓存、校验、权限控制时,Proxy 往往是最优雅的解决方案。