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

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项目中实现下拉刷新和上拉加载功能的完整方案,涵盖了:

  1. 技术背景分析:RNOH技术栈介绍与功能需求拆解
  2. 项目结构规划:合理的目录组织与模块划分
  3. 类型系统设计:完整的TypeScript类型定义
  4. 网络请求封装:可复用的HTTP客户端与API服务
  5. 自定义Hooks开发:useRefresh、useLoadMore、useListData
  6. UI组件实现:列表项、刷新头部、加载底部、空状态
  7. 性能优化:渲染优化、网络优化、内存管理
  8. 鸿蒙平台适配:配置文件与部署验证
  9. 工程化实践:Git规范与项目文档

通过这套方案,开发者可以快速构建具备完善数据加载功能的鸿蒙应用,并在真机、模拟器等多设备上验证功能完整性。


参考资源:

相关推荐
小哥Mark2 小时前
Flutter for OpenHarmony年味+实用实战应用|搭建【多 Tab 应用】基础工程 + 实现【弧形底部导航】
flutter·harmonyos·鸿蒙
江湖有缘3 小时前
基于华为openEuler部署WikiDocs文档管理系统
linux·华为
摘星编程3 小时前
React Native + OpenHarmony:Stepper步进器组件
javascript·react native·react.js
●VON3 小时前
React Native for OpenHarmony:简易计算器应用的开发与跨平台适配实践
javascript·react native·react.js
摘星编程10 小时前
OpenHarmony + RN:Placeholder文本占位
javascript·react native·react.js
摘星编程11 小时前
React Native鸿蒙:Loading加载动画效果
react native·react.js·harmonyos
Swift社区12 小时前
HarmonyOS 页面路由与导航开发
华为·harmonyos
以太浮标12 小时前
华为eNSP模拟器综合实验之- VLAN终结实践案例分析
网络·计算机网络·华为·智能路由器
摘星编程13 小时前
React Native + OpenHarmony:Spinner旋转加载器
javascript·react native·react.js