Skip to content

接口对接

在开发新闻/资讯APP时,接口对接是非常重要的一环。在这一节中,我们将学习如何使用 React Native 进行网络请求,对接新闻API,并处理数据。

1. 项目初始化

首先,让我们初始化一个新的 React Native 项目:

bash
npx react-native init NewsApp
cd NewsApp

2. 安装必要的依赖

我们需要安装以下依赖:

bash
# 安装网络请求库
npm install axios

# 安装导航库
npm install @react-navigation/native @react-navigation/stack

# 安装导航依赖
npm install react-native-screens react-native-safe-area-context

# 安装状态管理库
npm install @reduxjs/toolkit react-redux

# 安装UI组件库
npm install react-native-paper

# 安装图标库
npm install react-native-vector-icons

# 安装轮播图组件
npm install react-native-snap-carousel

3. API 选择

我们可以使用一些免费的新闻API进行开发,例如:

  1. News API (https://newsapi.org/) - 需要注册获取API密钥
  2. GNews API (https://gnews.io/) - 提供免费额度
  3. MediaStack API (https://mediastack.com/) - 提供免费额度
  4. 纽约时报API (https://developer.nytimes.com/) - 提供免费额度

在本教程中,我们将使用 News API 作为示例。

4. 创建 API 服务

首先,让我们创建一个 API 服务文件,用于处理所有的网络请求:

js
// src/services/api.js
import axios from 'axios';

// 创建 axios 实例
const api = axios.create({
  baseURL: 'https://newsapi.org/v2',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
  },
});

// API 密钥 - 请替换为你自己的 API 密钥
const API_KEY = 'YOUR_API_KEY';

// 获取头条新闻
export const getTopHeadlines = async (country = 'us', category = '') => {
  try {
    const response = await api.get('/top-headlines', {
      params: {
        country,
        category,
        apiKey: API_KEY,
      },
    });
    return response.data;
  } catch (error) {
    console.error('获取头条新闻失败:', error);
    throw error;
  }
};

// 搜索新闻
export const searchNews = async (query, page = 1, pageSize = 20) => {
  try {
    const response = await api.get('/everything', {
      params: {
        q: query,
        page,
        pageSize,
        apiKey: API_KEY,
      },
    });
    return response.data;
  } catch (error) {
    console.error('搜索新闻失败:', error);
    throw error;
  }
};

// 获取新闻源
export const getSources = async (category = '', country = '') => {
  try {
    const response = await api.get('/sources', {
      params: {
        category,
        country,
        apiKey: API_KEY,
      },
    });
    return response.data;
  } catch (error) {
    console.error('获取新闻源失败:', error);
    throw error;
  }
};

export default api;

5. 创建数据模型

为了更好地管理数据,我们可以创建数据模型:

js
// src/models/News.js
export class News {
  constructor({
    source,
    author,
    title,
    description,
    url,
    urlToImage,
    publishedAt,
    content,
  }) {
    this.source = source;
    this.author = author;
    this.title = title;
    this.description = description;
    this.url = url;
    this.urlToImage = urlToImage;
    this.publishedAt = publishedAt;
    this.content = content;
  }

  // 格式化发布时间
  get formattedDate() {
    const date = new Date(this.publishedAt);
    return date.toLocaleDateString('zh-CN', {
      year: 'numeric',
      month: 'long',
      day: 'numeric',
      hour: '2-digit',
      minute: '2-digit',
    });
  }

  // 获取来源名称
  get sourceName() {
    return this.source?.name || '未知来源';
  }
}

export class NewsSource {
  constructor({
    id,
    name,
    description,
    url,
    category,
    language,
    country,
  }) {
    this.id = id;
    this.name = name;
    this.description = description;
    this.url = url;
    this.category = category;
    this.language = language;
    this.country = country;
  }
}

6. 创建 Redux Store

为了管理应用状态,我们可以使用 Redux Toolkit:

js
// src/redux/store.js
import { configureStore } from '@reduxjs/toolkit';
import newsReducer from './slices/newsSlice';
import searchReducer from './slices/searchSlice';

export const store = configureStore({
  reducer: {
    news: newsReducer,
    search: searchReducer,
  },
});

export default store;
js
// src/redux/slices/newsSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { getTopHeadlines, getSources } from '../../services/api';
import { News, NewsSource } from '../../models/News';

// 异步获取头条新闻
export const fetchTopHeadlines = createAsyncThunk(
  'news/fetchTopHeadlines',
  async ({ country, category }, { rejectWithValue }) => {
    try {
      const data = await getTopHeadlines(country, category);
      return data.articles.map(article => new News(article));
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);

// 异步获取新闻源
export const fetchSources = createAsyncThunk(
  'news/fetchSources',
  async ({ category, country }, { rejectWithValue }) => {
    try {
      const data = await getSources(category, country);
      return data.sources.map(source => new NewsSource(source));
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);

const newsSlice = createSlice({
  name: 'news',
  initialState: {
    headlines: [],
    sources: [],
    loading: false,
    error: null,
    selectedCategory: 'general',
    selectedCountry: 'us',
  },
  reducers: {
    setCategory: (state, action) => {
      state.selectedCategory = action.payload;
    },
    setCountry: (state, action) => {
      state.selectedCountry = action.payload;
    },
  },
  extraReducers: (builder) => {
    builder
      // 处理 fetchTopHeadlines
      .addCase(fetchTopHeadlines.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchTopHeadlines.fulfilled, (state, action) => {
        state.loading = false;
        state.headlines = action.payload;
      })
      .addCase(fetchTopHeadlines.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload;
      })
      // 处理 fetchSources
      .addCase(fetchSources.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchSources.fulfilled, (state, action) => {
        state.loading = false;
        state.sources = action.payload;
      })
      .addCase(fetchSources.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload;
      });
  },
});

export const { setCategory, setCountry } = newsSlice.actions;
export default newsSlice.reducer;
js
// src/redux/slices/searchSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { searchNews } from '../../services/api';
import { News } from '../../models/News';

// 异步搜索新闻
export const fetchSearchResults = createAsyncThunk(
  'search/fetchSearchResults',
  async ({ query, page }, { rejectWithValue }) => {
    try {
      const data = await searchNews(query, page);
      return {
        articles: data.articles.map(article => new News(article)),
        totalResults: data.totalResults,
        page,
      };
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);

const searchSlice = createSlice({
  name: 'search',
  initialState: {
    results: [],
    totalResults: 0,
    loading: false,
    error: null,
    query: '',
    currentPage: 1,
    hasMore: true,
  },
  reducers: {
    setQuery: (state, action) => {
      state.query = action.payload;
      state.currentPage = 1;
      state.results = [];
      state.hasMore = true;
    },
    resetSearch: (state) => {
      state.results = [];
      state.totalResults = 0;
      state.query = '';
      state.currentPage = 1;
      state.hasMore = true;
      state.error = null;
    },
  },
  extraReducers: (builder) => {
    builder
      // 处理 fetchSearchResults
      .addCase(fetchSearchResults.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchSearchResults.fulfilled, (state, action) => {
        state.loading = false;
        if (action.payload.page === 1) {
          state.results = action.payload.articles;
        } else {
          state.results = [...state.results, ...action.payload.articles];
        }
        state.totalResults = action.payload.totalResults;
        state.currentPage = action.payload.page;
        state.hasMore = state.results.length < action.payload.totalResults;
      })
      .addCase(fetchSearchResults.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload;
      });
  },
});

export const { setQuery, resetSearch } = searchSlice.actions;
export default searchSlice.reducer;

7. 创建 API 测试组件

为了测试我们的 API 服务,我们可以创建一个简单的测试组件:

js
// src/components/APITest.js
import React, { useEffect, useState } from 'react';
import { View, Text, StyleSheet, ScrollView, ActivityIndicator, TouchableOpacity } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { fetchTopHeadlines, setCategory } from '../redux/slices/newsSlice';
import { News } from '../models/News';

const categories = [
  { id: 'general', name: '综合' },
  { id: 'business', name: '商业' },
  { id: 'entertainment', name: '娱乐' },
  { id: 'health', name: '健康' },
  { id: 'science', name: '科技' },
  { id: 'sports', name: '体育' },
  { id: 'technology', name: '技术' },
];

const APITest = () => {
  const dispatch = useDispatch();
  const { headlines, loading, error, selectedCategory } = useSelector(state => state.news);
  const [country, setCountry] = useState('us');

  useEffect(() => {
    dispatch(fetchTopHeadlines({ country, category: selectedCategory }));
  }, [dispatch, country, selectedCategory]);

  if (loading) {
    return (
      <View style={styles.center}>
        <ActivityIndicator size="large" color="#007AFF" />
      </View>
    );
  }

  if (error) {
    return (
      <View style={styles.center}>
        <Text style={styles.errorText}>加载失败: {error}</Text>
        <TouchableOpacity 
          style={styles.retryButton}
          onPress={() => dispatch(fetchTopHeadlines({ country, category: selectedCategory }))}
        >
          <Text style={styles.retryText}>重试</Text>
        </TouchableOpacity>
      </View>
    );
  }

  return (
    <View style={styles.container}>
      <ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.categoryContainer}>
        {categories.map(category => (
          <TouchableOpacity
            key={category.id}
            style={[
              styles.categoryButton,
              selectedCategory === category.id && styles.selectedCategoryButton
            ]}
            onPress={() => dispatch(setCategory(category.id))}
          >
            <Text 
              style={[
                styles.categoryText,
                selectedCategory === category.id && styles.selectedCategoryText
              ]}
            >
              {category.name}
            </Text>
          </TouchableOpacity>
        ))}
      </ScrollView>

      <ScrollView style={styles.newsContainer}>
        {headlines.map((news, index) => (
          <View key={index} style={styles.newsItem}>
            {news.urlToImage && (
              <View style={styles.imageContainer}>
                <Image 
                  source={{ uri: news.urlToImage }} 
                  style={styles.image}
                  resizeMode="cover"
                />
              </View>
            )}
            <View style={styles.newsContent}>
              <Text style={styles.title}>{news.title}</Text>
              <Text style={styles.description} numberOfLines={2}>
                {news.description}
              </Text>
              <View style={styles.newsFooter}>
                <Text style={styles.source}>{news.sourceName}</Text>
                <Text style={styles.date}>{news.formattedDate}</Text>
              </View>
            </View>
          </View>
        ))}
      </ScrollView>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F5F5F5',
  },
  center: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  errorText: {
    fontSize: 16,
    color: 'red',
    marginBottom: 16,
  },
  retryButton: {
    paddingVertical: 8,
    paddingHorizontal: 16,
    backgroundColor: '#007AFF',
    borderRadius: 4,
  },
  retryText: {
    color: 'white',
    fontSize: 16,
    fontWeight: '500',
  },
  categoryContainer: {
    backgroundColor: 'white',
    paddingVertical: 12,
    paddingHorizontal: 8,
    borderBottomWidth: 1,
    borderBottomColor: '#E0E0E0',
  },
  categoryButton: {
    paddingVertical: 6,
    paddingHorizontal: 16,
    marginHorizontal: 4,
    borderRadius: 16,
    backgroundColor: '#F0F0F0',
  },
  selectedCategoryButton: {
    backgroundColor: '#007AFF',
  },
  categoryText: {
    fontSize: 14,
    color: '#333',
  },
  selectedCategoryText: {
    color: 'white',
    fontWeight: '500',
  },
  newsContainer: {
    flex: 1,
  },
  newsItem: {
    backgroundColor: 'white',
    marginHorizontal: 12,
    marginTop: 12,
    borderRadius: 8,
    overflow: 'hidden',
    elevation: 2,
    shadowColor: '#000',
    shadowOffset: {
      width: 0,
      height: 1,
    },
    shadowOpacity: 0.1,
    shadowRadius: 2,
  },
  imageContainer: {
    width: '100%',
    height: 180,
  },
  image: {
    width: '100%',
    height: '100%',
  },
  newsContent: {
    padding: 12,
  },
  title: {
    fontSize: 16,
    fontWeight: '600',
    marginBottom: 8,
  },
  description: {
    fontSize: 14,
    color: '#666',
    marginBottom: 12,
  },
  newsFooter: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
  },
  source: {
    fontSize: 12,
    color: '#999',
  },
  date: {
    fontSize: 12,
    color: '#999',
  },
});

export default APITest;

8. 错误处理和边界情况

在进行 API 对接时,我们需要处理各种错误情况:

  1. 网络错误:设备无网络连接
  2. API 错误:API 返回错误状态码
  3. 数据错误:返回的数据格式不正确
  4. 超时错误:请求超时

让我们增强 API 服务的错误处理:

js
// src/services/api.js (增强版)
import axios from 'axios';
import NetInfo from '@react-native-community/netinfo';

// 创建 axios 实例
const api = axios.create({
  baseURL: 'https://newsapi.org/v2',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
  },
});

// API 密钥 - 请替换为你自己的 API 密钥
const API_KEY = 'YOUR_API_KEY';

// 请求拦截器
api.interceptors.request.use(
  async (config) => {
    // 检查网络连接
    const networkState = await NetInfo.fetch();
    if (!networkState.isConnected) {
      throw new Error('网络连接失败,请检查网络设置');
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// 响应拦截器
api.interceptors.response.use(
  (response) => {
    return response;
  },
  (error) => {
    if (error.response) {
      // API 返回错误状态码
      switch (error.response.status) {
        case 400:
          throw new Error('请求参数错误');
        case 401:
          throw new Error('API 密钥无效');
        case 403:
          throw new Error('请求被禁止');
        case 404:
          throw new Error('请求的资源不存在');
        case 429:
          throw new Error('请求过于频繁,请稍后再试');
        case 500:
          throw new Error('服务器内部错误');
        default:
          throw new Error(`请求失败: ${error.response.status}`);
      }
    } else if (error.request) {
      // 请求已发出,但没有收到响应
      throw new Error('服务器无响应,请稍后再试');
    } else {
      // 请求配置出错
      throw new Error('请求配置错误');
    }
  }
);

// 获取头条新闻
export const getTopHeadlines = async (country = 'us', category = '') => {
  try {
    const response = await api.get('/top-headlines', {
      params: {
        country,
        category,
        apiKey: API_KEY,
      },
    });
    return response.data;
  } catch (error) {
    console.error('获取头条新闻失败:', error);
    throw error;
  }
};

// 搜索新闻
export const searchNews = async (query, page = 1, pageSize = 20) => {
  try {
    const response = await api.get('/everything', {
      params: {
        q: query,
        page,
        pageSize,
        apiKey: API_KEY,
      },
    });
    return response.data;
  } catch (error) {
    console.error('搜索新闻失败:', error);
    throw error;
  }
};

// 获取新闻源
export const getSources = async (category = '', country = '') => {
  try {
    const response = await api.get('/sources', {
      params: {
        category,
        country,
        apiKey: API_KEY,
      },
    });
    return response.data;
  } catch (error) {
    console.error('获取新闻源失败:', error);
    throw error;
  }
};

export default api;

9. 缓存策略

为了提高应用性能和用户体验,我们可以实现缓存策略:

js
// src/services/cache.js
import AsyncStorage from '@react-native-async-storage/async-storage';

// 缓存键名
const CACHE_KEYS = {
  TOP_HEADLINES: 'top_headlines',
  SOURCES: 'sources',
  SEARCH_RESULTS: 'search_results',
};

// 缓存过期时间(毫秒)
const CACHE_EXPIRY = {
  TOP_HEADLINES: 10 * 60 * 1000, // 10分钟
  SOURCES: 24 * 60 * 60 * 1000, // 24小时
  SEARCH_RESULTS: 5 * 60 * 1000, // 5分钟
};

// 保存数据到缓存
export const saveToCache = async (key, data) => {
  try {
    const cacheData = {
      data,
      timestamp: Date.now(),
    };
    await AsyncStorage.setItem(key, JSON.stringify(cacheData));
  } catch (error) {
    console.error('保存缓存失败:', error);
  }
};

// 从缓存获取数据
export const getFromCache = async (key, expiry) => {
  try {
    const cacheDataStr = await AsyncStorage.getItem(key);
    if (!cacheDataStr) return null;

    const cacheData = JSON.parse(cacheDataStr);
    const now = Date.now();

    // 检查缓存是否过期
    if (now - cacheData.timestamp > expiry) {
      await AsyncStorage.removeItem(key);
      return null;
    }

    return cacheData.data;
  } catch (error) {
    console.error('获取缓存失败:', error);
    return null;
  }
};

// 清除缓存
export const clearCache = async (key) => {
  try {
    if (key) {
      await AsyncStorage.removeItem(key);
    } else {
      // 清除所有缓存
      const keys = Object.values(CACHE_KEYS);
      await AsyncStorage.multiRemove(keys);
    }
  } catch (error) {
    console.error('清除缓存失败:', error);
  }
};

export { CACHE_KEYS, CACHE_EXPIRY };

然后,我们可以在 API 服务中使用缓存:

js
// src/services/api.js (带缓存版本)
import axios from 'axios';
import NetInfo from '@react-native-community/netinfo';
import { saveToCache, getFromCache, CACHE_KEYS, CACHE_EXPIRY } from './cache';

// 创建 axios 实例
const api = axios.create({
  baseURL: 'https://newsapi.org/v2',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
  },
});

// API 密钥 - 请替换为你自己的 API 密钥
const API_KEY = 'YOUR_API_KEY';

// 请求拦截器
api.interceptors.request.use(
  async (config) => {
    // 检查网络连接
    const networkState = await NetInfo.fetch();
    if (!networkState.isConnected) {
      throw new Error('网络连接失败,请检查网络设置');
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// 响应拦截器
api.interceptors.response.use(
  (response) => {
    return response;
  },
  (error) => {
    if (error.response) {
      // API 返回错误状态码
      switch (error.response.status) {
        case 400:
          throw new Error('请求参数错误');
        case 401:
          throw new Error('API 密钥无效');
        case 403:
          throw new Error('请求被禁止');
        case 404:
          throw new Error('请求的资源不存在');
        case 429:
          throw new Error('请求过于频繁,请稍后再试');
        case 500:
          throw new Error('服务器内部错误');
        default:
          throw new Error(`请求失败: ${error.response.status}`);
      }
    } else if (error.request) {
      // 请求已发出,但没有收到响应
      throw new Error('服务器无响应,请稍后再试');
    } else {
      // 请求配置出错
      throw new Error('请求配置错误');
    }
  }
);

// 获取头条新闻
export const getTopHeadlines = async (country = 'us', category = '') => {
  try {
    // 尝试从缓存获取
    const cacheKey = `${CACHE_KEYS.TOP_HEADLINES}_${country}_${category}`;
    const cachedData = await getFromCache(cacheKey, CACHE_EXPIRY.TOP_HEADLINES);
    if (cachedData) {
      return cachedData;
    }

    // 缓存未命中,从 API 获取
    const response = await api.get('/top-headlines', {
      params: {
        country,
        category,
        apiKey: API_KEY,
      },
    });

    // 保存到缓存
    await saveToCache(cacheKey, response.data);
    return response.data;
  } catch (error) {
    console.error('获取头条新闻失败:', error);
    throw error;
  }
};

// 搜索新闻
export const searchNews = async (query, page = 1, pageSize = 20) => {
  try {
    // 尝试从缓存获取(仅第一页)
    if (page === 1) {
      const cacheKey = `${CACHE_KEYS.SEARCH_RESULTS}_${query}`;
      const cachedData = await getFromCache(cacheKey, CACHE_EXPIRY.SEARCH_RESULTS);
      if (cachedData) {
        return cachedData;
      }
    }

    // 缓存未命中,从 API 获取
    const response = await api.get('/everything', {
      params: {
        q: query,
        page,
        pageSize,
        apiKey: API_KEY,
      },
    });

    // 保存第一页到缓存
    if (page === 1) {
      const cacheKey = `${CACHE_KEYS.SEARCH_RESULTS}_${query}`;
      await saveToCache(cacheKey, response.data);
    }

    return response.data;
  } catch (error) {
    console.error('搜索新闻失败:', error);
    throw error;
  }
};

// 获取新闻源
export const getSources = async (category = '', country = '') => {
  try {
    // 尝试从缓存获取
    const cacheKey = `${CACHE_KEYS.SOURCES}_${category}_${country}`;
    const cachedData = await getFromCache(cacheKey, CACHE_EXPIRY.SOURCES);
    if (cachedData) {
      return cachedData;
    }

    // 缓存未命中,从 API 获取
    const response = await api.get('/sources', {
      params: {
        category,
        country,
        apiKey: API_KEY,
      },
    });

    // 保存到缓存
    await saveToCache(cacheKey, response.data);
    return response.data;
  } catch (error) {
    console.error('获取新闻源失败:', error);
    throw error;
  }
};

export default api;

10. 总结

通过本章节的学习,我们掌握了如何在 React Native 应用中进行 API 对接,包括:

  1. 如何使用 axios 进行网络请求
  2. 如何创建 API 服务和数据模型
  3. 如何使用 Redux Toolkit 管理应用状态
  4. 如何处理错误和边界情况
  5. 如何实现缓存策略

这些技能将帮助我们构建更加可靠、高效的新闻/资讯APP。

11. 下一步

接下来,我们将学习如何开发新闻列表和详情页,为用户提供更好的阅读体验。

© 2026 编程马·菜鸟教程 版权所有