Appearance
接口对接
在开发新闻/资讯APP时,接口对接是非常重要的一环。在这一节中,我们将学习如何使用 React Native 进行网络请求,对接新闻API,并处理数据。
1. 项目初始化
首先,让我们初始化一个新的 React Native 项目:
bash
npx react-native init NewsApp
cd NewsApp2. 安装必要的依赖
我们需要安装以下依赖:
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-carousel3. API 选择
我们可以使用一些免费的新闻API进行开发,例如:
- News API (https://newsapi.org/) - 需要注册获取API密钥
- GNews API (https://gnews.io/) - 提供免费额度
- MediaStack API (https://mediastack.com/) - 提供免费额度
- 纽约时报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 对接时,我们需要处理各种错误情况:
- 网络错误:设备无网络连接
- API 错误:API 返回错误状态码
- 数据错误:返回的数据格式不正确
- 超时错误:请求超时
让我们增强 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 对接,包括:
- 如何使用 axios 进行网络请求
- 如何创建 API 服务和数据模型
- 如何使用 Redux Toolkit 管理应用状态
- 如何处理错误和边界情况
- 如何实现缓存策略
这些技能将帮助我们构建更加可靠、高效的新闻/资讯APP。
11. 下一步
接下来,我们将学习如何开发新闻列表和详情页,为用户提供更好的阅读体验。
