在现代前端开发中,框架和库的选择对于开发效率和项目的可维护性至关重要。Vue.js 作为一款轻量级的前端框架,因其简洁的 API 和易用性而广受欢迎。
Vue.js 响应式原理概述
Vue.js 的核心特性之一就是响应式数据绑定。它允许我们将 JavaScript 对象与 DOM 元素建立连接,当数据变化时,视图能够自动更新。Vue 的响应式系统是通过 Object.defineProperty(在 Vue 2.x 中)或 Proxy(在 Vue 3.x 中)实现的。Vue 将数据对象中的每个属性都转化为 getter 和 setter,从而监控数据的访问和修改。
Vue 2.x 的实现方式
在 Vue 2.x 中,响应式系统是通过 Object.defineProperty() 来实现的。每个数据对象的属性都会被转化为 getter 和 setter,从而实现对数据变动的追踪。当属性被访问时,getter 会被触发;当属性被修改时,setter 会被触发,从而触发视图更新。
一个简单的 Vue 2.x 响应式实现的例子:
function defineReactive(obj, key, val) {
let dep = new Dep(); // 用于依赖收集
Object.defineProperty(obj, key, {
get() {
// 触发依赖收集
if (Dep.target) {
dep.addSub(Dep.target);
}
return val;
},
set(newVal) {
if (newVal !== val) {
val = newVal;
dep.notify(); // 数据更新时通知依赖更新
}
}
});
}
class Dep {
constructor() {
this.subs = []; // 存储所有依赖
}
addSub(watcher) {
this.subs.push(watcher);
}
notify() {
this.subs.forEach(watcher => watcher.update());
}
}
class Watcher {
constructor(vm, exp, cb) {
this.vm = vm;
this.exp = exp;
this.cb = cb;
this.value = this.get(); // 初始取值,触发 getter
}
get() {
Dep.target = this; // 设置全局的 target,收集依赖
let value = this.vm[this.exp]; // 访问属性时会触发 getter
Dep.target = null; // 清除 target
return value;
}
update() {
let newVal = this.vm[this.exp];
this.cb(newVal); // 数据变化后调用回调
}
}
let data = { message: 'Hello Vue!' };
defineReactive(data, 'message', data.message);
let watcher = new Watcher(data, 'message', (newVal) => {
console.log('message updated to:', newVal);
});
data.message = 'Hello World!'; // 这会触发 set,从而触发 update
在这个例子中,我们定义了 defineReactive 函数,它使用 Object.defineProperty() 为对象的属性创建了 getter 和 setter。每当 data.message 的值发生变化时,watcher.update() 就会被调用,从而实现视图的自动更新。
Vue 3.x 的实现方式
在 Vue 3.x 中,响应式系统的实现发生了重大变化。Vue 3 引入了 Proxy 对象,替代了 Object.defineProperty(),从而使得响应式系统更加灵活且性能更高。Proxy 可以拦截对象的所有操作,包括属性的读取、设置、删除等。
Vue 3 使用 Proxy 实现响应式对象时,不再依赖于 getter 和 setter,而是通过 Proxy 的陷阱(trap)来拦截操作,从而实现数据的响应式管理。
Vue 3.x 的响应式实现方式如下所示:
function reactive(target) {
const handler = {
get(target, key) {
console.log(`Getting ${key}:`, target[key]);
// 依赖收集过程
if (Dep.target) {
dep.addSub(Dep.target);
}
return target[key];
},
set(target, key, value) {
console.log(`Setting ${key} to ${value}`);
target[key] = value;
dep.notify(); // 数据更新时通知依赖更新
return true;
}
};
let dep = new Dep();
return new Proxy(target, handler);
}
class Dep {
constructor() {
this.subs = [];
}
addSub(watcher) {
this.subs.push(watcher);
}
notify() {
this.subs.forEach(watcher => watcher.update());
}
}
class Watcher {
constructor(vm, exp, cb) {
this.vm = vm;
this.exp = exp;
this.cb = cb;
this.value = this.get(); // 初始取值,触发 getter
}
get() {
Dep.target = this; // 设置全局的 target,收集依赖
let value = this.vm[this.exp]; // 访问属性时会触发 getter
Dep.target = null; // 清除 target
return value;
}
update() {
let newVal = this.vm[this.exp];
this.cb(newVal); // 数据变化后调用回调
}
}
let data = reactive({ message: 'Hello Vue 3!' });
let watcher = new Watcher(data, 'message', (newVal) => {
console.log('message updated to:', newVal);
});
data.message = 'Hello Proxy!'; // 这会触发 set,从而触发 update
在 Vue 3.x 中,我们使用 Proxy 来替代 Object.defineProperty(),使得整个对象都变成响应式,而不仅仅是它的属性。这使得 Vue 3 在处理复杂数据结构时表现得更为高效和灵活。
Proxy 的使用场景举例
在使用 Coze API 的时候,需要通过访问令牌进行 API 请求的鉴权,这里选择 OAuth PKCE 的方式来做例子。
PKCE 验证原理
PKCE(Proof Key for Code Exchange)是一种增强安全性的授权协议,它扩展了 OAuth 2.0 中的授权码流程,特别适用于公共客户端(例如移动应用或 SPA),因为这些客户端无法安全地存储客户端密钥。PKCE的目的是防止授权码拦截攻击。
PKCE通过使用两个关键元素来增强OAuth 2.0的安全性:
- code_verifier:客户端生成的一个随机字符串,用于证明授权请求的合法性。
- code_challenge:客户端将
code_verifier使用特定哈希算法(通常是SHA256)生成的散列值,并在请求授权时将其传递给授权服务器。
授权流程的基本步骤如下:
- 客户端首先生成一个随机的
code_verifier,然后基于此生成code_challenge并作为参数发送给授权服务器。 - 授权服务器将
code_challenge保存在服务器端,等待客户端在交换授权码时提供对应的code_verifier。 - 用户授权后,授权服务器返回一个授权码。
- 客户端将授权码与
code_verifier一起发送到授权服务器,请求令牌。 - 授权服务器使用
code_verifier和存储的code_challenge进行验证。如果匹配,授权服务器返回访问令牌,否则拒绝请求。
由于 PKCE 的存在,即使授权码被拦截,攻击者也无法使用该授权码获取访问令牌,因为没有 code_verifier,只有持有授权码和正确的 code_verifier 才能成功交换令牌。
每次请求时自动刷新 token
PKCE 虽然虽然按钮,但获取到的 token 只有十几分钟或者更短的有效期,参考 Coze OAuth PKCE,所以需要在每次发起请求时先检查 token 是否过期,过期了则使用 refresh_token 进行刷新。
所以需要封装一个函数,其目的是创建一个带有自动令牌刷新的 CozeAPI 客户端。它结合了 OAuth 2.0 的令牌刷新机制,在令牌过期时自动进行刷新,确保客户端能够持续访问 API。该函数的核心是通过代理(Proxy)实现自动令牌刷新机制,代码如下:
/**
* 创建一个带有自动刷新功能的 `CozeAPI` 客户端
*
* @returns `CozeAPI` 对象
*/
export const createCozeClientWithAutoRefresh = (): CozeAPI => {
const accessToken = sessionStorage.getItem("pkce_access_token") || "";
const cozeClient = getCozeClient(accessToken);
const handler: ProxyHandler<CozeAPI> = {
get(target: CozeAPI, propKey: keyof CozeAPI) {
const origProperty = target[propKey];
console.log("Triggered method:", propKey, typeof origProperty);
if (typeof origProperty !== "function") {
return origProperty;
}
return function (this: CozeAPI, ...args: unknown[]): Promise<unknown> {
return (async () => {
if (isTokenExpired()) {
const authRefreshToken =
sessionStorage.getItem("pkce_refresh_token");
if (authRefreshToken) {
const authToken: OAuthToken =
await refreshToken(authRefreshToken);
saveAuthToken(authToken);
target.token = authToken.access_token;
} else {
throw new Error("Refresh token is missing.");
}
}
return Reflect.apply(origProperty, target, args);
})();
};
},
};
return new Proxy(cozeClient, handler);
};
1. 获取现有的访问令牌
首先,该函数尝试从 sessionStorage 获取当前的访问令牌(pkce_access_token)。如果令牌存在且有效,它会用这个令牌初始化 CozeAPI 客户端。
2. 使用代理拦截 API 请求
createCozeClientWithAutoRefresh 通过 JavaScript 的 Proxy 对 CozeAPI 客户端对象进行了封装,代理的目的是在每次调用 CozeAPI 的方法时,检查访问令牌是否过期。如果令牌过期,代理会自动触发刷新令牌的过程,并更新客户端的令牌。
Proxy 的使用确保了每次调用 API 方法时,都会先检查令牌是否有效。如果令牌已经过期,代理会尝试使用刷新令牌获取一个新的访问令牌。
3. 刷新令牌逻辑
在代理拦截的 get 方法中,如果调用的是 CozeAPI 的方法,代理首先会检查访问令牌是否过期(通过 isTokenExpired 函数)。如果令牌已过期,代理会从 sessionStorage 中获取刷新令牌,并调用 refreshToken 函数来刷新令牌。
4. 返回原方法
一旦令牌被刷新,代理会继续执行原始的 CozeAPI 方法,并将其结果返回。
5. 创建代理客户端
最后,createCozeClientWithAutoRefresh 会返回一个新的 CozeAPI 客户端,该客户端使用了代理机制,可以在访问令牌过期时自动刷新令牌并继续执行请求。