前端如何给网络请求【加锁】

695 阅读3分钟

早上好,中午好,晚上好

近日志哥在对公司内部系统进行性能优化时,当表单存在多个用户选择控件时,每个控件的options数据都会请求相同的接口,只是有些参数不一样,考虑能否进行接口缓存,减轻服务端压力。

7d76b0e6-76f1-4227-921f-a4f343a4d9c6.jfif

JavaScript原生实现加锁

第一版:

  1. 创建一个简单的缓存对象来存储基于参数的响应
  2. 当缓存对象中存在基于参数的缓存时,返回对应缓存数据
  3. 否则参数作为key,缓存到缓存对象上

代码实现:

const cache = {};

function generateCacheKey(params) {
  return JSON.stringify(params);
}

function fetchData(url, params) {
  const cacheKey = generateCacheKey(params);

  // 检查缓存中是否已有该请求的响应
  if (cache[cacheKey]) {
    return Promise.resolve(cache[cacheKey]); // 返回缓存的响应
  }

  // 如果缓存中没有,则发起请求
  return fetch(`${url}?${new URLSearchParams(params)}`)
    .then(data => {
      cache[cacheKey] = data; // 存储返回的数据到缓存对象上
      return data;
    });
}

上面的方案有不足之处,就是当一个页面很多地方同时调用这个接口的时候,接口数据还没回来,也就是未能保存到cache上,那么还是会发起网络请求。

第二版:处理并发请求

为了避免对同一个参数组合的重复请求,添加一个锁机制来确保同一时间只有一个请求被发送。

那这个锁该怎么设计呢?

我们可以这样,当相同的参数的时候,返回相同的Promise实例对象,也就是:

const locks = {
    [cacheKey1]: Promise, 
    [cacheKey2]: Promise2,
    ...
}

然后在订阅数据完成后,释放锁。

delete locks[cacheKey];

代码实现:

const cache = {};
const locks = {};

function generateCacheKey(params) {
  return JSON.stringify(params);
}

function fetchData(url, params) {
  const cacheKey = generateCacheKey(params);

  // 检查缓存中是否已有该请求的响应
  if (cache[cacheKey]) {
    return Promise.resolve(cache[cacheKey]); // 返回缓存的响应
  }

  // 如果缓存中没有,则发起请求
  return fetch(`${url}?${new URLSearchParams(params)}`)
    .then(data => {
      cache[cacheKey] = data; // 存储返回的数据到缓存对象上
      return data;
    });
}


function fetchDataWithLock(url, params) {
  const cacheKey = generateCacheKey(params);

  // 如果已有锁,则返回同一个Promise
  if (locks[cacheKey]) {
    return locks[cacheKey];
  }

  // 设置锁
  locks[cacheKey] = fetchData(url, params).finally(() => {
    // 请求完成后释放锁
    delete locks[cacheKey];
  });

  return locks[cacheKey];
}

使用:

const api = '/data/xxx';
fetchDataWithLock(api, { query: 'zhige', page: 1 })
  .then(data => console.log(data))
  .catch(error => console.error(error));

上面的代码其实已经可以实现并发请求缓存了,但还是不好用:

  • 缓存失效:未实现缓存的过期时间
  • 内存泄漏:何时清理缓存和锁,特别是当它们不再需要时
  • 错误处理: 如果接口出错还是会返回相同的Promise,也就是未考虑异常情况

那如果考虑上述情况,那这个封装就一大坨了。

那有没有简单的方式呢?

使用Rxjs的ReplaySubject

志哥因公司的技术栈为Angular,所以接触了Angular的生态,在Angular的生态里,官方高度使用Rxjs,相当于使用Angular技术,就必须学习使用Rxjs。Rxjs里提供了一个类ReplaySubject。用这个类来实现加锁,将会相当轻松,原来几十上百行封装的代码,使用ReplaySubject只用区区10行以内的代码就可以实现,且帮你考虑了很多边界情况。

上代码:

@Injectable({
  providedIn: 'root',
})
export class getUesrsHttpService {
    private subjects = new Map<string, ReplaySubject<any>>();
    fetchData(url, params) {
      const cacheKey = generateCacheKey(params);
      if (!subjects.has(cacheKey)) {
          const subject = new ReplaySubject<any>(1);
          subjects.set(cacheKey, subject);
          this.http.post(url, params).pipe(map<any, any>((res) => res.result)).subscribe(subject);
       }
        return this.subjects.get(cacheKey).asObservable();
    }
}

使用:

const api = '/data/xxx';
fetchData(api, { query: 'zhige', page: 1 }).subscribe(data => {
    console.log(data)
});

上面使用ReplaySubject的例子中,无论有多少个组件订阅fetchData()方法,只要参数相同,都只会有一个HTTP请求被发出,并且所有订阅者都会接收到相同的数据。

志哥我想说

如果想学习Rxjs,可以看志哥的另一篇文章:juejin.cn/post/736607…

想详细了解ReplaySubject用法,点击:cn.rx.js.org/manual/over…

如果惊叹Rxjs的强大,那么你完全有潜力成为团队中的推广大使,引领大家深入了解并广泛应用这一技术。

头冷.jfif