Chapter 05

网络与数据持久化

Axios 请求拦截、React Query 缓存策略、MMKV 高性能本地存储,以及离线优先的设计思路。

RN 网络请求与 Web 的差异

React Native 内置了 fetch API(与浏览器 fetch 相同),也支持 Axios。与 Web 开发最大的不同是:没有 CORS 限制。原生 App 发出的网络请求不受同源策略约束,这既是方便之处,也意味着你需要在服务端做好权限验证。

另一个差异是证书验证。RN 会严格验证 HTTPS 证书,自签名证书需要额外配置才能在真机上使用。开发环境推荐用 mkcert 生成受信任的本地证书。

还有网络状态问题:手机用户经常在 WiFi 和移动网络之间切换,甚至进入无信号区域。一个好的移动应用必须优雅处理网络中断场景,这是 Web 开发较少面对的挑战。

核心名词解释

fetch
RN 内置的网络请求 API,与浏览器 fetch 兼容。适合简单请求,复杂项目推荐 Axios(更好的拦截器、错误处理、TypeScript 支持)。
Axios
功能丰富的 HTTP 客户端库,支持请求/响应拦截器、自动 JSON 转换、超时设置、取消请求。配合 React Query 使用是生产项目的标准组合。
React Query
服务端状态管理,提供智能缓存(避免重复请求)、后台刷新(Stale-While-Revalidate)、乐观更新、请求去重等功能。
AsyncStorage
React Native 官方异步键值存储,基于文件系统。数据以 JSON 字符串形式存储,存取都是异步操作。适合存储用户偏好、轻量数据,不适合存储大量数据。
MMKV
微信团队开源的高性能键值存储,基于内存映射文件(mmap)实现,读写速度比 AsyncStorage 快 30 倍,且支持同步操作。推荐作为 AsyncStorage 的替代品。
离线优先(Offline First)
设计模式:先展示本地缓存数据,后台尝试更新;写入操作在网络断开时排队,恢复后自动同步。让应用在无网络时依然可用。
Optimistic Update(乐观更新)
操作后立即更新 UI(假设成功),同时发送网络请求。若请求失败则回滚 UI 并提示用户。点赞、关注等操作的标准实现方式。

请求链路架构

完整请求链路(组件 → React Query → Axios → API) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ UI 组件 │ │ useQuery({ queryKey: ['posts'], queryFn: ... }) ▼ React Query Cache │ Cache Hit? ──Yes──▶ 立即返回缓存数据(stale) │ 同时在后台发起请求(revalidate) │ Cache Miss? ▼ QueryFn(你定义的函数) │ api.getPosts() ▼ Axios Instance │ 请求拦截器:自动附加 Authorization: Bearer │ 请求拦截器:添加 Accept-Language、设备信息 Header ▼ API Server │ 响应 200: data │ 响应 401: 刷新 Token 并重试 │ 响应 500: 触发 retry(最多2次) ▼ Axios 响应拦截器 │ 统一解包 data.data │ 统一处理错误 Toast ▼ React Query Cache 更新 │ 通知所有订阅该 queryKey 的组件重渲染 ▼ UI 组件收到最新数据

Axios 封装

// lib/api.ts — Axios 实例封装
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
import { useAuthStore } from '../store/authStore';
import { MMKV } from 'react-native-mmkv';

export const storage = new MMKV();

export const api = axios.create({
  baseURL: 'https://api.myapp.com/v1',
  timeout: 10000,   // 10秒超时
  headers: { 'Content-Type': 'application/json' },
});

// 请求拦截器:自动附加 Token
api.interceptors.request.use((config: InternalAxiosRequestConfig) => {
  const token = useAuthStore.getState().token;
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// 响应拦截器:统一错误处理 + Token 刷新
api.interceptors.response.use(
  response => response.data, // 自动解包 data
  async (error: AxiosError) => {
    const status = error.response?.status;

    if (status === 401) {
      // Token 过期,尝试刷新
      try {
        const refreshToken = storage.getString('refreshToken');
        const { token } = await api.post('/auth/refresh', { refreshToken });
        useAuthStore.getState().setToken(token);
        // 重试原始请求
        return api(error.config!);
      } catch {
        useAuthStore.getState().logout();
      }
    }

    return Promise.reject(error);
  }
);

AsyncStorage vs MMKV

AsyncStorage 是 RN 官方的本地存储方案,但它有明显的性能缺陷:所有操作都是异步的,且存储格式为纯文本 JSON,大数据量时解析开销显著。

MMKV 是微信团队开源的高性能方案,已被 React Native 社区广泛采用。它基于内存映射文件(mmap),读写都在内存中完成,速度极快,且支持同步操作(不需要 async/await)。

// AsyncStorage — 异步,慢
import AsyncStorage from '@react-native-async-storage/async-storage';

await AsyncStorage.setItem('theme', 'dark');
const theme = await AsyncStorage.getItem('theme');

// MMKV — 同步,快 30x
import { MMKV } from 'react-native-mmkv';
const storage = new MMKV();

storage.set('theme', 'dark');              // 同步!无需 await
const theme = storage.getString('theme'); // 同步!
storage.set('count', 42);                  // 支持 number 直接存储
const count = storage.getNumber('count'); // 100

// 多实例(不同命名空间)
const userStorage = new MMKV({ id: 'user' });
const cacheStorage = new MMKV({ id: 'cache' });

图片缓存与离线优先

// expo-image:官方推荐,内置磁盘/内存缓存
import { Image } from 'expo-image';

const blurhash = '|rF?hV%2WCj[ayj[a|j[az_NaeWBj@ayfRayfQfQM{M|azj[azf6fQfQIpWXofj[ayj[j[fQayWCoeoeaya}j[ayfQa{oLj?j[WVj[ayayj[fQoff7azayj[ayj[j[ayofayayayj[';

export function CachedImage({ uri }: { uri: string }) {
  return (
    <Image
      source={{ uri }}
      style={{ width: 300, height: 200 }}
      placeholder={blurhash}             // 模糊占位(加载前显示)
      contentFit="cover"
      transition={300}                   // 淡入过渡动画
      cachePolicy="memory-disk"         // 内存+磁盘双缓存
    />
  );
}
避免的反模式 不要在 useEffect 里直接写 fetch 请求——这种方式没有缓存、没有错误重试、没有加载状态管理,是 React 生态中出了名的"初学者陷阱"。所有服务端数据获取都应该通过 React Query 的 useQuery 进行。
网络状态监听 使用 @react-native-community/netinfo 监听网络状态变化,在断网时禁用提交按钮、显示离线提示;网络恢复时自动触发 React Query 的 refetch(React Query 内置了此功能,配置 refetchOnReconnect: true 即可)。