Bootstrap

React Native 全栈开发实战班 - 网络与数据之数据缓存策略SWR、Query

在移动应用中,数据缓存 是提升应用性能和用户体验的重要手段。通过缓存数据,可以减少网络请求次数,降低延迟,提高应用在离线或网络不稳定情况下的可用性。React Native 提供了多种数据缓存策略,包括内存缓存、异步存储(AsyncStorage)以及第三方缓存库。本章节将详细介绍常见的数据缓存策略、实现方式以及最佳实践。


2.1 数据缓存概述

数据缓存 的目标是将从服务器获取的数据存储在本地,以便在后续请求中快速访问,而无需每次都进行网络请求。缓存策略的选择取决于数据的类型、使用频率、更新频率以及应用的具体需求。

常见的缓存策略包括:

  1. 内存缓存: 将数据存储在内存中,访问速度快,但生命周期与组件生命周期相关。
  2. 异步存储(AsyncStorage): 将数据持久化存储在设备上,适用于需要长期保存的数据。
  3. 第三方缓存库:react-query, SWR 等,提供更强大的缓存管理和数据同步功能。

2.2 内存缓存

内存缓存 是将数据存储在内存中,访问速度快,适用于生命周期较短的数据。

2.2.1 使用 React 的 useStateuseEffect

可以通过 React 的 useStateuseEffect Hook 实现简单的内存缓存。

示例:

// components/MemoryCacheExample.js
import React, { useState, useEffect } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';

const MemoryCacheExample = () => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);

  const fetchData = async () => {
    setLoading(true);
    // 模拟网络请求
    await new Promise((resolve) => setTimeout(resolve, 1000));
    const fetchedData = { id: 1, title: 'Hello, World!' };
    setData(fetchedData);
    setLoading(false);
  };

  useEffect(() => {
    if (!data) {
      fetchData();
    }
  }, []);

  return (
    <View style={styles.container}>
      {loading ? (
        <Text>Loading...</Text>
      ) : data ? (
        <Text style={styles.text}>{data.title}</Text>
      ) : null}
      <Button title="Refresh" onPress={fetchData} />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  text: {
    fontSize: 18,
    marginBottom: 10,
  },
});

export default MemoryCacheExample;

解释:

  • 数据存储在 data 状态中。
  • 组件挂载时,如果 data 为空,则发起网络请求。
  • 数据被存储在内存中,刷新页面时不会重新发起网络请求。
2.2.2 使用 React Context API

可以通过 Context API 实现全局内存缓存。

示例:

// context/DataContext.js
import React, { createContext, useState, useEffect } from 'react';

export const DataContext = createContext();

export const DataProvider = ({ children }) => {
  const [data, setData] = useState(null);

  useEffect(() => {
    // 模拟数据获取
    const fetchData = async () => {
      await new Promise((resolve) => setTimeout(resolve, 1000));
      const fetchedData = { id: 1, title: 'Hello, Context!' };
      setData(fetchedData);
    };
    fetchData();
  }, []);

  return <DataContext.Provider value={data}>{children}</DataContext.Provider>;
};
// components/MemoryCacheWithContext.js
import React, { useContext } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import { DataContext, DataProvider } from '../context/DataContext';

const MemoryCacheWithContext = () => {
  const data = useContext(DataContext);

  const refreshData = () => {
    // 重新获取数据
    // 这里简单模拟,实际应用中需要更新 Context 的值
    alert('Refresh Data');
  };

  return (
    <View style={styles.container}>
      {data ? <Text style={styles.text}>{data.title}</Text> : <Text>Loading...</Text>}
      <Button title="Refresh" onPress={refreshData} />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  text: {
    fontSize: 18,
    marginBottom: 10,
  },
});

export default MemoryCacheWithContext;

解释:

  • DataContext 提供全局数据存储。
  • 数据被存储在 Context 中,所有订阅的组件都可以访问到。

2.3 使用 AsyncStorage 进行持久化缓存

AsyncStorage 是 React Native 提供的一个简单的异步键值对存储系统,适用于持久化存储数据。

2.3.1 安装 AsyncStorage

对于 React Native 0.59 及以上版本,AsyncStorage 已经内置,无需额外安装。

对于 React Native 0.58 及以下版本,需要手动安装:

npm install @react-native-async-storage/async-storage
2.3.2 基本用法

存储数据:

import AsyncStorage from '@react-native-async-storage/async-storage';

const storeData = async (key, value) => {
  try {
    const jsonValue = JSON.stringify(value);
    await AsyncStorage.setItem(key, jsonValue);
  } catch (e) {
    console.error(e);
  }
};

获取数据:

const getData = async (key) => {
  try {
    const jsonValue = await AsyncStorage.getItem(key);
    return jsonValue != null ? JSON.parse(jsonValue) : null;
  } catch (e) {
    console.error(e);
    return null;
  }
};

示例:

// components/AsyncStorageCacheExample.js
import React, { useState, useEffect } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';

const AsyncStorageCacheExample = () => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);

  const fetchData = async () => {
    setLoading(true);
    // 模拟网络请求
    await new Promise((resolve) => setTimeout(resolve, 1000));
    const fetchedData = { id: 1, title: 'Hello, AsyncStorage!' };
    setData(fetchedData);
    await AsyncStorage.setItem('data_key', JSON.stringify(fetchedData));
    setLoading(false);
  };

  useEffect(() => {
    const loadData = async () => {
      const storedData = await AsyncStorage.getItem('data_key');
      if (storedData) {
        setData(JSON.parse(storedData));
      } else {
        fetchData();
      }
    };
    loadData();
  }, []);

  return (
    <View style={styles.container}>
      {loading ? (
        <Text>Loading...</Text>
      ) : data ? (
        <Text style={styles.text}>{data.title}</Text>
      ) : null}
      <Button title="Refresh" onPress={fetchData} />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  text: {
    fontSize: 18,
    marginBottom: 10,
  },
});

export default AsyncStorageCacheExample;

解释:

  • 数据被存储在 data_key 键下。
  • 组件挂载时,先从 AsyncStorage 中获取数据,如果不存在,则发起网络请求并存储数据。
2.3.3 注意事项
  • 数据大小限制: AsyncStorage 适用于存储少量数据,不适合存储大量数据。
  • 性能: AsyncStorage 的性能不如内存缓存,不适合频繁读写操作。
  • 安全性: AsyncStorage 不提供加密功能,敏感数据需要谨慎处理。

2.4 使用第三方缓存库

第三方缓存库提供了更强大的缓存管理和数据同步功能,适用于复杂的数据缓存需求。

2.4.1 react-query

react-query 是一个用于数据获取和缓存的库,支持自动重新获取数据、缓存失效、乐观更新等功能。

安装:

npm install @tanstack/react-query

示例:

// components/ReactQueryExample.js
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';

const queryClient = new QueryClient();

const fetchData = async () => {
  const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
  return response.json();
};

const MyQueryComponent = () => {
  const { data, error, isLoading } = useQuery(['post'], fetchData, {
    staleTime: 5 * 60 * 1000, // 数据缓存时间
  });

  if (isLoading) return <Text>Loading...</Text>;
  if (error) return <Text>Error: {error.message}</Text>;

  return <Text style={styles.text}>{data.title}</Text>;
};

const ReactQueryExample = () => {
  return (
    <QueryClientProvider client={queryClient}>
      <View style={styles.container}>
        <MyQueryComponent />
      </View>
    </QueryClientProvider>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  text: {
    fontSize: 18,
    marginBottom: 10,
  },
});

export default ReactQueryExample;

解释:

  • useQuery 钩子用于发起数据请求,并自动管理缓存。
  • staleTime 定义数据缓存时间,超过时间后数据会被标记为过期,下次请求时会重新获取。
2.4.1 SWR 简介

SWR(Stale-While-Revalidate)是一个由 Vercel 开发的 React Hooks 库,用于数据获取和缓存。SWR 的核心思想是:在请求数据时,首先返回缓存中的旧数据(stale),然后在后台重新获取最新数据(revalidate),并将最新数据更新到缓存中。这种方式可以有效减少请求延迟,提高用户体验。

SWR 的主要特点:

  • 快速响应: 立即返回缓存数据,同时在后台重新获取最新数据。
  • 自动重新获取: 在组件重新挂载或窗口重新聚焦时,自动重新获取数据。
  • 错误重试: 在请求失败时,自动重试请求。
  • 乐观更新: 在数据提交时,先更新本地缓存,再提交到服务器。
  • 灵活的缓存策略: 支持自定义缓存逻辑和缓存失效时间。

安装 SWR:

npm install swr

2.4.2 基本用法

以下是一个使用 SWR 进行数据获取和缓存的示例。

示例:

// components/SWRExample.js
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import useSWR from 'swr';

const fetcher = async (url) => {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error('Network response was not ok');
  }
  return response.json();
};

const SWRExample = () => {
  const { data, error, isValidating } = useSWR('https://jsonplaceholder.typicode.com/posts/1', fetcher);

  if (error) return <Text style={styles.text}>Error: {error.message}</Text>;
  if (isValidating) return <Text style={styles.text}>Loading...</Text>;

  return (
    <View style={styles.container}>
      <Text style={styles.text}>Title: {data.title}</Text>
      <Text style={styles.text}>Body: {data.body}</Text>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
  },
  text: {
    fontSize: 16,
  },
});

export default SWRExample;

解释:

  • useSWR Hook:

    • 第一个参数是数据的唯一键(key),通常是请求的 URL。
    • 第二个参数是数据获取函数(fetcher),用于发起网络请求。
    • 返回一个对象,包含 data, error, isValidating 等属性。
  • 数据获取流程:

    • 组件挂载时,useSWR 会检查缓存中是否存在数据。
    • 如果有缓存数据,立即返回缓存数据,同时在后台重新获取最新数据。
    • 如果没有缓存数据,则发起网络请求。
  • 错误处理:

    • 如果请求失败,error 属性会包含错误信息。
  • 加载状态:

    • isValidating 属性表示数据是否正在重新获取。

2.4.3 缓存失效与重新获取

SWR 提供了多种方式来控制缓存失效和重新获取数据。

2.4.3.1 revalidateOnFocus

默认情况下,SWR 会在窗口重新聚焦时重新获取数据。可以通过 revalidateOnFocus 选项关闭此功能。

示例:

const { data, error, isValidating } = useSWR('https://jsonplaceholder.typicode.com/posts/1', fetcher, {
  revalidateOnFocus: false,
});
2.4.3.2 revalidateOnReconnect

默认情况下,SWR 会在网络重新连接时重新获取数据。可以通过 revalidateOnReconnect 选项关闭此功能。

示例:

const { data, error, isValidating } = useSWR('https://jsonplaceholder.typicode.com/posts/1', fetcher, {
  revalidateOnReconnect: false,
});
2.4.3.3 refreshInterval

可以通过 refreshInterval 选项设置定时重新获取数据的间隔时间(毫秒)。

示例:

const { data, error, isValidating } = useSWR('https://jsonplaceholder.typicode.com/posts/1', fetcher, {
  refreshInterval: 5000, // 每5秒重新获取一次数据
});
2.4.3.4 dedupingInterval

可以通过 dedupingInterval 选项设置请求去重的时间间隔(毫秒),避免在短时间内重复请求相同的数据。

示例:

const { data, error, isValidating } = useSWR('https://jsonplaceholder.typicode.com/posts/1', fetcher, {
  dedupingInterval: 1000, // 1秒内重复请求相同的数据会被去重
});

2.4.4 错误重试

SWR 支持自动重试失败的请求,可以通过 errorRetryCounterrorRetryInterval 选项控制重试次数和重试间隔。

示例:

const { data, error, isValidating } = useSWR('https://jsonplaceholder.typicode.com/posts/1', fetcher, {
  errorRetryCount: 3, // 最大重试次数
  errorRetryInterval: 5000, // 重试间隔时间
});

2.4.5 乐观更新

SWR 支持乐观更新,即在数据提交时,先更新本地缓存,再提交到服务器。

示例:

import React from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import useSWR, { mutate } from 'swr';

const fetcher = async (url) => {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error('Network response was not ok');
  }
  return response.json();
};

const OptimisticUpdateExample = () => {
  const { data, error, isValidating } = useSWR('https://jsonplaceholder.typicode.com/posts/1', fetcher);

  const updateTitle = () => {
    mutate('https://jsonplaceholder.typicode.com/posts/1', {
      ...data,
      title: 'Updated Title',
    }, false); // 先更新缓存

    fetch('https://jsonplaceholder.typicode.com/posts/1', {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        ...data,
        title: 'Updated Title',
      }),
    })
      .then((response) => response.json())
      .then((json) => {
        mutate('https://jsonplaceholder.typicode.com/posts/1'); // 重新获取最新数据
      })
      .catch((error) => {
        console.error(error);
        mutate('https://jsonplaceholder.typicode.com/posts/1', data, false); // 回滚缓存
      });
  };

  if (error) return <Text style={styles.text}>Error: {error.message}</Text>;
  if (isValidating) return <Text style={styles.text}>Loading...</Text>;

  return (
    <View style={styles.container}>
      <Text style={styles.text}>Title: {data.title}</Text>
      <Button title="Update Title" onPress={updateTitle} />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
  },
  text: {
    fontSize: 16,
  },
});

export default OptimisticUpdateExample;

解释:

  • mutate 函数用于更新缓存数据。
  • 在数据提交前,先调用 mutate 更新缓存。
  • 然后发起网络请求,如果请求成功,则重新获取最新数据。
  • 如果请求失败,则回滚缓存数据。

2.4.6 总结

SWR 是一个功能强大的数据获取和缓存库,适用于需要高效数据管理和缓存的应用。通过使用 SWR,可以简化数据获取逻辑,提高应用性能和用户体验。

作者简介

前腾讯电子签的前端负责人,现 whentimes tech CTO,专注于前端技术的大咖一枚!一路走来,从小屏到大屏,从 Web 到移动,什么前端难题都见过。热衷于用技术打磨产品,带领团队把复杂的事情做到极简,体验做到极致。喜欢探索新技术,也爱分享一些实战经验,帮助大家少走弯路!

温馨提示:可搜老码小张公号联系导师

;