JohnieXu's Blog

Back

Lynx 移动端 HTTP 请求方案对比:TanStack Query vs Axios#

前言#

在 Lynx(字节跳动的跨端框架)项目中进行网络请求时,开发者通常面临两个主要选择:原生 Fetch API 配合 TanStack Query,或者使用 Axios 进行封装。本文将深入对比这两种方案,帮助你在项目中做出合适的技术选型。

方案一:TanStack Query + Fetch#

为什么推荐 TanStack Query?#

TanStack Query 是 React 生态系统中最流行的服务端状态管理库,它不仅仅是一个 HTTP 客户端,更是数据获取和缓存管理的完整解决方案。官方文档明确推荐在 ReactLynx 中使用 TanStack Query。

核心特性#

特性说明
自动缓存开箱即用的请求缓存和自动重新获取
乐观更新支持 mutations 的乐观更新模式
后台刷新窗口聚焦时自动刷新 stale 数据
依赖查询支持查询之间的依赖关系
分页 & 无限滚动内置支持分页和无限滚动场景

基础用法#

import { QueryClient, QueryClientProvider, useQuery } from "@tanstack/react-query";

const queryClient = new QueryClient();

// 封装请求函数
interface Post {
  userId: number;
  id: number;
  title: string;
  body: string;
}

const fetchPosts = async (): Promise<Post[]> => {
  const response = await fetch("https://jsonplaceholder.typicode.com/posts");
  if (!response.ok) {
    throw new Error("Failed to fetch posts");
  }
  return response.json();
};

// 在组件中使用
function PostList() {
  const { data, isLoading, isError, error, refetch } = useQuery({
    queryKey: ["posts"],
    queryFn: fetchPosts,
    staleTime: 5 * 60 * 1000, // 5分钟内不重新获取
  });

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

  return (
    <scroll-view scroll-y>
      {data?.map((post) => (
        <view key={post.id}>
          <text>{post.title}</text>
        </view>
      ))}
    </scroll-view>
  );
}

// 完整应用包装
root.render(
  <QueryClientProvider client={queryClient}>
    <PostList />
  </QueryClientProvider>
);
typescript

高级用法:Mutation 与乐观更新#

const useDeletePost = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (postId: number) => {
      const response = await fetch(
        `https://jsonplaceholder.typicode.com/posts/${postId}`,
        { method: "DELETE" }
      );
      if (!response.ok) throw new Error("Failed to delete");
      return postId;
    },

    // 乐观更新:在请求完成前就更新 UI
    onMutate: async (postId) => {
      // 取消任何现有的刷新操作
      await queryClient.cancelQueries({ queryKey: ["posts"] });

      // 快照当前数据用于回滚
      const previousPosts = queryClient.getQueryData<Post[]>(["posts"]);

      // 立即更新缓存
      queryClient.setQueryData<Post[]>(["posts"], (old) =>
        old ? old.filter((p) => p.id !== postId) : []
      );

      return { previousPosts };
    },

    // 失败时回滚
    onError: (err, postId, context) => {
      if (context?.previousPosts) {
        queryClient.setQueryData(["posts"], context.previousPosts);
      }
    },

    // 请求完成后刷新数据
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ["posts"] });
    },
  });
};

// 组件中使用
function DeleteButton({ postId }) {
  const deletePost = useDeletePost();

  return (
    <view bindtap={() => deletePost.mutate(postId)}>
      <text>Delete</text>
    </view>
  );
}
typescript

分页查询#

const usePostsPage = (page: number) => {
  return useQuery({
    queryKey: ["posts", page],
    queryFn: async () => {
      const response = await fetch(
        `https://jsonplaceholder.typicode.com/posts?_page=${page}&_limit=10`
      );
      return response.json();
    },
    keepPreviousData: true, // 保持前一页数据直到新数据加载完成
  });
};

// 使用
function PostPage({ pageNum }) {
  const { data, isFetching } = usePostsPage(pageNum);
  return (
    // ...
  );
}
typescript

不足之处#

  1. 包体积:作为完整的解决方案,库体积相对较大
  2. 概念门槛:需要理解缓存、失效、重置等核心概念
  3. 移动端适配:某些 web 生态的库可能需要适配 Lynx 环境

方案二:Axios 封装#

为什么考虑 Axios?#

Axios 是历史悠久的 HTTP 客户端库,API 设计直观,提供请求拦截器、取消请求、自动转换等功能。如果你已经有现成的 Axios 使用经验,或者需要更精细的 HTTP 控制,Axios 是一个合理的选择。

核心特性#

特性说明
请求/响应拦截器统一的请求前后处理
自动 JSON 转换自动序列化请求体和解析响应
请求取消使用 CancelToken 取消请求
错误处理统一的错误处理结构
浏览器兼容良好的跨环境兼容性

基础封装#

import axios, { AxiosInstance, AxiosRequestConfig, AxiosError } from "axios";

// 创建实例
const createApiClient = (baseURL: string): AxiosInstance => {
  const client = axios.create({
    baseURL,
    timeout: 10000,
    headers: {
      "Content-Type": "application/json",
    },
  });

  // 请求拦截器
  client.interceptors.request.use(
    (config) => {
      // 添加 token
      const token = getAuthToken();
      if (token) {
        config.headers.Authorization = `Bearer ${token}`;
      }
      // 请求日志
      console.log(`[Request] ${config.method?.toUpperCase()} ${config.url}`);
      return config;
    },
    (error) => Promise.reject(error)
  );

  // 响应拦截器
  client.interceptors.response.use(
    (response) => {
      console.log(`[Response] ${response.status} ${response.config.url}`);
      return response.data;
    },
    (error: AxiosError) => {
      // 统一错误处理
      if (error.response) {
        switch (error.response.status) {
          case 401:
            handleUnauthorized();
            break;
          case 403:
            handleForbidden();
            break;
          case 404:
            handleNotFound();
            break;
          case 500:
            handleServerError();
            break;
        }
      } else if (error.request) {
        // 请求已发出但没有收到响应
        console.error("Network Error:", error.message);
      }
      return Promise.reject(error);
    }
  );

  return client;
};

// 使用工厂函数
const api = createApiClient("https://api.example.com");

// 封装通用请求方法
export const http = {
  get: <T>(url: string, config?: AxiosRequestConfig) =>
    api.get<T>(url, config),

  post: <T>(url: string, data?: unknown, config?: AxiosRequestConfig) =>
    api.post<T>(url, data, config),

  put: <T>(url: string, data?: unknown, config?: AxiosRequestConfig) =>
    api.put<T>(url, data, config),

  delete: <T>(url: string, config?: AxiosRequestConfig) =>
    api.delete<T>(url, config),

  patch: <T>(url: string, data?: unknown, config?: AxiosRequestConfig) =>
    api.patch<T>(url, data, config),
};
typescript

请求函数封装#

// types.ts
interface ApiResponse<T> {
  data: T;
  code: number;
  message: string;
}

interface User {
  id: number;
  name: string;
  email: string;
}

// userApi.ts
export const userApi = {
  list: () => http.get<ApiResponse<User[]>>("/users"),

  getById: (id: number) => http.get<ApiResponse<User>>(`/users/${id}`),

  create: (user: Omit<User, "id">) =>
    http.post<ApiResponse<User>>("/users", user),

  update: (id: number, user: Partial<User>) =>
    http.put<ApiResponse<User>>(`/users/${id}`, user),

  delete: (id: number) => http.delete<ApiResponse<void>>(`/users/${id}`),
};

// 组件中使用
function UserList() {
  const [users, setUsers] = useState<User[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    userApi
      .list()
      .then((res) => {
        if (res.code === 0) {
          setUsers(res.data);
        } else {
          setError(res.message);
        }
      })
      .catch((err) => setError(err.message))
      .finally(() => setLoading(false));
  }, []);

  // ...
}
typescript

请求取消#

// 创建取消令牌
const controller = new AbortController();

// 发送可取消的请求
const fetchUser = async () => {
  try {
    const response = await axios.get("/api/user/1", {
      signal: controller.signal,
    });
    return response.data;
  } catch (error) {
    if (axios.isCancel(error)) {
      console.log("Request was cancelled");
    }
    throw error;
  }
};

// 取消请求
const cancelRequest = () => {
  controller.abort();
};
typescript

与 TanStack Query 结合#

如果你既想用 Axios 的便利,又想享受 TanStack Query 的缓存管理,可以结合使用:

import { useQuery } from "@tanstack/react-query";
import axios from "axios";

// 封装 Axios 为 queryFn
const fetchWithAxios = async <T>(url: string): Promise<T> => {
  const response = await axios.get<T>(url);
  return response.data;
};

// 组件中使用
function PostList() {
  const { data, isLoading } = useQuery({
    queryKey: ["posts"],
    queryFn: () => fetchWithAxios<Post[]>("https://jsonplaceholder.typicode.com/posts"),
  });

  // ...
}
typescript

对比总结#

功能对比#

功能TanStack QueryAxios
HTTP 请求❌ 需配合 fetch/axios✅ 原生支持
缓存管理✅ 自动缓存❌ 需自行实现
乐观更新✅ 内置支持❌ 需自行实现
后台刷新✅ 支持❌ 需自行实现
请求拦截❌ 需配合 fetch✅ 原生支持
取消请求✅ 内置✅ 原生
包体积~14KB (gzipped)~14KB (gzipped)
学习曲线较陡平缓

使用场景建议#

推荐使用 TanStack Query + Fetch:

  • 复杂的数据获取场景(分页、无限滚动)
  • 需要乐观更新提升用户体验
  • 多处重复获取相同数据的场景
  • 需要缓存和自动刷新功能

推荐使用 Axios:

  • 已经大量使用 Axios 的存量项目迁移
  • 需要细粒度的 HTTP 控制
  • 请求/响应需要特殊处理(加密、压缩等)
  • 团队对 Axios 熟悉度高

混合方案#

实际上,最佳实践可能是结合两者:

// 统一封装
import { QueryClient } from "@tanstack/react-query";
import axios from "axios";

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000,
      retry: 2,
    },
  },
});

// Axios 实例
const http = axios.create({
  baseURL: "/api",
  timeout: 10000,
});

// 封装查询函数
const queryFn = async <T>(url: string): Promise<T> => {
  const response = await http.get<T>(url);
  return response.data;
};

// 使用
const { data } = useQuery({
  queryKey: ["posts"],
  queryFn: () => queryFn("/posts"),
});
typescript

Lynx Fetch API 与 Web Fetch 的兼容性差异#

在 Web 生态中,Axios 等库对 XMLHttpRequestfetch 进行了大量兼容性处理,以弥合不同浏览器和环境之间的差异。Lynx 只支持 fetch API,这意味着许多 Axios 在 Web 端做的工作在 Lynx 环境中需要手动处理或依赖其他方式。

Axios 在 Web 端做的兼容性处理(Lynx 中需自行处理的部分)#

功能Web Axios 处理方式Lynx Fetch 现状
JSON 自动转换请求时自动 JSON.stringify,响应时自动 response.json()需手动调用 .json()
超时控制axios.timeout 配置需使用 AbortController 自行实现
请求取消CancelToken 机制原生支持 AbortController
HTTP Basic Auth自动处理 Authorization需手动设置
XSRF Token自动从 cookie 读取并添加到 header需自行实现
请求进度支持 onUploadProgress不支持
响应进度支持 onDownloadProgress不支持
FormData 上传自动处理 multipart/form-data需手动构建
URL 编码自动处理 application/x-www-form-urlencoded需手动处理
重试机制需插件支持需自行实现
请求拦截器原生支持需封装或使用库
响应拦截器原生支持需封装或使用库
错误类型区分区分网络错误、HTTP 错误等需自行判断

详细差异说明#

1. JSON 自动转换#

Web Axios(自动处理):

// Axios 自动序列化,自动解析
axios.post('/api/user', { name: 'test' })
  .then(res => console.log(res.data)); // 直接得到对象
typescript

Lynx Fetch(需手动处理):

// 需手动 JSON.stringify
fetch('/api/user', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'test' })
}).then(res => res.json()) // 手动调用 .json()
  .then(data => console.log(data));
typescript

2. 超时控制#

Web Axios:

axios.get('/api/user', { timeout: 5000 });
typescript

Lynx Fetch:

const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);

fetch('/api/user', { signal: controller.signal })
  .catch(err => {
    if (err.name === 'AbortError') {
      console.log('Request timeout');
    }
  });
typescript

3. 请求/响应拦截器#

Web Axios(原生支持):

axios.interceptors.request.use(config => {
  config.headers['Authorization'] = 'Bearer token';
  return config;
});

axios.interceptors.response.use(
  response => response.data, // 转换响应
  error => { /* 统一错误处理 */ }
);
typescript

Lynx Fetch(封装方案):

// 封装 fetch 拦截器
class FetchClient {
  private interceptors = {
    request: [],
    response: []
  };

  async request(url, options = {}) {
    // 请求拦截
    let config = { ...options };
    for (const fn of this.interceptors.request) {
      config = await fn(config);
    }

    const response = await fetch(url, {
      ...config,
      headers: {
        'Content-Type': 'application/json',
        ...config.headers
      }
    });

    // 响应拦截
    let data = await response.json();
    for (const fn of this.interceptors.response) {
      data = await fn(data, response);
    }

    return data;
  }

  // 添加拦截器方法
  addRequestInterceptor(fn) {
    this.interceptors.request.push(fn);
  }

  addResponseInterceptor(fn) {
    this.interceptors.response.push(fn);
  }
}
typescript

4. FormData 上传#

Web Axios(自动处理):

const formData = new FormData();
formData.append('file', file);
formData.append('name', 'test');

axios.post('/api/upload', formData); // 自动设置 Content-Type
typescript

Lynx Fetch(需手动处理):

const formData = new FormData();
formData.append('file', file);
formData.append('name', 'test');

fetch('/api/upload', {
  method: 'POST',
  body: formData
  // 注意:不需要手动设置 Content-Type,fetch 会自动设置
});
typescript

5. XSRF Token 处理#

Web Axios:

// Axios 自动从 cookie 中读取 xsrf-token 并添加到 header
axios.get('/api/user');
typescript

Lynx Fetch(需自行实现):

function getCookie(name: string): string | null {
  const match = document.cookie.match(new RegExp(`(^| )${name}=([^;]+)`));
  return match ? match[2] : null;
}

fetch('/api/user', {
  headers: {
    'X-XSRF-TOKEN': getCookie('XSRF-TOKEN')
  }
});
typescript

6. 错误类型区分#

Web Axios:

axios.get('/api/user')
  .catch(error => {
    if (error.response) {
      // 服务器返回错误状态码
      console.log(error.response.status);
    } else if (error.request) {
      // 请求已发出但没有收到响应
      console.log('Network error');
    } else {
      // 请求配置出错
      console.log('Request error');
    }
  });
typescript

Lynx Fetch(需自行判断):

fetch('/api/user')
  .catch(error => {
    if (error.name === 'AbortError') {
      console.log('Request timeout');
    } else if (error.message) {
      // 网络错误或 Fetch 自身错误
      console.log('Fetch error:', error.message);
    }
  });

// 检查响应状态需在 response.ok 中判断
const response = await fetch('/api/user').catch(error => {
  throw error;
});

if (!response.ok) {
  console.log('HTTP error:', response.status);
}
typescript

进度监控不支持的解决方案#

Lynx Fetch 不支持 onUploadProgressonDownloadProgress,这是与 Web Axios 的重要差异。对于需要显示上传/下载进度的场景,可以考虑:

  1. 使用原生 XMLHttpRequest(如果有暴露):
// 如果 Lynx 环境支持 XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.upload.onprogress = (e) => {
  if (e.lengthComputable) {
    const percent = (e.loaded / e.total) * 100;
    console.log(`Upload: ${percent}%`);
  }
};
typescript
  1. 使用分段上传:将大文件分成小块上传,服务端返回已接收的部分。

  2. 使用 Server-Sent Events:服务器推送进度状态,客户端监听更新。

完整的 Fetch 封装示例#

以下是综合处理的封装方案,模拟了 Axios 的大部分功能:

interface FetchOptions extends RequestInit {
  timeout?: number;
  params?: Record<string, string>;
}

interface FetchError extends Error {
  status?: number;
  timeout?: boolean;
}

class LynxFetchClient {
  private baseURL: string;

  constructor(baseURL: string) {
    this.baseURL = baseURL;
  }

  private async request<T>(
    method: string,
    url: string,
    options: FetchOptions = {}
  ): Promise<T> {
    const { timeout = 10000, params, headers, body, ...rest } = options;

    // 构建完整 URL
    let fullUrl = `${this.baseURL}${url}`;
    if (params) {
      const searchParams = new URLSearchParams(params);
      fullUrl += `?${searchParams.toString()}`;
    }

    // 构建 headers
    const mergedHeaders: Record<string, string> = {
      'Content-Type': 'application/json',
      ...(headers as Record<string, string>),
    };

    // 添加 token(示例)
    const token = this.getToken();
    if (token) {
      mergedHeaders['Authorization'] = `Bearer ${token}`;
    }

    // 超时控制
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeout);

    try {
      const response = await fetch(fullUrl, {
        method,
        headers: mergedHeaders,
        body: body ? (typeof body === 'string' ? body : JSON.stringify(body)) : undefined,
        signal: controller.signal,
        ...rest,
      });

      clearTimeout(timeoutId);

      if (!response.ok) {
        const error: FetchError = new Error(`HTTP ${response.status}`);
        error.status = response.status;
        throw error;
      }

      return response.json();
    } catch (error: any) {
      clearTimeout(timeoutId);

      if (error.name === 'AbortError') {
        const timeoutError: FetchError = new Error('Request timeout');
        timeoutError.timeout = true;
        throw timeoutError;
      }
      throw error;
    }
  }

  private getToken(): string | null {
    // 实现获取 token 的逻辑
    return null;
  }

  get<T>(url: string, options?: FetchOptions) {
    return this.request<T>('GET', url, options);
  }

  post<T>(url: string, data?: unknown, options?: FetchOptions) {
    return this.request<T>('POST', url, { ...options, body: data });
  }

  put<T>(url: string, data?: unknown, options?: FetchOptions) {
    return this.request<T>('PUT', url, { ...options, body: data });
  }

  delete<T>(url: string, options?: FetchOptions) {
    return this.request<T>('DELETE', url, options);
  }

  patch<T>(url: string, data?: unknown, options?: FetchOptions) {
    return this.request<T>('PATCH', url, { ...options, body: data });
  }
}

// 使用
const api = new LynxFetchClient('https://api.example.com');

api.get<User[]>('/users').then(users => {
  console.log(users);
}).catch(error => {
  if (error.timeout) {
    console.log('请求超时');
  } else if (error.status) {
    console.log(`HTTP 错误: ${error.status}`);
  }
});
typescript

结论#

对于 Lynx/ReactLynx 项目,推荐优先考虑 TanStack Query + Fetch 方案。它提供了完整的数据获取解决方案,能够显著提升开发效率和用户体验。Axios 则适合需要精细 HTTP 控制或已有存量项目的场景。两者也可以结合使用,取长补短。

最终的技术选型应该基于项目实际需求、团队技术背景和长期维护成本来综合考虑。

Lynx 移动端 HTTP 请求方案对比:TanStack Query vs Axios
https://johniexu.github.io/blog/lynx-http-client-comparison
Author JohnieXu
Published at April 27, 2026