引言
在 React Native 应用开发中,良好的用户体验离不开完善的加载状态和错误处理机制。本文将带你一步步封装可复用的加载中组件和网络错误组件,并分享如何优雅地处理网络请求。
1. 加载中组件的封装与使用
1.1 为什么需要加载中组件?
在移动应用开发中,网络请求是不可避免的。用户在进行操作后,如果没有任何反馈,会感到困惑甚至认为应用卡死了。加载中组件就是用来告诉用户:"应用正在努力工作,请稍等片刻。"
1.2 基础实现
先来看一个简单的加载状态实现:
export default function App() {
const [courses, setCourses] = useState([]);
const [loading, setLoading] = useState(true);
const fetchData = async () => {
try {
// 延迟 2 秒模拟网络请求
await new Promise(resolve => setTimeout(resolve, 2000));
const res = await fetch(`http://localhost:3000/search?q=${keyword}`);
const { data } = await res.json();
setCourses(data.courses);
} finally {
// 无论成功失败,都要关闭加载状态
setLoading(false);
}
};
// 关键:在返回主要内容前检查加载状态
if (loading) {
return <ActivityIndicator size="small" color="#1f99b0" style={styles.loading} />;
}
return (
// 页面主要内容
);
}
const styles = StyleSheet.create({
loading: {
backgroundColor: '#fff',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: 1, // 确保在最上层
}
});
开发小技巧:在 Expo 开发环境中,按 r 键可以快速刷新页面,方便调试。
1.3 封装可复用组件
为了提高代码的复用性和可维护性,我们将加载组件独立封装:
// components/shared/Loading.js
import { ActivityIndicator, StyleSheet } from 'react-native';
/**
* 加载中组件
* 用于数据加载时显示等待状态
*/
export default function Loading() {
return <ActivityIndicator size="small" color="#1f99b0" style={styles.loading} />;
}
const styles = StyleSheet.create({
loading: {
backgroundColor: '#fff',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: 1,
}
});
在父组件中使用封装后的组件:
import Loading from './components/shared/Loading';
// 在需要显示加载状态的地方
if (loading) {
return <Loading />;
}
2. 网络错误组件的封装
2.1 错误处理的重要性
网络请求可能会因为各种原因失败:网络不稳定、服务器错误、超时等。一个好的应用应该能够优雅地处理这些错误,而不是直接崩溃或显示空白页面。
2.2 基础错误组件
// components/shared/NetworkError.js
import { StyleSheet, Text, View } from 'react-native';
import SimpleLineIcons from '@expo/vector-icons/SimpleLineIcons';
export default function NetworkError() {
return (
<View style={styles.container}>
<SimpleLineIcons name={'drawer'} size={160} color={'#ddd'} />
<Text style={styles.title}>抱歉,网络连接出错了!</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
title: {
color: '#999'
}
});
在 App 组件中使用:
const [error, setError] = useState(false);
const fetchData = async () => {
try {
await new Promise(resolve => setTimeout(resolve, 2000));
const res = await fetch(`http://localhost:3000/search?q=${keyword}`);
const { data } = await res.json();
setCourses(data.courses);
} catch(err) {
setError(true); // 捕获错误
} finally {
setLoading(false);
}
};
if (error) {
return <NetworkError />;
}
2.3 使用 Expo 图标库
Expo 提供了丰富的矢量图标库,可以轻松地为错误页面添加视觉元素:
# 安装图标库
npx expo install @expo/vector-icons
3. 实现重新加载功能
3.1 通过 Props 传递自定义内容
让错误组件更加灵活,可以接收父组件传递的参数:
// 父组件传递自定义标题
if (error) {
return <NetworkError title='网络连接异常,请检查网络设置' />;
}
// 子组件接收 props
export default function NetworkError(props) {
// 使用默认值,避免未传递 title 时出错
const title = props.title || '抱歉,网络连接出错了!';
return (
<View style={styles.container}>
<SimpleLineIcons name={'drawer'} size={160} color={'#ddd'} />
<Text style={styles.title}>{title}</Text>
</View>
);
}
3.2 添加重新加载按钮
使用 TouchableOpacity 组件创建可点击的按钮:
import { TouchableOpacity } from 'react-native';
export default function NetworkError(props) {
const title = props.title || '抱歉,网络连接出错了!';
return (
<View style={styles.container}>
<SimpleLineIcons name={'drawer'} size={160} color={'#ddd'} />
<Text style={styles.title}>{title}</Text>
<TouchableOpacity style={styles.reload}>
<Text style={styles.label}>重新加载</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
title: {
color: '#999',
marginTop: 10,
},
reload: {
marginTop: 10,
backgroundColor: '#1f99b0',
height: 40,
borderRadius: 4,
paddingLeft: 10,
paddingRight: 10,
justifyContent: 'center',
alignItems: 'center',
},
label: {
color: '#fff',
fontSize: 16,
},
});
3.3 实现重新加载功能
// 父组件定义重新加载函数
const onReload = async () => {
setLoading(true);
setError(false);
await fetchData(); // 重新执行数据获取
};
if (error) {
// 注意:传递函数时不要加括号,否则会立即执行
return <NetworkError title='网络连接异常' onReload={onReload} />;
}
// 子组件接收并绑定点击事件
export default function NetworkError(props) {
const { title = '抱歉,网络连接出错了!', onReload } = props;
return (
<View style={styles.container}>
<SimpleLineIcons name={'drawer'} size={160} color={'#ddd'} />
<Text style={styles.title}>{title}</Text>
<TouchableOpacity style={styles.reload} onPress={onReload}>
<Text style={styles.label}>重新加载</Text>
</TouchableOpacity>
</View>
);
}
4. 封装网络请求
4.1 环境变量配置
在项目根目录创建 .env 文件,管理不同环境下的 API 地址:
# 开发环境
EXPO_PUBLIC_API_URL=http://localhost:3000
# 生产环境(使用真机测试时需要改为实际 IP)
EXPO_PUBLIC_API_URL=http://192.168.31.100:3000
注意:环境变量名必须全大写且以 EXPO_PUBLIC_ 开头。
4.2 安装 URL 处理库
npm install urlcat
urlcat 可以智能地拼接 URL 和查询参数:
import urlcat from 'urlcat';
// 自动处理问号和连接符
urlcat('http://localhost:3000', '/articles', { page: 1, limit: 10 })
// 输出:http://localhost:3000/articles?page=1&limit=10
4.3 封装请求工具
创建 utils/request.js 文件:
import urlcat from 'urlcat';
/**
* 基础请求函数
* @param {string} url - API 请求路径(如 '/articles')
* @param {object} [options] - 请求配置项
* @param {string} [options.method='GET'] - HTTP 方法
* @param {object} [options.params] - URL 查询参数
* @param {object} [options.body] - 请求体数据
* @returns {Promise<object>} 返回解析后的JSON数据
*/
const request = async (url, { method = 'GET', params, body } = {}) => {
const apiUrl = process.env.EXPO_PUBLIC_API_URL;
const requestUrl = urlcat(apiUrl, url, params);
const headers = {
Accept: 'application/json',
'Content-Type': 'application/json',
// 待完成:传递 token
};
const config = {
method,
headers,
// RN 中需要将 JavaScript 对象转为 JSON 字符串
...(body && { body: JSON.stringify(body) }),
};
const response = await fetch(requestUrl, config);
if (!response.ok) {
// 处理 HTTP 错误状态
const { message, errors } = await response.json().catch(() => ({}));
const error = new Error(message || '请求失败');
error.status = response.status;
error.errors = errors;
throw error;
}
return await response.json();
};
export default request;
4.4 封装语义化请求方法
为了提高代码的可读性,我们封装常用的 HTTP 方法:
/**
* GET 请求
* @param {string} url - 请求地址
* @param {object} [params] - 查询参数
*/
export const get = (url, params) => request(url, { method: 'GET', params });
/**
* POST 请求
* @param {string} url - 请求地址
* @param {object} body - 请求体数据
*/
export const post = (url, body) => request(url, { method: 'POST', body });
/**
* PUT 请求
* @param {string} url - 请求地址
* @param {object} body - 请求体数据
*/
export const put = (url, body) => request(url, { method: 'PUT', body });
/**
* PATCH 请求
* @param {string} url - 请求地址
* @param {object} body - 请求体数据
*/
export const patch = (url, body) => request(url, { method: 'PATCH', body });
/**
* DELETE 请求(避免使用 delete 关键字)
* @param {string} url - 请求地址
*/
export const del = (url) => request(url, { method: 'DELETE' });
4.5 在组件中使用封装的请求
方式一:使用基础的 request 函数
import request from '../utils/request';
const fetchData = async () => {
try {
const { data } = await request(`/search?q=${keyword}`);
setCourses(data.courses);
} catch (err) {
setError(true);
} finally {
setLoading(false);
}
};
方式二:使用 params 参数(推荐)
const { data } = await request('/search', {
params: { q: keyword } // 自动处理查询参数
});
方式三:使用语义化的 get 方法
import { get } from '../utils/request';
const fetchData = async () => {
try {
// 两种写法都可以
const { data } = await get(`/search?q=${keyword}`);
// 或
const { data } = await get('/search', { q: keyword });
setCourses(data.courses);
} catch (err) {
setError(true);
} finally {
setLoading(false);
}
};
5. 完整示例
将以上所有部分组合起来,形成一个完整的应用:
import React, { useState, useEffect } from 'react';
import { get } from './utils/request';
import Loading from './components/shared/Loading';
import NetworkError from './components/shared/NetworkError';
export default function App() {
const [courses, setCourses] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const [keyword, setKeyword] = useState('');
const fetchData = async () => {
try {
setError(false);
const { data } = await get('/search', { q: keyword });
setCourses(data.courses);
} catch (err) {
setError(true);
} finally {
setLoading(false);
}
};
const onReload = async () => {
setLoading(true);
await fetchData();
};
useEffect(() => {
fetchData();
}, []);
if (loading) {
return <Loading />;
}
if (error) {
return <NetworkError onReload={onReload} />;
}
return (
// 应用主要内容
);
}
总结
- 组件封装思维:将通用的 UI 逻辑封装成可复用组件,提高代码的复用性和可维护性
- 用户体验优化:提供清晰的加载状态和错误反馈,增强用户对应用的信任感
- 网络请求规范化:统一的请求处理和错误管理,减少重复代码
- 代码可维护性:语义化的 API 设计和合理的文件组织,便于团队协作