最近在工作过程中,遇到在页面初始化时,多个模块都请求同一个接口的场景,导致network面板中存在很多重复请求,拖慢页面整体的初始化时间。
由于不想改动业务代码,所以考虑在请求层面做优化,即对于某个时间段内所有的幂等请求都返回同一个promise实例,这样只会有一个promise去发送网络请求,且请求完成后,通过该promise.then添加的回调都会被执行。
由于这个需求输入输出明确,且网上有大量类似案例,所以想通过AI来帮忙生成代码,以下是与豆包 斗智斗勇的全过程。
开始battle
import axios from 'axios';
// 用于存储请求的缓存对象
const requestCache = {};
/**
* 封装的带有缓存功能的 Axios 请求函数
* @param {Object} config - Axios 请求配置对象
* @returns {Promise} - Axios 请求的 Promise
*/
function cachedAxios(config) {
const url = config.url;
// 如果缓存中已经存在该 URL 对应的 Promise,直接返回
if (requestCache[url]) {
return requestCache[url];
}
// 发起新的 Axios 请求
const promise = axios(config)
.then(response => {
// 请求成功后,将响应数据存入缓存
requestCache[url] = response;
return response;
})
.catch(error => {
// 请求失败后,也将错误信息存入缓存
requestCache[url] = Promise.reject(error);
return Promise.reject(error);
});
// 将 Promise 存入缓存
requestCache[url] = promise;
return promise;
}
很明显,这份代码没有考虑请求参数的情况,继续battle
import axios from 'axios';
// 用于存储请求的缓存对象
const requestCache = {};
/**
* 生成唯一的缓存键
* @param {Object} config - Axios 请求配置对象
* @returns {string} - 唯一的缓存键
*/
function generateCacheKey(config) {
let key = config.url;
if (config.params) {
// 将 params 转换为字符串并排序,确保相同参数顺序不同时键相同
const sortedParams = Object.entries(config.params).sort((a, b) => a[0].localeCompare(b[0]));
key += '?' + sortedParams.map(([k, v]) => `${k}=${v}`).join('&');
}
if (config.data) {
// 将 body 数据转换为字符串
key += JSON.stringify(config.data);
}
return key;
}
/**
* 封装的带有缓存功能的 Axios 请求函数
* @param {Object} config - Axios 请求配置对象
* @returns {Promise} - Axios 请求的 Promise
*/
function cachedAxios(config) {
const cacheKey = generateCacheKey(config);
// 如果缓存中已经存在该键对应的 Promise,直接返回
if (requestCache[cacheKey]) {
return requestCache[cacheKey];
}
// 发起新的 Axios 请求
const promise = axios(config)
.then(response => {
// 请求成功后,将响应数据存入缓存
requestCache[cacheKey] = response;
return response;
})
.catch(error => {
// 请求失败后,也将错误信息存入缓存
requestCache[cacheKey] = Promise.reject(error);
return Promise.reject(error);
});
// 将 Promise 存入缓存
requestCache[cacheKey] = promise;
return promise;
}
可以看到,AI生成的代码中使用了一个全局的对象requestCache,会存在内存泄漏的风险,让AI自查一下
import axios from 'axios';
// 用于存储请求的缓存对象
const requestCache = {};
/**
* 生成唯一的缓存键
* @param {Object} config - Axios 请求配置对象
* @returns {string} - 唯一的缓存键
*/
function generateCacheKey(config) {
let key = config.url;
if (config.params) {
// 将 params 转换为字符串并排序,确保相同参数顺序不同时键相同
const sortedParams = Object.entries(config.params).sort((a, b) => a[0].localeCompare(b[0]));
key += '?' + sortedParams.map(([k, v]) => `${k}=${v}`).join('&');
}
if (config.data) {
// 将 body 数据转换为字符串
key += JSON.stringify(config.data);
}
return key;
}
/**
* 封装的带有缓存功能的 Axios 请求函数
* @param {Object} config - Axios 请求配置对象
* @param {number} cacheDuration - 缓存持续时间(毫秒),默认 5 分钟
* @returns {Promise} - Axios 请求的 Promise
*/
function cachedAxios(config, cacheDuration = 5 * 60 * 1000) {
const cacheKey = generateCacheKey(config);
const now = Date.now();
// 检查缓存中是否存在该键对应的 Promise 且未过期
if (requestCache[cacheKey] && requestCache[cacheKey].expiry > now) {
return requestCache[cacheKey].promise;
}
// 发起新的 Axios 请求
const promise = axios(config)
.then(response => {
// 请求成功后,将响应数据存入缓存
requestCache[cacheKey] = {
promise: response,
expiry: now + cacheDuration
};
return response;
})
.catch(error => {
// 请求失败后,也将错误信息存入缓存
requestCache[cacheKey] = {
promise: Promise.reject(error),
expiry: now + cacheDuration
};
return Promise.reject(error);
});
return promise;
}
可以看到AI添加了缓存时间的处理,但并没有清理过期的requestCache key,继续battle
// 代码同上
// 定期清除过期缓存的函数
function clearExpiredCache() {
const now = Date.now();
for (const key in requestCache) {
if (requestCache.hasOwnProperty(key) && requestCache[key].expiry < now) {
delete requestCache[key];
}
}
}
// 每 5 分钟执行一次清除过期缓存的操作
setInterval(clearExpiredCache, 5 * 60 * 1000);
AI添加了定时清除的逻辑,基本满足了我们的需求,来一份ts的代码
import axios, { AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
// 用于存储请求的缓存对象
const requestCache: { [key: string]: { promise: AxiosResponse | Promise<AxiosError>, expiry: number } } = {};
/**
* 生成唯一的缓存键
* @param {AxiosRequestConfig} config - Axios 请求配置对象
* @returns {string} - 唯一的缓存键
*/
function generateCacheKey(config: AxiosRequestConfig): string {
let key = config.url || '';
if (config.params) {
// 将 params 转换为字符串并排序,确保相同参数顺序不同时键相同
const sortedParams = Object.entries(config.params).sort((a, b) => a[0].localeCompare(b[0]));
key += '?' + sortedParams.map(([k, v]) => `${k}=${v}`).join('&');
}
if (config.data) {
// 将 body 数据转换为字符串
key += JSON.stringify(config.data);
}
return key;
}
/**
* 封装的带有缓存功能的 Axios 请求函数
* @param {AxiosRequestConfig} config - Axios 请求配置对象
* @param {number} cacheDuration - 缓存持续时间(毫秒),默认 5 分钟
* @returns {Promise<AxiosResponse | AxiosError>} - Axios 请求的 Promise
*/
function cachedAxios(config: AxiosRequestConfig, cacheDuration: number = 5 * 60 * 1000): Promise<AxiosResponse | AxiosError> {
const cacheKey = generateCacheKey(config);
const now = Date.now();
// 检查缓存中是否存在该键对应的 Promise 且未过期
if (requestCache[cacheKey] && requestCache[cacheKey].expiry > now) {
return requestCache[cacheKey].promise;
}
// 发起新的 Axios 请求
const promise = axios(config)
.then((response: AxiosResponse) => {
// 请求成功后,将响应数据存入缓存
requestCache[cacheKey] = {
promise: response,
expiry: now + cacheDuration
};
return response;
})
.catch((error: AxiosError) => {
// 请求失败后,也将错误信息存入缓存
requestCache[cacheKey] = {
promise: Promise.reject(error),
expiry: now + cacheDuration
};
return Promise.reject(error);
});
return promise;
}
// 定期清除过期缓存的函数
function clearExpiredCache() {
const now = Date.now();
for (const key in requestCache) {
if (requestCache.hasOwnProperty(key) && requestCache[key].expiry < now) {
delete requestCache[key];
}
}
}
// 每 5 分钟执行一次清除过期缓存的操作
setInterval(clearExpiredCache, 5 * 60 * 1000);
可以看到AI生成的ts代码质量还是很不错的,我们复制到项目中看一下
额,啪啪打脸啊,让AI自查一下
import axios, { AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
// 用于存储请求的缓存对象
const requestCache: { [key: string]: { promise: Promise<AxiosResponse | AxiosError>, expiry: number } } = {};
/**
* 生成唯一的缓存键
* @param {AxiosRequestConfig} config - Axios 请求配置对象
* @returns {string} - 唯一的缓存键
*/
function generateCacheKey(config: AxiosRequestConfig): string {
let key = config.url || '';
if (config.params) {
// 将 params 转换为字符串并排序,确保相同参数顺序不同时键相同
const sortedParams = Object.entries(config.params).sort((a, b) => a[0].localeCompare(b[0]));
key += '?' + sortedParams.map(([k, v]) => `${k}=${v}`).join('&');
}
if (config.data) {
// 将 body 数据转换为字符串
key += JSON.stringify(config.data);
}
return key;
}
/**
* 封装的带有缓存功能的 Axios 请求函数
* @param {AxiosRequestConfig} config - Axios 请求配置对象
* @param {number} cacheDuration - 缓存持续时间(毫秒),默认 5 分钟
* @returns {Promise<AxiosResponse | AxiosError>} - Axios 请求的 Promise
*/
function cachedAxios(config: AxiosRequestConfig, cacheDuration: number = 5 * 60 * 1000): Promise<AxiosResponse | AxiosError> {
const cacheKey = generateCacheKey(config);
const now = Date.now();
// 检查缓存中是否存在该键对应的 Promise 且未过期
if (requestCache[cacheKey] && requestCache[cacheKey].expiry > now) {
return requestCache[cacheKey].promise;
}
// 发起新的 Axios 请求
const promise = axios(config)
.then((response: AxiosResponse): AxiosResponse => {
// 请求成功后,将响应数据存入缓存
requestCache[cacheKey] = {
promise: Promise.resolve(response),
expiry: now + cacheDuration
};
return response;
})
.catch((error: AxiosError): AxiosError => {
// 请求失败后,也将错误信息存入缓存
requestCache[cacheKey] = {
promise: Promise.reject(error),
expiry: now + cacheDuration
};
return error;
});
return promise;
}
// 定期清除过期缓存的函数
function clearExpiredCache() {
const now = Date.now();
for (const key in requestCache) {
if (requestCache.hasOwnProperty(key) && requestCache[key].expiry < now) {
delete requestCache[key];
}
}
}
// 每 5 分钟执行一次清除过期缓存的操作
setInterval(clearExpiredCache, 5 * 60 * 1000);
可以看到,修改之后已经没有类型问题了,给AI点个赞👍
但我希望这个方法可以以工具函数的形式提供出去,这意味着这个文件很可能会被多次import,这样就会导致开启多个定时清除任务,浪费浏览器资源,继续让AI优化
很贴心,还给出了验证demo(莫不是以为我不懂!!!),看看它的解决方案
// 省略重复代码
let intervalId: ReturnType<typeof setInterval> | null = null;
function startCacheCleanup() {
if (!intervalId) {
intervalId = setInterval(clearExpiredCache, 5 * 60 * 1000);
}
}
// 只在模块首次加载时启动一次
startCacheCleanup();
很棒,人工分析下AI写的代码
这块的处理,是在请求完成之后才写入缓存requestCache,很明显在并发请求的情况下,如果第一个请求未完成,后续的请求无法复用promise实例,不满足我们的要求。battle继续
/**
* 封装的带有缓存功能的 Axios 请求函数
* @param {AxiosRequestConfig} config - Axios 请求配置对象
* @param {number} cacheDuration - 缓存持续时间(毫秒),默认 5 分钟
* @returns {Promise<AxiosResponse | AxiosError>} - Axios 请求的 Promise
*/
function cachedAxios(config: AxiosRequestConfig, cacheDuration: number = 5 * 60 * 1000): Promise<AxiosResponse | AxiosError> {
const cacheKey = generateCacheKey(config);
const now = Date.now();
// 如果缓存中已经存在该键对应的 Promise 且未过期
if (requestCache[cacheKey] && requestCache[cacheKey].expiry > now) {
return requestCache[cacheKey].promise;
}
// 创建一个占位的 Promise
let pendingPromise: Promise<AxiosResponse | AxiosError>;
if (!requestCache[cacheKey] || requestCache[cacheKey].expiry <= now) {
pendingPromise = new Promise((resolve, reject) => {
requestCache[cacheKey] = {
promise: pendingPromise,
expiry: now + cacheDuration
};
});
} else {
pendingPromise = requestCache[cacheKey].promise;
}
// 如果占位的 Promise 还没有结果,发起实际的请求
if (!requestCache[cacheKey].promise || requestCache[cacheKey].expiry <= now) {
axios(config)
.then((response: AxiosResponse) => {
const updatedPromise = Promise.resolve(response);
requestCache[cacheKey].promise = updatedPromise;
requestCache[cacheKey].expiry = now + cacheDuration;
resolve(response);
})
.catch((error: AxiosError) => {
const updatedPromise = Promise.reject(error);
requestCache[cacheKey].promise = updatedPromise;
requestCache[cacheKey].expiry = now + cacheDuration;
reject(error);
});
}
return pendingPromise;
}
emmm... 感觉AI已经神志不清了,让它自查一下吧
/**
* 封装的带有缓存功能的 Axios 请求函数
* @param {AxiosRequestConfig} config - Axios 请求配置对象
* @param {number} cacheDuration - 缓存持续时间(毫秒),默认 5 分钟
* @returns {Promise<AxiosResponse | AxiosError>} - Axios 请求的 Promise
*/
function cachedAxios(config: AxiosRequestConfig, cacheDuration: number = 5 * 60 * 1000): Promise<AxiosResponse | AxiosError> {
const cacheKey = generateCacheKey(config);
const now = Date.now();
// 如果缓存中已经存在该键对应的 Promise 且未过期
if (requestCache[cacheKey] && requestCache[cacheKey].expiry > now) {
return requestCache[cacheKey].promise;
}
// 创建一个占位的 Promise
const pendingPromise = new Promise<AxiosResponse | AxiosError>((resolve, reject) => {
requestCache[cacheKey] = {
promise: pendingPromise,
expiry: now + cacheDuration
};
axios(config)
.then((response: AxiosResponse) => {
const updatedPromise = Promise.resolve(response);
requestCache[cacheKey].promise = updatedPromise;
requestCache[cacheKey].expiry = now + cacheDuration;
resolve(response);
})
.catch((error: AxiosError) => {
const updatedPromise = Promise.reject(error);
requestCache[cacheKey].promise = updatedPromise;
requestCache[cacheKey].expiry = now + cacheDuration;
reject(error);
});
});
return pendingPromise;
}
这下应该没问题了,部署测试下
有点不对劲,点进去看看
cachedAxios函数逻辑应该有问题,会在某种情况下返回undefined,先让AI自查一下
看来它还挺自信的,人工介入下:
问题出在这一行,这段代码在requestCache中创建了一个占位promise,但注意此时pendingPromise还没有执行完成,赋值给requestCache[key].promise是undefined,所以后续(并发情况)缓存命中的时候,返回的promise为undefined
给AI点提示,看它能否修正
// ...省略重复代码
function cachedAxios(config: AxiosRequestConfig, cacheDuration: number = 5 * 60 * 1000): Promise<AxiosResponse | AxiosError> {
const cacheKey = generateCacheKey(config);
const now = Date.now();
// 如果缓存中已经存在该键对应的 Promise 且未过期
const cached = requestCache[cacheKey];
if (cached && cached.expiry > now) {
// 再次确认 promise 是否已经被正确赋值
if (cached.promise) {
return cached.promise;
} else {
return cached.promise as Promise<AxiosResponse | AxiosError>;
}
}
// ...省略重复代码
}
有点意思,居然能想到用as,这不自欺欺人么,battle继续
/**
* 封装的带有缓存功能的 Axios 请求函数
* @param {AxiosRequestConfig} config - Axios 请求配置对象
* @param {number} cacheDuration - 缓存持续时间(毫秒),默认 5 分钟
* @returns {Promise<AxiosResponse | AxiosError>} - Axios 请求的 Promise
*/
function cachedAxios(config: AxiosRequestConfig, cacheDuration: number = 5 * 60 * 1000): Promise<AxiosResponse | AxiosError> {
const cacheKey = generateCacheKey(config);
const now = Date.now();
let cached = requestCache[cacheKey];
if (cached && cached.expiry > now) {
return cached.promise;
}
// 创建一个占位的 Promise
const newPromise = new Promise<AxiosResponse | AxiosError>((resolve, reject) => {
axios(config)
.then((response: AxiosResponse) => {
const updatedPromise = Promise.resolve(response);
requestCache[cacheKey] = {
promise: updatedPromise,
expiry: now + cacheDuration
};
resolve(response);
})
.catch((error: AxiosError) => {
const updatedPromise = Promise.reject(error);
requestCache[cacheKey] = {
promise: updatedPromise,
expiry: now + cacheDuration
};
reject(error);
});
});
if (!cached || cached.expiry <= now) {
requestCache[cacheKey] = {
promise: newPromise,
expiry: now + cacheDuration
};
}
return newPromise;
}
应该没什么问题了,保险期间,让AI再自证一下:
逻辑自洽,验证代码也没啥问题,勉强给AI点个赞吧
后续
目前已经正常发布到测试环境,正好借一轮验收的机会验证下,后续有问题再补充。
ps: AI代码的生成很大程度受限于上下文信息的完整性,往往要通过多轮对话来反复验证其生成代码的准确性,但对开发效率的提升确实很明显。