Skip to content

前端实现 SWR(Stale-While-Revalidate)缓存策略 #134

Description

@gluu1

前端实现 SWR(Stale-While-Revalidate)缓存策略

SWR 是由 Vercel 提出的客户端缓存策略,核心是「优先返回缓存数据(Stale)→ 后台异步重新请求验证(Revalidate)→ 数据更新后刷新视图」,既保证用户体验(无加载等待),又能保证数据最终一致性。以下分「基于官方 SWR 库(推荐)」和「手动实现(理解原理)」两种方案讲解,覆盖 React 生态(最常用),也适配非 React 场景。

一、核心原理回顾

SWR 执行流程:

  1. 首次请求:无缓存 → 发起请求 → 数据存入缓存 → 渲染视图;
  2. 再次请求:有缓存 → 立即返回缓存数据渲染 → 后台静默发起新请求;
  3. 请求完成
    • 新数据与缓存一致 → 无操作;
    • 新数据更新 → 替换缓存 → 触发视图刷新;
    • 请求失败 → 保留旧缓存,不影响当前视图(可提示错误)。

二、方案 1:使用官方 SWR 库(React 生态推荐)

官方 swr 库(https://swr.vercel.app/)封装了完整的 SWR 逻辑,支持缓存、重新验证、聚焦刷新、网络恢复刷新等,开箱即用。

1. 安装依赖

# npm
npm install swr
# yarn
yarn add swr
# pnpm
pnpm add swr

2. 基础使用(核心示例)

封装请求函数 + 使用 useSWR Hook,实现「先缓存后更新」:

import React from 'react';
import useSWR from 'swr';

// 1. 封装通用请求函数(适配 Fetch/AXIOS)
const fetcher = async (url) => {
  const res = await fetch(url);
  if (!res.ok) throw new Error('请求失败');
  return res.json();
};

// 2. 业务组件中使用 SWR
function UserList() {
  // 核心:useSWR(缓存键, 请求函数, 配置项)
  const { data, error, isLoading, mutate } = useSWR(
    '/api/users', // 缓存键:通常是请求 URL(唯一标识缓存)
    fetcher,
    {
      // 核心配置(按需调整)
      revalidateOnFocus: true, // 页面聚焦时重新验证(比如切回浏览器标签)
      revalidateOnReconnect: true, // 网络恢复时重新验证
      dedupingInterval: 5000, // 5s 内相同请求去重(避免重复请求)
      staleWhileRevalidate: 30000, // 缓存「新鲜期」30s,过期后仍可用但后台更新
      revalidateIfStale: true, // 缓存过期后自动重新验证
    }
  );

  // 加载状态(首次请求无缓存时)
  if (isLoading) return <div>加载中...</div>;
  // 错误处理(仅新请求失败,缓存仍保留)
  if (error) return <div>请求失败:{error.message}(展示缓存数据)</div>;

  return (
    <div>
      <h3>用户列表(SWR 缓存)</h3>
      <ul>
        {data?.map((user) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
      {/* 手动触发重新验证(比如点击刷新按钮) */}
      <button onClick={() => mutate()}>手动刷新数据</button>
    </div>
  );
}

export default UserList;

3. 核心配置说明(控制缓存行为)

配置项 作用
staleWhileRevalidate 缓存「新鲜期」(ms),过期后数据标记为「过期但可用」,后台自动更新
dedupingInterval 相同缓存键的请求去重时间(ms),避免短时间重复请求
revalidateOnFocus 页面聚焦时是否重新验证(比如从其他标签切回)
revalidateOnReconnect 网络从断开恢复时是否重新验证
revalidateIfStale 缓存过期后,是否自动后台重新验证
refreshInterval 定时轮询重新验证(ms),比如 5000 表示每 5s 刷新一次
fallbackData 初始回退数据(无缓存、无请求结果时展示)
persist 持久化缓存(需配合存储适配器,如 localStorage)

4. 持久化缓存(localStorage 保存,页面刷新不丢失)

默认 SWR 缓存仅在内存中,页面刷新后丢失,可通过 SWRConfig + 存储适配器实现持久化:

import React from 'react';
import { SWRConfig } from 'swr';
import localforage from 'localforage'; // 推荐用 localforage 处理存储(兼容多端)

// 1. 自定义存储适配器
const storage = {
  get: async (key) => {
    try {
      return await localforage.getItem(key);
    } catch (err) {
      return null;
    }
  },
  set: async (key, value) => {
    await localforage.setItem(key, value);
  },
  remove: async (key) => {
    await localforage.removeItem(key);
  },
};

// 2. 全局配置 SWR
function App() {
  return (
    <SWRConfig
      value={{
        fetcher: (url) => fetch(url).then((res) => res.json()),
        cache: storage, // 启用持久化缓存
        staleWhileRevalidate: 60 * 1000, // 1分钟新鲜期
        revalidateOnFocus: true,
      }}
    >
      <UserList />
    </SWRConfig>
  );
}

5. 高级用法:依赖请求(比如先查用户 ID,再查用户详情)

SWR 支持缓存键为 null 时暂停请求,适配依赖场景:

function UserDetail({ userId }) {
  // 缓存键为 null 时,不发起请求
  const { data: user } = useSWR(userId ? `/api/users/${userId}` : null, fetcher);

  if (!userId) return <div>请选择用户</div>;
  if (!user) return <div>加载中...</div>;

  return (
    <div>
      <h3>用户详情:{user.name}</h3>
      <p>邮箱:{user.email}</p>
    </div>
  );
}

三、方案 2:手动实现 SWR(无库依赖,理解核心逻辑)

若项目未使用 React 或不想引入第三方库,可手动封装 SWR 逻辑,核心是「缓存管理 + 异步重新验证」。

1. 核心设计

  • 缓存存储:内存缓存(Map)+ 可选 localStorage 持久化;
  • 状态管理:维护「缓存数据、加载状态、错误状态」;
  • 重新验证:支持页面聚焦、定时、手动触发;
  • 核心逻辑:先读缓存 → 异步请求 → 更新缓存 + 视图。

2. 完整实现(React Hook 版)

import React, { useState, useEffect, useRef } from 'react';

// 1. 全局缓存池(内存缓存)
const cache = new Map();
// 缓存元信息(记录过期时间)
const cacheMeta = new Map();

/**
 * 手动实现 SWR Hook
 * @param {string} key - 缓存键(请求URL)
 * @param {Function} fetcher - 请求函数
 * @param {Object} options - 配置项
 */
function useCustomSWR(key, fetcher, options = {}) {
  const {
    staleWhileRevalidate = 30000, // 30s 新鲜期
    revalidateOnFocus = true,
    refreshInterval = 0, // 定时刷新,0 表示关闭
  } = options;

  // 状态管理
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [isLoading, setIsLoading] = useState(false);
  // 避免重复请求的锁
  const isRevalidating = useRef(false);

  // 2. 从缓存读取数据(初始化/缓存更新时)
  useEffect(() => {
    if (!key) return;
    // 读取缓存
    const cachedData = cache.get(key);
    const cachedMeta = cacheMeta.get(key) || { expireTime: 0 };
    if (cachedData) {
      setData(cachedData);
      setError(null);
      setIsLoading(false);
    } else {
      // 无缓存,标记加载中
      setIsLoading(true);
    }
  }, [key]);

  // 3. 核心:重新验证函数(后台异步请求)
  const revalidate = async () => {
    if (!key || isRevalidating.current) return;
    isRevalidating.current = true;
    try {
      // 后台发起新请求
      const newData = await fetcher(key);
      // 更新缓存
      cache.set(key, newData);
      cacheMeta.set(key, {
        expireTime: Date.now() + staleWhileRevalidate,
      });
      // 更新视图
      setData(newData);
      setError(null);
    } catch (err) {
      setError(err);
      // 失败不覆盖缓存,保留旧数据
    } finally {
      setIsLoading(false);
      isRevalidating.current = false;
    }
  };

  // 4. 首次请求/缓存过期时触发重新验证
  useEffect(() => {
    if (!key) return;
    const cachedMeta = cacheMeta.get(key) || { expireTime: 0 };
    // 无缓存 或 缓存过期 → 触发验证
    if (!cache.has(key) || Date.now() > cachedMeta.expireTime) {
      revalidate();
    }
  }, [key]);

  // 5. 页面聚焦时重新验证
  useEffect(() => {
    if (!revalidateOnFocus || !key) return;
    const handleFocus = () => revalidate();
    window.addEventListener('focus', handleFocus);
    return () => window.removeEventListener('focus', handleFocus);
  }, [key, revalidateOnFocus]);

  // 6. 定时刷新
  useEffect(() => {
    if (!refreshInterval || !key) return;
    const timer = setInterval(() => revalidate(), refreshInterval);
    return () => clearInterval(timer);
  }, [key, refreshInterval]);

  // 7. 手动触发刷新的函数
  const mutate = () => revalidate();

  return { data, error, isLoading, mutate };
}

// ---------------------- 使用示例 ----------------------
function ProductList() {
  const fetcher = async (url) => {
    const res = await fetch(url);
    if (!res.ok) throw new Error('请求失败');
    return res.json();
  };

  const { data, error, isLoading, mutate } = useCustomSWR(
    '/api/products',
    fetcher,
    {
      staleWhileRevalidate: 60 * 1000, // 1分钟新鲜期
      revalidateOnFocus: true,
      refreshInterval: 10 * 1000, // 每10s刷新一次
    }
  );

  if (isLoading && !data) return <div>加载中...</div>;
  if (error) return <div>请求失败:{error.message}(展示缓存数据)</div>;

  return (
    <div>
      <h3>商品列表(自定义 SWR)</h3>
      <ul>
        {data?.map((item) => (
          <li key={item.id}>{item.name} - ¥{item.price}</li>
        ))}
      </ul>
      <button onClick={mutate}>手动刷新</button>
    </div>
  );
}

export default ProductList;

3. 非 React 场景适配(原生 JS)

若项目是原生 JS/ Vue / Angular,核心逻辑不变,只需替换状态管理方式:

// 原生 JS 版 SWR 工具函数
class SWR {
  constructor(fetcher, options = {}) {
    this.fetcher = fetcher;
    this.options = {
      staleWhileRevalidate: 30000,
      revalidateOnFocus: true,
      ...options,
    };
    this.cache = new Map();
    this.cacheMeta = new Map();
    this.listeners = new Map(); // 监听缓存更新的回调
  }

  // 读取数据(核心 SWR 逻辑)
  async get(key) {
    // 1. 读取缓存
    const cachedData = this.cache.get(key);
    const cachedMeta = this.cacheMeta.get(key) || { expireTime: 0 };
    const isStale = Date.now() > cachedMeta.expireTime;

    // 2. 触发监听回调(返回缓存数据)
    this.notify(key, {
      data: cachedData,
      isLoading: !cachedData,
      error: null,
    });

    // 3. 后台重新验证(缓存不存在/过期)
    if (!cachedData || isStale) {
      try {
        const newData = await this.fetcher(key);
        this.cache.set(key, newData);
        this.cacheMeta.set(key, {
          expireTime: Date.now() + this.options.staleWhileRevalidate,
        });
        // 新数据触发回调
        this.notify(key, { data: newData, isLoading: false, error: null });
      } catch (err) {
        this.notify(key, { data: cachedData, isLoading: false, error: err });
      }
    }

    return cachedData;
  }

  // 监听缓存更新
  on(key, callback) {
    if (!this.listeners.has(key)) {
      this.listeners.set(key, []);
    }
    this.listeners.get(key).push(callback);
  }

  // 触发监听回调
  notify(key, payload) {
    const callbacks = this.listeners.get(key) || [];
    callbacks.forEach((cb) => cb(payload));
  }

  // 手动刷新
  mutate(key) {
    return this.get(key);
  }
}

// ---------------------- 使用示例 ----------------------
// 1. 初始化 SWR 实例
const swr = new SWR(async (url) => {
  const res = await fetch(url);
  return res.json();
}, { staleWhileRevalidate: 60000 });

// 2. 监听数据更新并渲染
const renderProducts = (payload) => {
  const container = document.getElementById('product-container');
  if (payload.isLoading && !payload.data) {
    container.innerHTML = '<div>加载中...</div>';
    return;
  }
  if (payload.error) {
    container.innerHTML = `<div>请求失败:${payload.error.message}</div>`;
    return;
  }
  container.innerHTML = `
    <ul>
      ${payload.data.map(item => `<li>${item.name} - ¥${item.price}</li>`).join('')}
    </ul>
    <button id="refresh">手动刷新</button>
  `;
  // 绑定手动刷新事件
  document.getElementById('refresh').addEventListener('click', () => {
    swr.mutate('/api/products');
  });
};

// 3. 发起请求(先缓存后更新)
swr.on('/api/products', renderProducts);
swr.get('/api/products');

四、关键注意事项

1. 缓存键设计

  • 缓存键需唯一标识请求,建议用「URL + 参数序列化」,比如:
    // 带参数的请求,缓存键需包含参数
    const key = `/api/users?page=${page}&size=${size}`;
    // 复杂参数可序列化
    const key = `/api/users?${JSON.stringify({ page, size })}`;

2. 错误处理原则

  • 重新验证失败时,绝对不覆盖旧缓存,避免用户看到空白/错误页面;
  • 可增加「错误重试」逻辑,比如失败后 3s 自动重试:
    const revalidate = async () => {
      if (!key || isRevalidating.current) return;
      isRevalidating.current = true;
      let retryCount = 0;
      while (retryCount < 3) { // 最多重试3次
        try {
          const newData = await fetcher(key);
          cache.set(key, newData);
          setData(newData);
          setError(null);
          break; // 成功则退出重试
        } catch (err) {
          retryCount++;
          if (retryCount >= 3) setError(err);
          await new Promise(resolve => setTimeout(resolve, 3000)); // 3s 重试
        }
      }
      isRevalidating.current = false;
    };

3. 性能优化

  • 请求去重:避免同一缓存键同时发起多个请求(用 isRevalidating 锁);
  • 缓存清理:限制缓存池大小,避免内存溢出(比如最多缓存 100 条,超出则删除最久未使用的);
    // 缓存清理逻辑(LRU 策略)
    const MAX_CACHE_SIZE = 100;
    if (cache.size > MAX_CACHE_SIZE) {
      const oldestKey = Array.from(cache.keys()).shift();
      cache.delete(oldestKey);
      cacheMeta.delete(oldestKey);
    }
  • 防抖/节流:对高频触发的重新验证(比如搜索框)加节流:
    import { throttle } from 'lodash';
    const revalidateThrottled = throttle(revalidate, 1000); // 1s 内仅触发一次

4. 适用场景

  • ✅ 高频访问的静态/准静态数据(如商品列表、用户信息);
  • ✅ 对实时性要求不高,但要求体验流畅的场景;
  • ❌ 强实时性场景(如秒杀倒计时、聊天消息)→ 建议用 WebSocket;
  • ❌ 一次性请求(如表单提交)→ 无需缓存。

五、总结

  • 推荐方案:React 项目直接用官方 swr 库,开箱即用,支持丰富的配置和生态;
  • 手动实现:非 React 项目或需定制化场景,核心是「缓存池 + 异步重新验证 + 状态通知」;
  • 核心原则:缓存优先,后台更新,失败不兜底,既保证体验又保证数据一致性。

通过以上实现,前端可完整落地 SWR 策略,达到「先展示缓存,后台静默更新」的核心效果。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions