React Native鸿蒙应用开发:下拉刷新与上拉加载完整实战

前言:本文详细介绍在React Native for OpenHarmony项目中实现下拉刷新和上拉加载功能的完整方案。从技术选型、核心实现、性能优化到工程化交付,提供可直接复制运行的完整代码,帮助开发者快速掌握鸿蒙应用中列表数据加载的最佳实践。
一、技术背景与需求分析
1.1 React Native for OpenHarmony简介
React Native for OpenHarmony(简称RNOH)是React Native针对开源鸿蒙系统的适配版本,让开发者可以使用熟悉的React技术栈开发鸿蒙应用。相比原生ArkTS开发,RNOH具有以下优势:
| 特性 | RNOH | 原生ArkTS |
|---|---|---|
| 学习成本 | 低(React开发者友好) | 中(需学习ArkTS) |
| 开发效率 | 高(热更新、组件复用) | 中 |
| 跨平台能力 | 支持Android/iOS/HarmonyOS | 仅HarmonyOS |
| 性能 | 接近原生 | 原生级别 |
| 社区生态 | 丰富(RN生态可复用) | 发展中 |
1.2 核心功能需求
列表数据加载是移动应用中最常见的场景之一,本文实现的功能包括:
1.2.1 下拉刷新(Pull to Refresh)
功能描述:用户在列表顶部向下拉动,触发刷新动画,松手后重新请求数据。
核心要点:
- 触发阈值控制(避免误触发)
- 刷新状态可视化(进度指示器)
- 数据更新后列表滚动位置处理
- 刷新失败后的错误处理
交互流程:
用户下拉 → 显示刷新指示器 → 达到阈值 → 触发刷新请求
↓
请求进行中 → 显示加载动画 → 请求完成
↓
成功:更新数据 + 隐藏指示器 + 提示成功
失败:保持原数据 + 隐藏指示器 + 提示错误
1.2.2 上拉加载(Load More)
功能描述:用户滚动到列表底部时,自动加载下一页数据。
核心要点:
- 触发阈值控制(距离底部多少像素触发)
- 加载状态锁(防止重复触发)
- 无更多数据的判断与提示
- 加载失败后的重试机制
交互流程:
用户滚动 → 接近底部 → 检查是否有更多数据
↓
有更多数据 → 显示加载指示器 → 请求下一页
↓
请求完成 → 追加数据到列表 → 隐藏指示器
↓
无更多数据 → 显示"暂无更多数据"提示
1.2.3 多场景状态提示
| 状态 | 触发条件 | 展示内容 |
|---|---|---|
| 首次加载中 | 组件初始化 | 全屏加载动画 |
| 加载失败 | 网络请求失败 | 错误提示 + 重试按钮 |
| 空数据 | 列表无数据 | 空状态提示 + 引导刷新 |
| 列表有数据 | 正常状态 | 正常显示列表 |
| 加载更多中 | 上拉加载进行中 | 底部加载动画 |
| 无更多数据 | 已加载全部数据 | 底部"暂无更多"提示 |
1.3 技术选型分析
1.3.1 列表组件选择
| 组件 | 优势 | 劣势 | 推荐指数 |
|---|---|---|---|
| FlatList | 虚拟化、性能好 | 配置稍复杂 | ⭐⭐⭐⭐⭐ |
| ScrollView | 简单易用 | 大数据量卡顿 | ⭐⭐⭐ |
| SectionList | 支持分组 | 不适合简单列表 | ⭐⭐⭐⭐ |
推荐使用FlatList,原因:
- 内置虚拟化渲染,性能优秀
- 支持下拉刷新原生属性
- 提供onEndReached回调实现上拉加载
- 支持keyExtractor优化渲染
1.3.2 状态管理方案
| 方案 | 复杂度 | 适用场景 |
|---|---|---|
| useState | 低 | 简单列表 |
| useReducer | 中 | 复杂状态逻辑 |
| Redux/MobX | 高 | 大型应用 |
本文采用useState,适合中等复杂度的列表场景。
二、项目环境搭建
2.1 项目初始化
bash
# 创建RN项目(使用OpenHarmony兼容版本)
npx react-native@0.72.5 init RNOHRefreshList --version 0.72.5
# 进入项目目录
cd RNOHRefreshList
# 安装鸿蒙适配依赖
npm install @react-native-oh/react-native-harmony@0.72.90
2.2 目录结构规划
RNOHRefreshList/
├── src/
│ ├── components/ # 组件目录
│ │ ├── RefreshList/ # 刷新列表组件
│ │ │ ├── index.tsx # 组件入口
│ │ │ ├── ListItem.tsx # 列表项组件
│ │ │ ├── RefreshHeader.tsx # 刷新头部
│ │ │ ├── LoadMoreFooter.tsx # 加载底部
│ │ │ └── EmptyState.tsx # 空状态
│ │ └── Toast/ # 提示组件
│ ├── services/ # 服务层
│ │ ├── ApiService.ts # API接口
│ │ └── MockService.ts # Mock数据
│ ├── hooks/ # 自定义Hooks
│ │ ├── useRefresh.ts # 刷新Hook
│ │ └── useLoadMore.ts # 加载更多Hook
│ ├── types/ # 类型定义
│ │ └── index.ts # 类型导出
│ └── utils/ # 工具函数
│ ├── request.ts # 网络请求封装
│ └── constants.ts # 常量定义
├── harmony/ # 鸿蒙工程
└── App.tsx # 应用入口
2.3 类型定义
typescript
// src/types/index.ts
/**
* 列表数据项接口
*/
export interface ListItem {
id: string; // 唯一标识
title: string; // 标题
description: string; // 描述
timestamp: number; // 时间戳
extra?: Record<string, any>; // 扩展字段
}
/**
* 列表响应数据
*/
export interface ListResponse {
code: number; // 响应码
message: string; // 响应消息
data: {
list: ListItem[]; // 数据列表
total: number; // 总数量
page: number; // 当前页码
pageSize: number; // 每页大小
hasMore: boolean; // 是否有更多数据
};
}
/**
* 请求参数
*/
export interface ListRequestParams {
page: number; // 页码
pageSize: number; // 每页大小
keyword?: string; // 搜索关键词
}
/**
* 列表状态枚举
*/
export enum ListState {
IDLE = 'idle', // 空闲
LOADING = 'loading', // 加载中
SUCCESS = 'success', // 成功
ERROR = 'error', // 错误
EMPTY = 'empty' // 空数据
}
/**
* 刷新状态
*/
export interface RefreshState {
isRefreshing: boolean; // 是否正在刷新
lastRefreshTime: number; // 上次刷新时间
}
/**
* 加载更多状态
*/
export interface LoadMoreState {
isLoadingMore: boolean; // 是否正在加载更多
hasMore: boolean; // 是否有更多数据
currentPage: number; // 当前页码
}
2.4 常量配置
typescript
// src/utils/constants.ts
/**
* API配置常量
*/
export const API_CONFIG = {
BASE_URL: 'https://api.example.com',
TIMEOUT: 15000, // 请求超时时间(毫秒)
} as const;
/**
* 列表配置常量
*/
export const LIST_CONFIG = {
PAGE_SIZE: 20, // 每页数据量
INITIAL_PAGE: 1, // 起始页码
REFRESH_THRESHOLD: 80, // 下拉刷新触发阈值
LOAD_MORE_THRESHOLD: 0.2,// 上拉加载触发阈值(距底部比例)
MIN_REFRESH_INTERVAL: 1000, // 最小刷新间隔(毫秒)
} as const;
/**
* 样式配置常量
*/
export const STYLE_CONFIG = {
COLORS: {
PRIMARY: '#007AFF',
SUCCESS: '#34C759',
ERROR: '#FF3B30',
WARNING: '#FF9500',
TEXT_PRIMARY: '#000000',
TEXT_SECONDARY: '#8E8E93',
TEXT_TERTIARY: '#C7C7CC',
BACKGROUND: '#F2F2F7',
CARD_BACKGROUND: '#FFFFFF',
BORDER: '#E5E5EA',
},
SPACING: {
XS: 4,
SM: 8,
MD: 12,
LG: 16,
XL: 20,
XXL: 24,
},
FONT_SIZE: {
XS: 12,
SM: 14,
MD: 16,
LG: 18,
XL: 20,
XXL: 24,
},
BORDER_RADIUS: {
SM: 4,
MD: 8,
LG: 12,
XL: 16,
},
} as const;
三、网络请求封装
3.1 请求工具类
typescript
// src/utils/request.ts
import { API_CONFIG } from './constants';
/**
* HTTP请求方法枚举
*/
export enum RequestMethod {
GET = 'GET',
POST = 'POST',
PUT = 'PUT',
DELETE = 'DELETE',
}
/**
* 请求配置接口
*/
interface RequestConfig {
method?: RequestMethod;
headers?: Record<string, string>;
params?: Record<string, any>;
data?: any;
timeout?: number;
}
/**
* 请求响应接口
*/
interface Response<T> {
code: number;
message: string;
data: T;
}
/**
* HTTP错误类
*/
export class HttpError extends Error {
constructor(
public code: number,
message: string,
public data?: any
) {
super(message);
this.name = 'HttpError';
}
}
/**
* 网络请求工具类
*/
export class HttpClient {
private baseURL: string;
private defaultTimeout: number;
constructor(baseURL: string = API_CONFIG.BASE_URL, timeout: number = API_CONFIG.TIMEOUT) {
this.baseURL = baseURL;
this.defaultTimeout = timeout;
}
/**
* 构建完整URL
*/
private buildURL(endpoint: string, params?: Record<string, any>): string {
const url = new URL(endpoint, this.baseURL);
if (params) {
Object.keys(params).forEach(key => {
if (params[key] !== undefined && params[key] !== null) {
url.searchParams.append(key, String(params[key]));
}
});
}
return url.toString();
}
/**
* 通用请求方法
*/
private async request<T>(
endpoint: string,
config: RequestConfig = {}
): Promise<Response<T>> {
const {
method = RequestMethod.GET,
headers = {},
params,
data,
timeout = this.defaultTimeout,
} = config;
const url = this.buildURL(endpoint, params);
// 构建请求配置
const requestInit: RequestInit = {
method,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
...headers,
},
};
if (data && method !== RequestMethod.GET) {
requestInit.body = JSON.stringify(data);
}
try {
// 创建超时控制器
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const response = await fetch(url, {
...requestInit,
signal: controller.signal,
});
clearTimeout(timeoutId);
// 解析响应
const responseData = await response.json();
if (!response.ok) {
throw new HttpError(
responseData.code || response.status,
responseData.message || '请求失败',
responseData.data
);
}
return responseData;
} catch (error) {
if (error instanceof HttpError) {
throw error;
}
if (error instanceof Error && error.name === 'AbortError') {
throw new HttpError(408, '请求超时');
}
throw new HttpError(500, '网络请求失败');
}
}
/**
* GET请求
*/
async get<T>(endpoint: string, params?: Record<string, any>): Promise<Response<T>> {
return this.request<T>(endpoint, { method: RequestMethod.GET, params });
}
/**
* POST请求
*/
async post<T>(endpoint: string, data?: any): Promise<Response<T>> {
return this.request<T>(endpoint, { method: RequestMethod.POST, data });
}
/**
* PUT请求
*/
async put<T>(endpoint: string, data?: any): Promise<Response<T>> {
return this.request<T>(endpoint, { method: RequestMethod.PUT, data });
}
/**
* DELETE请求
*/
async delete<T>(endpoint: string): Promise<Response<T>> {
return this.request<T>(endpoint, { method: RequestMethod.DELETE });
}
}
// 导出单例
export const httpClient = new HttpClient();
3.2 API服务层
typescript
// src/services/ApiService.ts
import { httpClient, HttpError } from '../utils/request';
import { ListItem, ListResponse, ListRequestParams } from '../types';
/**
* 列表API服务
*/
export class ListApiService {
private static readonly ENDPOINT = '/api/v1/list';
/**
* 获取列表数据
*/
static async getList(params: ListRequestParams): Promise<ListResponse> {
try {
const response = await httpClient.get<ListResponse>(
this.ENDPOINT,
params
);
return response;
} catch (error) {
if (error instanceof HttpError) {
throw error;
}
throw new HttpError(500, '获取列表数据失败');
}
}
/**
* 刷新列表(获取第一页数据)
*/
static async refreshList(): Promise<ListResponse> {
return this.getList({
page: 1,
pageSize: 20,
});
}
}
3.3 Mock服务(用于开发测试)
typescript
// src/services/MockService.ts
import { ListItem, ListResponse, ListRequestParams } from '../types';
import { LIST_CONFIG } from '../utils/constants';
/**
* Mock数据服务
*/
export class MockDataService {
private static readonly DELAY = 1000; // 模拟网络延迟
private static readonly ERROR_RATE = 0.1; // 模拟错误率
/**
* 模拟网络延迟
*/
private static async delay(): Promise<void> {
await new Promise(resolve => setTimeout(resolve, this.DELAY));
}
/**
* 模拟随机错误
*/
private static maybeThrowError(): void {
if (Math.random() < this.ERROR_RATE) {
throw new Error('网络请求失败');
}
}
/**
* 生成Mock列表项
*/
private static generateMockItem(page: number, index: number): ListItem {
const id = `${page}_${index}_${Date.now()}`;
return {
id,
title: `列表项目 ${page}-${index + 1}`,
description: `这是第 ${page} 页第 ${index + 1} 项的详细描述内容`,
timestamp: Date.now(),
extra: {
page,
index,
likes: Math.floor(Math.random() * 1000),
comments: Math.floor(Math.random() * 100),
},
};
}
/**
* 获取Mock列表数据
*/
static async getMockList(params: ListRequestParams): Promise<ListResponse> {
await this.delay();
this.maybeThrowError();
const { page, pageSize = LIST_CONFIG.PAGE_SIZE } = params;
// 模拟最后一页数据不足的情况
const isLastPage = page >= 5;
const itemCount = isLastPage ? Math.floor(pageSize / 2) : pageSize;
const list: ListItem[] = Array.from({ length: itemCount }, (_, index) =>
this.generateMockItem(page, index)
);
return {
code: 200,
message: 'success',
data: {
list,
total: 5 * pageSize - Math.floor(pageSize / 2),
page,
pageSize,
hasMore: !isLastPage,
},
};
}
}
四、自定义Hooks开发
4.1 下拉刷新Hook
typescript
// src/hooks/useRefresh.ts
import { useState, useCallback, useRef } from 'react';
import { ListApiService, MockDataService } from '../services';
import { ListItem } from '../types';
import { LIST_CONFIG } from '../utils/constants';
/**
* 下拉刷新Hook配置
*/
interface UseRefreshConfig {
onSuccess?: (data: ListItem[]) => void;
onError?: (error: Error) => void;
useMock?: boolean; // 是否使用Mock数据
}
/**
* 下拉刷新Hook
*/
export function useRefresh(config: UseRefreshConfig = {}) {
const { onSuccess, onError, useMock = true } = config;
const [isRefreshing, setIsRefreshing] = useState(false);
const [lastRefreshTime, setLastRefreshTime] = useState(0);
const refreshTimerRef = useRef<NodeJS.Timeout>();
/**
* 执行刷新
*/
const refresh = useCallback(async () => {
// 防抖:限制刷新频率
const now = Date.now();
if (now - lastRefreshTime < LIST_CONFIG.MIN_REFRESH_INTERVAL) {
return;
}
// 清除之前的定时器
if (refreshTimerRef.current) {
clearTimeout(refreshTimerRef.current);
}
setIsRefreshing(true);
try {
let response;
if (useMock) {
response = await MockDataService.getMockList({
page: 1,
pageSize: LIST_CONFIG.PAGE_SIZE,
});
} else {
response = await ListApiService.refreshList();
}
setLastRefreshTime(now);
onSuccess?.(response.data.list);
} catch (error) {
onError?.(error as Error);
} finally {
// 延迟重置状态,确保动画完整播放
refreshTimerRef.current = setTimeout(() => {
setIsRefreshing(false);
}, 300);
}
}, [lastRefreshTime, onSuccess, onError, useMock]);
/**
* 手动触发刷新(忽略防抖)
*/
const forceRefresh = useCallback(async () => {
setLastRefreshTime(0);
await refresh();
}, [refresh]);
return {
isRefreshing,
refresh,
forceRefresh,
lastRefreshTime,
};
}
4.2 上拉加载Hook
typescript
// src/hooks/useLoadMore.ts
import { useState, useCallback, useRef } from 'react';
import { ListApiService, MockDataService } from '../services';
import { ListItem, LoadMoreState } from '../types';
import { LIST_CONFIG } from '../utils/constants';
/**
* 上拉加载Hook配置
*/
interface UseLoadMoreConfig {
initialPage?: number;
onSuccess?: (data: ListItem[], hasMore: boolean) => void;
onError?: (error: Error) => void;
useMock?: boolean;
}
/**
* 上拉加载Hook
*/
export function useLoadMore(config: UseLoadMoreConfig = {}) {
const {
initialPage = LIST_CONFIG.INITIAL_PAGE,
onSuccess,
onError,
useMock = true,
} = config;
const [state, setState] = useState<LoadMoreState>({
isLoadingMore: false,
hasMore: true,
currentPage: initialPage,
});
const loadingLockRef = useRef(false);
/**
* 加载下一页数据
*/
const loadMore = useCallback(async () => {
// 加载锁:防止重复触发
if (loadingLockRef.current || !state.hasMore) {
return;
}
loadingLockRef.current = true;
setState(prev => ({ ...prev, isLoadingMore: true }));
try {
const nextPage = state.currentPage + 1;
let response;
if (useMock) {
response = await MockDataService.getMockList({
page: nextPage,
pageSize: LIST_CONFIG.PAGE_SIZE,
});
} else {
response = await ListApiService.getList({
page: nextPage,
pageSize: LIST_CONFIG.PAGE_SIZE,
});
}
const { list, hasMore } = response.data;
setState({
isLoadingMore: false,
hasMore,
currentPage: nextPage,
});
onSuccess?.(list, hasMore);
} catch (error) {
setState(prev => ({ ...prev, isLoadingMore: false }));
onError?.(error as Error);
} finally {
loadingLockRef.current = false;
}
}, [state.currentPage, state.hasMore, onSuccess, onError, useMock]);
/**
* 重置加载状态
*/
const reset = useCallback(() => {
setState({
isLoadingMore: false,
hasMore: true,
currentPage: initialPage,
});
loadingLockRef.current = false;
}, [initialPage]);
/**
* 设置是否有更多数据
*/
const setHasMore = useCallback((hasMore: boolean) => {
setState(prev => ({ ...prev, hasMore }));
}, []);
return {
...state,
loadMore,
reset,
setHasMore,
};
}
4.3 列表数据管理Hook
typescript
// src/hooks/useListData.ts
import { useState, useCallback, useEffect } from 'react';
import { ListItem, ListState } from '../types';
import { useRefresh } from './useRefresh';
import { useLoadMore } from './useLoadMore';
import { LIST_CONFIG } from '../utils/constants';
/**
* 列表数据管理Hook配置
*/
interface UseListDataConfig {
initialData?: ListItem[];
autoLoad?: boolean; // 是否自动加载初始数据
useMock?: boolean;
}
/**
* 列表数据管理Hook
*/
export function useListData(config: UseListDataConfig = {}) {
const {
initialData = [],
autoLoad = true,
useMock = true,
} = config;
// 数据状态
const [data, setData] = useState<ListItem[]>(initialData);
const [listState, setListState] = useState<ListState>(
initialData.length > 0 ? ListState.SUCCESS : ListState.IDLE
);
const [error, setError] = useState<Error | null>(null);
// 下拉刷新
const { isRefreshing, refresh, forceRefresh } = useRefresh({
useMock,
onSuccess: (newData) => {
setData(newData);
setListState(newData.length === 0 ? ListState.EMPTY : ListState.SUCCESS);
setError(null);
},
onError: (err) => {
setError(err);
setListState(data.length === 0 ? ListState.ERROR : ListState.SUCCESS);
},
});
// 上拉加载
const { isLoadingMore, hasMore, loadMore, reset: resetLoadMore } = useLoadMore({
useMock,
onSuccess: (newData, hasMoreData) => {
setData(prev => [...prev, ...newData]);
},
onError: (err) => {
setError(err);
},
});
/**
* 加载初始数据
*/
const loadInitialData = useCallback(async () => {
setListState(ListState.LOADING);
setError(null);
await forceRefresh();
}, [forceRefresh]);
/**
* 刷新数据(重置所有状态)
*/
const handleRefresh = useCallback(async () => {
resetLoadMore();
await refresh();
}, [refresh, resetLoadMore]);
/**
* 加载更多数据
*/
const handleLoadMore = useCallback(() => {
if (!isLoadingMore && hasMore) {
loadMore();
}
}, [isLoadingMore, hasMore, loadMore]);
/**
* 重试(加载失败时)
*/
const retry = useCallback(() => {
if (data.length === 0) {
loadInitialData();
} else {
loadMore();
}
}, [data.length, loadInitialData, loadMore]);
// 自动加载初始数据
useEffect(() => {
if (autoLoad && listState === ListState.IDLE) {
loadInitialData();
}
}, [autoLoad, listState, loadInitialData]);
return {
// 数据
data,
listState,
error,
// 刷新相关
isRefreshing,
refresh: handleRefresh,
// 加载更多相关
isLoadingMore,
hasMore,
loadMore: handleLoadMore,
// 其他操作
retry,
loadInitialData,
};
}
五、UI组件开发
5.1 列表项组件
typescript
// src/components/RefreshList/ListItem.tsx
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { ListItem } from '../../types';
import { STYLE_CONFIG } from '../../utils/constants';
interface ListItemProps {
item: ListItem;
onPress?: (item: ListItem) => void;
onLongPress?: (item: ListItem) => void;
}
export const ListItemComponent: React.FC<ListItemProps> = React.memo(({
item,
onPress,
onLongPress,
}) => {
const handlePress = () => {
onPress?.(item);
};
const handleLongPress = () => {
onLongPress?.(item);
};
// 格式化时间
const formatTime = (timestamp: number): string => {
const date = new Date(timestamp);
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
};
return (
<TouchableOpacity
onPress={handlePress}
onLongPress={handleLongPress}
activeOpacity={0.7}
style={styles.container}
>
{/* 主要内容 */}
<View style={styles.contentContainer}>
<Text style={styles.title} numberOfLines={2}>
{item.title}
</Text>
<Text style={styles.description} numberOfLines={2}>
{item.description}
</Text>
{/* 底部信息栏 */}
<View style={styles.footerContainer}>
<Text style={styles.timeText}>
{formatTime(item.timestamp)}
</Text>
{item.extra && (
<>
<View style={styles.divider} />
<Text style={styles.likesText}>
👍 {item.extra.likes}
</Text>
<View style={styles.divider} />
<Text style={styles.commentsText}>
💬 {item.extra.comments}
</Text>
</>
)}
</View>
</View>
{/* 右侧箭头 */}
<Text style={styles.arrow}>›</Text>
</TouchableOpacity>
);
});
ListItemComponent.displayName = 'ListItemComponent';
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: STYLE_CONFIG.COLORS.CARD_BACKGROUND,
paddingHorizontal: STYLE_CONFIG.SPACING.LG,
paddingVertical: STYLE_CONFIG.SPACING.MD,
marginHorizontal: STYLE_CONFIG.SPACING.MD,
marginTop: STYLE_CONFIG.SPACING.SM,
borderRadius: STYLE_CONFIG.BORDER_RADIUS.MD,
...STYLE_CONFIG.SHADOW.SM,
},
contentContainer: {
flex: 1,
},
title: {
fontSize: STYLE_CONFIG.FONT_SIZE.MD,
fontWeight: '600',
color: STYLE_CONFIG.COLORS.TEXT_PRIMARY,
marginBottom: STYLE_CONFIG.SPACING.XS,
},
description: {
fontSize: STYLE_CONFIG.FONT_SIZE.SM,
color: STYLE_CONFIG.COLORS.TEXT_SECONDARY,
lineHeight: 20,
marginBottom: STYLE_CONFIG.SPACING.SM,
},
footerContainer: {
flexDirection: 'row',
alignItems: 'center',
},
timeText: {
fontSize: STYLE_CONFIG.FONT_SIZE.XS,
color: STYLE_CONFIG.COLORS.TEXT_TERTIARY,
},
divider: {
width: StyleSheet.hairlineWidth,
height: 12,
backgroundColor: STYLE_CONFIG.COLORS.BORDER,
marginHorizontal: STYLE_CONFIG.SPACING.SM,
},
likesText: {
fontSize: STYLE_CONFIG.FONT_SIZE.XS,
color: STYLE_CONFIG.COLORS.TEXT_TERTIARY,
},
commentsText: {
fontSize: STYLE_CONFIG.FONT_SIZE.XS,
color: STYLE_CONFIG.COLORS.TEXT_TERTIARY,
},
arrow: {
fontSize: 24,
color: STYLE_CONFIG.COLORS.TEXT_TERTIARY,
marginLeft: STYLE_CONFIG.SPACING.SM,
},
});
// 添加阴影样式
declare global {
namespace StyleSheet {
interface AmbientProperties {
shadow?: {
radius?: number;
color?: string;
offsetX?: number;
offsetY?: number;
opacity?: number;
};
}
}
}
// 扩展样式配置
STYLE_CONFIG.SHADOW = {
SM: {
radius: 4,
color: '#000000',
offsetX: 0,
offsetY: 2,
opacity: 0.1,
},
MD: {
radius: 8,
color: '#000000',
offsetX: 0,
offsetY: 4,
opacity: 0.1,
},
};
5.2 刷新头部组件
typescript
// src/components/RefreshList/RefreshHeader.tsx
import React from 'react';
import { View, Text, StyleSheet, ActivityIndicator } from 'react-native';
import { STYLE_CONFIG } from '../../utils/constants';
interface RefreshHeaderProps {
isRefreshing: boolean;
lastRefreshTime?: number;
}
export const RefreshHeader: React.FC<RefreshHeaderProps> = ({
isRefreshing,
lastRefreshTime,
}) => {
// 格式化刷新时间
const formatRefreshTime = (timestamp: number): string => {
const now = Date.now();
const diff = now - timestamp;
if (diff < 60000) {
return '刚刚';
} else if (diff < 3600000) {
return `${Math.floor(diff / 60000)}分钟前`;
} else {
const date = new Date(timestamp);
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
}
};
return (
<View style={styles.container}>
{isRefreshing ? (
<View style={styles.refreshingContainer}>
<ActivityIndicator
size="small"
color={STYLE_CONFIG.COLORS.PRIMARY}
/>
<Text style={styles.text}>刷新中...</Text>
</View>
) : (
lastRefreshTime && (
<View style={styles.infoContainer}>
<Text style={styles.text}>
上次更新:{formatRefreshTime(lastRefreshTime)}
</Text>
</View>
)
)}
</View>
);
};
const styles = StyleSheet.create({
container: {
height: 60,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: STYLE_CONFIG.COLORS.BACKGROUND,
},
refreshingContainer: {
flexDirection: 'row',
alignItems: 'center',
},
infoContainer: {
paddingHorizontal: STYLE_CONFIG.SPACING.LG,
},
text: {
fontSize: STYLE_CONFIG.FONT_SIZE.SM,
color: STYLE_CONFIG.COLORS.TEXT_SECONDARY,
marginLeft: STYLE_CONFIG.SPACING.SM,
},
});
5.3 加载底部组件
typescript
// src/components/RefreshList/LoadMoreFooter.tsx
import React from 'react';
import { View, Text, StyleSheet, ActivityIndicator } from 'react-native';
import { STYLE_CONFIG } from '../../utils/constants';
interface LoadMoreFooterProps {
isLoadingMore: boolean;
hasMore: boolean;
}
export const LoadMoreFooter: React.FC<LoadMoreFooterProps> = ({
isLoadingMore,
hasMore,
}) => {
if (!hasMore) {
return (
<View style={styles.container}>
<View style={styles.noMoreContainer}>
<View style={styles.line} />
<Text style={styles.noMoreText}>--- 暂无更多内容 ---</Text>
<View style={styles.line} />
</View>
</View>
);
}
if (isLoadingMore) {
return (
<View style={styles.container}>
<View style={styles.loadingContainer}>
<ActivityIndicator
size="small"
color={STYLE_CONFIG.COLORS.PRIMARY}
/>
<Text style={styles.loadingText}>加载中...</Text>
</View>
</View>
);
}
return null;
};
const styles = StyleSheet.create({
container: {
paddingVertical: STYLE_CONFIG.SPACING.LG,
backgroundColor: STYLE_CONFIG.COLORS.BACKGROUND,
},
loadingContainer: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
},
loadingText: {
fontSize: STYLE_CONFIG.FONT_SIZE.SM,
color: STYLE_CONFIG.COLORS.TEXT_SECONDARY,
marginLeft: STYLE_CONFIG.SPACING.SM,
},
noMoreContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
noMoreText: {
fontSize: STYLE_CONFIG.FONT_SIZE.SM,
color: STYLE_CONFIG.COLORS.TEXT_TERTIARY,
marginHorizontal: STYLE_CONFIG.SPACING.MD,
},
line: {
flex: 1,
height: StyleSheet.hairlineWidth,
backgroundColor: STYLE_CONFIG.COLORS.BORDER,
maxWidth: 60,
},
});
5.4 空状态组件
typescript
// src/components/RefreshList/EmptyState.tsx
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Image } from 'react-native';
import { STYLE_CONFIG } from '../../utils/constants';
interface EmptyStateProps {
onRefresh?: () => void;
errorMessage?: string;
}
export const EmptyState: React.FC<EmptyStateProps> = ({
onRefresh,
errorMessage,
}) => {
const isError = !!errorMessage;
return (
<View style={styles.container}>
{/* 图标/占位图 */}
<View style={styles.iconContainer}>
<Text style={styles.icon}>{isError ? '⚠️' : '📭'}</Text>
</View>
{/* 主标题 */}
<Text style={styles.title}>
{isError ? '加载失败' : '暂无数据'}
</Text>
{/* 副标题 */}
<Text style={styles.subtitle}>
{isError
? errorMessage || '网络连接出现问题,请检查网络后重试'
: '下拉刷新试试吧~'
}
</Text>
{/* 操作按钮 */}
{isError && onRefresh && (
<TouchableOpacity
style={styles.retryButton}
onPress={onRefresh}
activeOpacity={0.8}
>
<Text style={styles.retryButtonText}>重新加载</Text>
</TouchableOpacity>
)}
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: STYLE_CONFIG.SPACING.XXL,
backgroundColor: STYLE_CONFIG.COLORS.BACKGROUND,
minHeight: 400,
},
iconContainer: {
marginBottom: STYLE_CONFIG.SPACING.LG,
},
icon: {
fontSize: 64,
},
title: {
fontSize: STYLE_CONFIG.FONT_SIZE.LG,
fontWeight: '600',
color: STYLE_CONFIG.COLORS.TEXT_PRIMARY,
marginBottom: STYLE_CONFIG.SPACING.SM,
},
subtitle: {
fontSize: STYLE_CONFIG.FONT_SIZE.SM,
color: STYLE_CONFIG.COLORS.TEXT_SECONDARY,
textAlign: 'center',
marginBottom: STYLE_CONFIG.SPACING.XXL,
},
retryButton: {
backgroundColor: STYLE_CONFIG.COLORS.PRIMARY,
paddingHorizontal: STYLE_CONFIG.SPACING.XXL,
paddingVertical: STYLE_CONFIG.SPACING.MD,
borderRadius: STYLE_CONFIG.BORDER_RADIUS.MD,
},
retryButtonText: {
fontSize: STYLE_CONFIG.FONT_SIZE.MD,
color: '#FFFFFF',
fontWeight: '500',
},
});
5.5 刷新列表主组件
typescript
// src/components/RefreshList/index.tsx
import React, { useRef } from 'react';
import {
View,
FlatList,
StyleSheet,
RefreshControl,
NativeSyntheticEvent,
NativeScrollEvent,
} from 'react-native';
import { ListItem } from '../../types';
import { ListItemComponent } from './ListItem';
import { LoadMoreFooter } from './LoadMoreFooter';
import { EmptyState } from './EmptyState';
import { useListData } from '../../hooks/useListData';
import { LIST_CONFIG } from '../../utils/constants';
interface RefreshListProps {
// 是否使用Mock数据
useMock?: boolean;
// 是否自动加载初始数据
autoLoad?: boolean;
// 列表项点击事件
onItemPress?: (item: ListItem) => void;
// 列表项长按事件
onItemLongPress?: (item: ListItem) => void;
}
export const RefreshList: React.FC<RefreshListProps> = ({
useMock = true,
autoLoad = true,
onItemPress,
onItemLongPress,
}) => {
// 使用列表数据管理Hook
const {
data,
listState,
error,
isRefreshing,
isLoadingMore,
hasMore,
refresh,
loadMore,
retry,
} = useListData({ useMock, autoLoad });
const flatListRef = useRef<FlatList>(null);
/**
* 处理滚动事件,判断是否触发加载更多
*/
const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
const { layoutMeasurement, contentOffset, contentSize } = event.nativeEvent;
// 计算距离底部的高度
const distanceToBottom = contentSize.height - layoutMeasurement.height - contentOffset.y;
// 达到阈值时触发加载更多
if (distanceToBottom < LIST_CONFIG.LOAD_MORE_THRESHOLD * layoutMeasurement.height) {
loadMore();
}
};
/**
* 渲染列表项
*/
const renderItem = ({ item }: { item: ListItem }) => (
<ListItemComponent
item={item}
onPress={onItemPress}
onLongPress={onItemLongPress}
/>
);
/**
* 渲染列表头部
*/
const ListHeaderComponent = (
<View style={styles.header} />
);
/**
* 渲染空状态
*/
const renderEmptyComponent = () => {
if (listState === 'loading') {
return null; // 刷新时由RefreshControl处理
}
return <EmptyState onRefresh={retry} errorMessage={error?.message} />;
};
/**
* 获取列表内容容器样式
*/
const getContentContainerStyle = () => {
if (data.length === 0) {
return styles.emptyContentContainer;
}
return styles.contentContainer;
};
return (
<View style={styles.container}>
<FlatList
ref={flatListRef}
data={data}
renderItem={renderItem}
keyExtractor={(item) => item.id}
contentContainerStyle={getContentContainerStyle()}
ListHeaderComponent={ListHeaderComponent}
ListFooterComponent={
<LoadMoreFooter isLoadingMore={isLoadingMore} hasMore={hasMore} />
}
ListEmptyComponent={renderEmptyComponent()}
refreshControl={
<RefreshControl
refreshing={isRefreshing}
onRefresh={refresh}
colors={['#007AFF']}
tintColor="#007AFF"
title="下拉刷新"
titleColor="#8E8E93"
progressViewOffset={-20}
/>
}
onScroll={handleScroll}
scrollEventThrottle={16}
removeClippedSubviews={true}
maxToRenderPerBatch={10}
updateCellsBatchingPeriod={50}
initialNumToRender={10}
windowSize={10}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F2F2F7',
},
contentContainer: {
paddingVertical: 8,
},
emptyContentContainer: {
flexGrow: 1,
},
header: {
height: 8,
},
});
六、应用入口与页面集成
6.1 应用入口文件
typescript
// App.tsx
import React from 'react';
import {
SafeAreaView,
StatusBar,
StyleSheet,
View,
} from 'react-native';
import { RefreshList } from './src/components/RefreshList';
import { ListItem } from './src/types';
export default function App() {
const handleItemPress = (item: ListItem) => {
console.log('点击项目:', item);
// 可以在这里跳转到详情页
};
const handleItemLongPress = (item: ListItem) => {
console.log('长按项目:', item);
// 可以在这里显示操作菜单
};
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="dark-content" backgroundColor="#FFFFFF" />
{/* 顶部导航栏 */}
<View style={styles.navigationBar}>
<View style={styles.navigationBarContent}>
<View style={styles.statusBarPlaceholder} />
<View style={styles.titleContainer}>
<View style={styles.titleBar} />
</View>
</View>
</View>
{/* 列表内容 */}
<RefreshList
useMock={true}
autoLoad={true}
onItemPress={handleItemPress}
onItemLongPress={handleItemLongPress}
/>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F2F2F7',
},
navigationBar: {
backgroundColor: '#FFFFFF',
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#E5E5EA',
},
navigationBarContent: {
height: 88,
},
statusBarPlaceholder: {
flex: 1,
},
titleContainer: {
height: 44,
justifyContent: 'center',
alignItems: 'center',
},
titleBar: {
width: 100,
height: 20,
backgroundColor: '#E5E5EA',
borderRadius: 4,
},
});
6.2 模拟器预览配置
json
// package.json (添加scripts)
{
"scripts": {
"start": "react-native start",
"android": "react-native run-android",
"ios": "react-native run-ios",
"harmony": "react-native bundle-harmony --dev",
"clean": "react-native clean",
"test": "jest"
}
}
七、鸿蒙平台适配
7.1 鸿蒙工程配置
json5
// harmony/entry/build-profile.json5
{
"apiType": "stageMode",
"buildOption": {
"externalNativeOptions": {
"path": "./src/main/cpp/CMakeLists.txt",
"arguments": "",
"cppFlags": "",
"abiFilters": ["arm64-v8a", "x86_64"]
}
},
"targets": [
{
"name": "default"
}
]
}
7.2 鸿蒙权限配置
json5
// harmony/entry/src/main/module.json5
{
"module": {
"name": "entry",
"type": "entry",
"deviceTypes": ["phone", "tablet"],
"requestPermissions": [
{
"name": "ohos.permission.INTERNET",
"reason": "$string:internet_permission_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
}
}
7.3 部署与验证
bash
# 生成鸿蒙bundle文件
npm run harmony
# 部署到鸿蒙设备
hdc install ./build/outputs/ohos/release/app.hap
# 查看运行日志
hdc shell hilog -x
八、性能优化与最佳实践
8.1 列表渲染优化
typescript
// 优化技巧总结
// 1. 使用React.memo避免不必要的重渲染
export const ListItemComponent = React.memo(({ item }) => {
// ...
}, (prevProps, nextProps) => {
// 自定义比较函数
return prevProps.item.id === nextProps.item.id &&
prevProps.item.timestamp === nextProps.item.timestamp;
});
// 2. 优化keyExtractor
const keyExtractor = (item: ListItem) => {
// 使用稳定的唯一标识,避免使用索引
return item.id;
};
// 3. 虚拟化配置
<FlatList
// 限制渲染数量
maxToRenderPerBatch={10}
// 批量更新间隔
updateCellsBatchingPeriod={50}
// 初始渲染数量
initialNumToRender={10}
// 渲染窗口大小
windowSize={10}
// 移除屏幕外的视图
removeClippedSubviews={true}
/>
// 4. 避免在render中创建函数和对象
const handlePress = useCallback(() => {
// ...
}, [依赖项]);
const renderItem = useCallback(({ item }) => (
<ListItemComponent item={item} onPress={handlePress} />
), [handlePress]);
8.2 网络请求优化
typescript
// 优化技巧总结
// 1. 请求防抖
const debouncedLoadMore = useMemo(
() => debounce(() => loadMore(), 300),
[loadMore]
);
// 2. 请求取消
class HttpClient {
private abortControllers: Map<string, AbortController> = new Map();
async request<T>(key: string, ...args: any[]): Promise<T> {
// 取消之前的请求
const previousController = this.abortControllers.get(key);
if (previousController) {
previousController.abort();
}
// 创建新请求
const controller = new AbortController();
this.abortControllers.set(key, controller);
try {
const response = await fetch(...args, {
signal: controller.signal,
});
return await response.json();
} finally {
this.abortControllers.delete(key);
}
}
}
// 3. 数据预加载
const prefetchNextPage = async (currentPage: number) => {
const nextPage = currentPage + 1;
// 预加载下一页数据
await prefetchQuery(['list', nextPage], () =>
fetchListData(nextPage)
);
};
8.3 内存管理优化
typescript
// 清理资源
useEffect(() => {
return () => {
// 组件卸载时清理
cancelPendingRequests();
clearImageCache();
};
}, []);
// 图片懒加载
const LazyImage: React.FC<{ source: string }> = ({ source }) => {
const [isInView, setIsInView] = useState(false);
const viewRef = useRef<View>(null);
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
setIsInView(true);
observer.disconnect();
}
});
if (viewRef.current) {
// @ts-ignore
observer.observe(viewRef.current);
}
return () => observer.disconnect();
}, []);
return (
<View ref={viewRef}>
{isInView ? <Image source={{ uri: source }} /> : <Placeholder />}
</View>
);
};
九、常见问题与解决方案
9.1 下拉刷新问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 刷新不触发 | threshold设置过大 | 调整RefreshControl的progressViewOffset |
| 刷新动画卡顿 | 主线程阻塞 | 将数据处理移到Web Worker或使用异步操作 |
| 刷新后位置不正确 | 未设置scrollIndex | 刷新成功后使用flatListRef.scrollToOffset |
9.2 上拉加载问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 重复触发加载 | 未设置加载锁 | 使用useRef防止重复调用 |
| 加载不及时 | onEndReachedThreshold过大 | 调整阈值为0.1-0.3 |
| 无法加载到底 | lastItem高度问题 | 确保ListFooterComponent正确渲染 |
9.3 性能问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 列表滑动卡顿 | 渲染项过多 | 启用removeClippedSubviews |
| 内存占用过高 | 图片未释放 | 使用图片缓存库设置最大缓存 |
| 首次渲染慢 | initialNumToRender过大 | 减少初始渲染数量 |
十、工程化交付
10.1 Git提交规范
bash
# 功能开发
git add .
git commit -m "feat(list): 实现下拉刷新与上拉加载功能
- 添加useRefresh和useLoadMore自定义Hooks
- 实现RefreshList主组件及相关子组件
- 支持Mock数据用于开发测试
- 添加完整的错误处理和状态管理"
# 问题修复
git commit -m "fix(list): 修复上拉加载重复触发问题
- 添加loadingLockRef防止重复调用
- 优化loadMore触发条件判断"
# 文档更新
git commit -m "docs(readme): 更新项目运行环境说明"
10.2 README模板
markdown
# RNOH刷新列表示例
React Native for OpenHarmony下拉刷新与上拉加载功能实现。
## 功能特性
- ✅ 下拉刷新数据
- ✅ 上拉加载更多
- ✅ 多状态提示(加载中/失败/空数据)
- ✅ 防抖与节流优化
- ✅ 完整的错误处理
## 运行环境
- Node.js 16+
- React Native 0.72.5
- OpenHarmony SDK API 20+
## 快速开始
\`\`\`bash
# 安装依赖
npm install
# 运行iOS
npm run ios
# 运行Android
npm run android
# 生成鸿蒙bundle
npm run harmony
\`\`\`
## 项目结构
\`\`\`
src/
├── components/ # 组件
├── hooks/ # 自定义Hooks
├── services/ # 服务层
├── types/ # 类型定义
└── utils/ # 工具函数
\`\`\`
## License
MIT
十一、总结
本文详细介绍了在React Native for OpenHarmony项目中实现下拉刷新和上拉加载功能的完整方案,涵盖了:
- 技术背景分析:RNOH技术栈介绍与功能需求拆解
- 项目结构规划:合理的目录组织与模块划分
- 类型系统设计:完整的TypeScript类型定义
- 网络请求封装:可复用的HTTP客户端与API服务
- 自定义Hooks开发:useRefresh、useLoadMore、useListData
- UI组件实现:列表项、刷新头部、加载底部、空状态
- 性能优化:渲染优化、网络优化、内存管理
- 鸿蒙平台适配:配置文件与部署验证
- 工程化实践:Git规范与项目文档
通过这套方案,开发者可以快速构建具备完善数据加载功能的鸿蒙应用,并在真机、模拟器等多设备上验证功能完整性。
参考资源: