基于React Native鸿蒙跨平台PaginationDemo组件中的generateData函数实现了数据的生成和分页逻辑,可通过修改itemsPerPage和totalItems调整分页行为
欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。
在移动应用开发中,分页功能是处理大量数据展示的关键技术之一。本文将深入分析一个功能完备的 React Native 分页组件实现,探讨其架构设计、跨端兼容性以及性能优化策略。
组件化
该实现采用了清晰的组件化设计,将功能拆分为四个主要组件:
- Pagination - 核心分页控制组件,负责显示页码和处理页面切换
- DataItem - 数据项展示组件,负责渲染单个数据项
- SearchBar - 搜索栏组件,提供数据搜索功能
- FilterBar - 筛选组件,提供数据排序功能
这种组件化设计不仅提高了代码的可维护性,也增强了组件的复用性。每个组件都有明确的职责边界,符合单一职责原则。
状态管理
主应用组件 PaginationDemo 使用 useState 钩子管理两个关键状态:
const [currentPage, setCurrentPage] = useState(1);
const [sortBy, setSortBy] = useState('id');
这种状态管理方式简洁高效,通过状态更新触发组件重新渲染,实现了分页和排序功能的联动。
智能页码渲染算法
Pagination 组件实现了一个智能的页码渲染算法,具有以下特点:
- 动态页码范围 - 始终显示当前页附近的页码,最多显示 5 个页码
- 边界处理 - 当接近首页或末页时,自动调整显示的页码范围
- 省略号处理 - 当页码过多时,使用省略号表示隐藏的页码
- 首尾页码固定 - 始终显示第一页和最后一页的页码(当它们不在当前显示范围内时)
这种实现方式既美观又实用,避免了页码过多导致的界面拥挤问题。
导航控制
分页组件提供了直观的导航控制:
- 左侧箭头 - 跳转到上一页
- 右侧箭头 - 跳转到下一页
- 页码按钮 - 直接跳转到指定页码
当到达第一页或最后一页时,相应的导航按钮会被禁用,提供了良好的用户反馈。
数据生成与分页逻辑
PaginationDemo 组件中的 generateData 函数实现了数据的生成和分页逻辑:
const generateData = (page: number, sort: string) => {
const startIndex = (page - 1) * itemsPerPage;
const endIndex = Math.min(startIndex + itemsPerPage, totalItems);
const data = [];
for (let i = startIndex + 1; i <= endIndex; i++) {
data.push({
id: i,
title: `数据项 ${i}`,
description: `这是第 ${i} 个数据项的详细描述,用于演示分页功能。`,
});
}
// 根据排序方式进行排序
if (sort === 'title') {
data.sort((a, b) => a.title.localeCompare(b.title));
} else if (sort === 'date') {
// 模拟
}
};
这种实现方式确保了每次只生成和渲染当前页所需的数据,减少了不必要的计算和渲染,提高了应用性能。
排序功能
FilterBar 组件提供了三种排序方式:
- 按 ID 排序
- 按标题排序
- 按日期排序
用户可以通过点击相应的排序选项来切换排序方式,组件会通过 onSortChange 回调函数通知父组件更新排序状态。
在设计跨端组件时,需要特别关注以下几个方面:
- 组件 API 兼容性 - 确保使用的 React Native 组件在鸿蒙系统上有对应实现
- 样式系统差异 - 不同平台对样式的支持程度不同,需要确保样式在两端都能正常显示
- 触摸事件处理 - 不同平台的触摸事件机制可能存在差异
- 滚动行为 - 不同平台的滚动组件行为可能有所不同
针对上述问题,该实现采用了以下适配策略:
- 使用 React Native 核心组件 - 优先使用 React Native 内置的组件,如 View、Text、TouchableOpacity、ScrollView 等
- 统一的样式定义 - 使用 StyleSheet.create 定义样式,确保样式在不同平台上的一致性
- 平台无关的图标 - 使用 Unicode 表情符号作为图标,避免使用平台特定的图标库
- 简化的交互逻辑 - 使用简单直接的交互逻辑,减少平台差异带来的问题
- 分页渲染 - 只渲染当前页的数据,减少渲染负担
- 条件渲染 - 只在需要时渲染省略号和边界页码
- 组件拆分 - 将复杂的分页逻辑拆分为独立的组件,提高渲染性能
- 水平滚动优化 - 在 FilterBar 组件中使用水平 ScrollView 并禁用水平滚动指示器,提高滚动性能
计算优化
- 页码计算优化 - 智能的页码计算算法,避免不必要的计算
- 数据生成优化 - 按需生成数据,避免一次性生成所有数据
- 排序优化 - 只对当前页的数据进行排序,减少排序开销
该实现采用了高度配置化的设计,通过 props 可以灵活配置各个组件的行为:
- Pagination 组件 - 可配置当前页码、总页数和页码变更回调
- FilterBar 组件 - 可配置当前排序方式和排序变更回调
- 数据生成 - 可通过修改
itemsPerPage和totalItems调整分页行为
这种设计使得组件能够轻松适应各种使用场景,无需修改组件内部实现。
类型安全
使用 TypeScript 类型定义确保了组件 props 的类型安全:
const Pagination = ({
currentPage,
totalPages,
onPageChange
}: {
currentPage: number;
totalPages: number;
onPageChange: (page: number) => void
}) => {
// 组件实现
};
类型定义不仅提高了代码的可读性,也减少了运行时错误的可能性,对于团队协作尤为重要。
分页组件最常见的应用场景是展示大量列表数据,如:
- 商品列表
- 新闻文章列表
- 用户评论列表
- 订单列表
结合搜索和排序功能,可以实现更复杂的数据展示场景:
- 电商平台的商品筛选
- 招聘网站的职位搜索
- 新闻应用的内容分类
在处理大量数据时,分页功能尤为重要,可以显著提高应用性能:
- 大数据报表
- 日志查看器
- 历史记录查询
1. 数据处理
当前实现中,数据是在前端生成的,实际应用中数据通常来自后端 API。建议添加数据获取逻辑:
const fetchData = async (page: number, sort: string) => {
try {
const response = await fetch(`/api/data?page=${page}&sort=${sort}&limit=${itemsPerPage}`);
const data = await response.json();
setData(data.items);
setTotalItems(data.total);
} catch (error) {
console.error('Error fetching data:', error);
}
};
2. 状态管理
对于复杂应用,可以考虑使用 Context API 或状态管理库来管理分页和排序状态:
// 创建分页上下文
const PaginationContext = createContext<{
currentPage: number;
totalPages: number;
sortBy: string;
setCurrentPage: (page: number) => void;
setSortBy: (sort: string) => void;
}>({
currentPage: 1,
totalPages: 1,
sortBy: 'id',
setCurrentPage: () => {},
setSortBy: () => {}
});
// 在应用根组件中提供上下文
const PaginationProvider: React.FC<{children: React.ReactNode}> = ({ children }) => {
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [sortBy, setSortBy] = useState('id');
return (
<PaginationContext.Provider value={{
currentPage,
totalPages,
sortBy,
setCurrentPage,
setSortBy
}}>
{children}
</PaginationContext.Provider>
);
};
3. 动画
可以为分页切换添加动画效果,提升用户体验:
import { Animated } from 'react-native';
const DataItem = ({ id, title, description }: { id: number; title: string; description: string }) => {
const fadeAnim = useRef(new Animated.Value(0)).current;
useEffect(() => {
Animated.timing(fadeAnim, {
toValue: 1,
duration: 500,
useNativeDriver: true
}).start();
}, [fadeAnim]);
return (
<Animated.View style={[styles.dataItem, { opacity: fadeAnim }]}>
<Text style={styles.itemTitle}>{title}</Text>
<Text style={styles.itemDescription}>{description}</Text>
<Text style={styles.itemId}>ID: {id}</Text>
</Animated.View>
);
};
4. 错误处理
添加错误处理机制,提高应用的健壮性:
const PaginationDemo: React.FC = () => {
const [currentPage, setCurrentPage] = useState(1);
const [sortBy, setSortBy] = useState('id');
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const fetchData = async (page: number, sort: string) => {
setLoading(true);
setError(null);
try {
// 数据获取逻辑
} catch (err) {
setError('获取数据失败,请重试');
} finally {
setLoading(false);
}
};
// 渲染逻辑
};
本文深入分析了一个功能完备的 React Native 分页组件实现,从架构设计、状态管理、核心算法到跨端兼容性都进行了详细探讨。该实现不仅功能完整,而且代码结构清晰,具有良好的可扩展性和可维护性。
通过本文的分析,我们可以看到,一个优秀的分页组件不仅需要实现基本的分页功能,还需要考虑性能优化、用户体验和跨端兼容性等多个方面。只有综合考虑这些因素,才能开发出真正高质量的移动应用组件。
你想要深入理解这个 React Native 开发的通用分页组件演示页面的技术实现逻辑,以及如何将其适配到鸿蒙(HarmonyOS)平台。该页面的核心价值在于封装了一个高度智能化的分页控件,具备动态页码渲染、边界处理、分页导航等企业级特性,同时结合了筛选、排序等常见数据列表交互功能,是移动端数据展示类页面的典型范例,下面从 React Native 端实现逻辑、鸿蒙跨端适配要点等维度进行全面解读。
该分页演示页面的核心亮点是智能化分页算法与组件化数据展示的深度结合,打造了一个符合用户体验最佳实践的通用分页解决方案,同时完整展示了数据列表的筛选、排序、分页全流程交互。
1. 组件设计
页面采用清晰的组件分层架构,实现了职责单一化和高度复用性:
- 核心功能组件层:
Pagination:核心分页控件,封装了页码计算、动态渲染、分页导航等所有分页逻辑,通过 props 接收当前页、总页数、页码切换回调;DataItem:数据项展示组件,封装统一的数据项样式,接收 id、标题、描述等数据属性;SearchBar:搜索栏组件,封装搜索框的视觉样式和布局;FilterBar:筛选排序组件,封装排序选项的展示和切换逻辑;
- 页面逻辑层:
PaginationDemo:主页面组件,负责管理分页状态、排序状态,生成模拟数据,处理分页切换和排序切换的业务逻辑;
- 组件通信模式:
- 父传子:主页面向分页组件传递当前页、总页数、页码切换回调;向筛选组件传递当前排序方式、排序切换回调;
- 子调用父:分页组件和筛选组件通过回调函数触发父组件的状态更新,实现页码切换和排序方式切换;
- 单向数据流:所有状态变更均由父组件统一管理,子组件仅负责展示和触发回调,符合 React 设计原则。
2. 智能分页算法
Pagination 组件的核心价值在于其智能化的页码渲染逻辑,解决了大量数据页时页码展示的用户体验问题:
- 核心算法逻辑(
renderPageNumbers方法):- 可见页码限制:设置
maxVisiblePages = 5,控制同时显示的页码数量,避免页码过多导致的布局溢出; - 动态计算起止页:根据当前页动态计算显示的起始页(
startPage = Math.max(1, currentPage - 2))和结束页(endPage = Math.min(totalPages, startPage + maxVisiblePages - 1)),保证当前页始终在可视区域中间位置; - 边界调整:当结束页靠近总页数时,反向调整起始页,保证显示完整的可见页码数量;
- 省略号处理:
- 起始页大于1时,显示第一页并在间隔大于1时显示省略号;
- 结束页小于总页数时,显示最后一页并在间隔大于1时显示省略号;
- 可见页码限制:设置
- 分页导航逻辑:
- 上一页/下一页按钮:根据当前页是否为第一页/最后一页决定是否可点击;
- 页码按钮:当前页高亮显示,其他页码正常显示;
- 不可交互元素:省略号按钮仅作展示,不绑定点击事件。
3. 状态管理
页面采用轻量级状态管理,聚焦于核心业务状态,同时实现了完整的数据处理逻辑:
- 核心状态:
currentPage:当前页码状态,初始值为 1,控制分页组件显示和数据加载;sortBy:排序方式状态,初始值为 ‘id’,控制数据的排序规则;
- 数据生成逻辑(
generateData方法):- 分页数据截取:根据当前页和每页条数(
itemsPerPage = 5)计算数据的起始索引和结束索引,实现数据分页; - 模拟排序逻辑:根据排序方式(id/title/date)对数据进行不同规则的排序,其中 date 排序通过 ID 倒序模拟;
- 边界处理:使用
Math.min处理最后一页的数据边界,避免超出总数据量;
- 分页数据截取:根据当前页和每页条数(
- 状态更新逻辑:
handlePageChange:更新当前页码,触发数据重新加载;handleSortChange:更新排序方式并重置页码为 1,符合用户体验最佳实践(排序后从第一页重新查看)。
页面样式设计遵循移动端数据列表的设计规范,注重视觉层次感和交互体验:
- 视觉分层设计:
- 卡片组件(数据项、搜索栏、筛选栏)使用轻微阴影(
elevation: 1/2)和圆角(borderRadius: 8/12)提升视觉层次感; - 分页组件使用背景色区分当前页(蓝色)和其他页(浅灰),引导用户识别当前位置;
- 排序选项使用背景色区分选中状态(蓝色)和未选中状态(浅灰);
- 卡片组件(数据项、搜索栏、筛选栏)使用轻微阴影(
- 响应式布局:
- 使用
Dimensions.get('window')获取屏幕宽度,保证组件适配不同设备; - 筛选栏的排序选项使用横向滚动(
ScrollView horizontal),适配排序选项较多的场景; - 数据列表使用纵向滚动,保证大量数据时的浏览体验;
- 使用
- 交互反馈设计:
- 可点击元素(页码、排序选项、导航按钮)均有明确的视觉样式区分;
- 禁用状态的导航按钮(第一页的上一页、最后一页的下一页)通过
disabled属性禁用点击事件; - 文本色阶区分:主文本(
#1e293b)、次要文本(#64748b)、辅助文本(#94a3b8)形成清晰的视觉层级。
将该 React Native 分页组件适配到鸿蒙平台,核心是将 React Native 的组件模型、分页算法、样式系统映射到鸿蒙 ArkTS + ArkUI 生态,以下是关键适配步骤和实现方案。
鸿蒙端采用 ArkTS 语言 + ArkUI 组件库,与 React Native 的核心 API 映射关系如下:
| React Native 核心API | 鸿蒙 ArkTS 对应实现 | 适配说明 |
|---|---|---|
useState |
@State/@Link |
状态管理API替换,逻辑一致 |
SafeAreaView |
SafeArea 组件 + safeArea(true) |
安全区域适配 |
View |
Column/Row/Stack |
基础布局组件替换 |
Text |
Text 组件 |
文本组件,属性基本兼容 |
TouchableOpacity |
Button/Text + onClick |
可点击组件替换 |
ScrollView |
Scroll 组件 |
滚动容器替换(纵向/横向) |
StyleSheet |
内联样式 + @Styles/@Extend |
样式体系重构 |
Alert.alert |
promptAction.showToast |
弹窗交互替换(本页未使用) |
Dimensions.get('window') |
viewportWidth/viewportHeight |
屏幕尺寸获取 |
flexDirection: 'row' |
Row 组件 |
横向布局适配 |
flex: 1 |
flex(1) 方法 |
弹性布局适配 |
disabled 属性 |
enabled 属性 |
按钮禁用状态适配 |
showsHorizontalScrollIndicator |
scrollBar(BarState.Off) |
滚动条显示控制 |
鸿蒙实现
// index.ets - 鸿蒙端入口文件
@Entry
@Component
struct PaginationDemo {
// 核心状态 - 对应 React Native 的 useState
@State currentPage: number = 1;
@State sortBy: string = 'id';
private itemsPerPage: number = 5;
private totalItems: number = 23;
private totalPages: number = Math.ceil(this.totalItems / this.itemsPerPage);
// 图标库(完全复用)
private ICONS = {
home: '🏠',
arrowLeft: '⬅️',
arrowRight: '➡️',
more: '…',
search: '🔍',
filter: '🔍',
sort: '📊',
settings: '⚙️',
};
build() {
Column({ space: 0 }) {
this.Header();
this.SearchBar();
this.FilterBar();
this.Content();
this.BottomNav();
}
.width('100%')
.height('100%')
.backgroundColor('#f8fafc')
.safeArea(true);
}
// 通用样式封装
@Styles
cardShadow() {
.shadow({ radius: 2, color: '#000', opacity: 0.1, offsetX: 0, offsetY: 1 });
}
@Styles
smallCardShadow() {
.shadow({ radius: 2, color: '#000', opacity: 0.1, offsetX: 0, offsetY: 1 });
}
// 生成模拟数据
private generateData(page: number, sort: string) {
const startIndex = (page - 1) * this.itemsPerPage;
const endIndex = Math.min(startIndex + this.itemsPerPage, this.totalItems);
const data = [];
for (let i = startIndex + 1; i <= endIndex; i++) {
data.push({
id: i,
title: `数据项 ${i}`,
description: `这是第 ${i} 个数据项的详细描述,用于演示分页功能。`,
});
}
// 根据排序方式进行排序
if (sort === 'title') {
data.sort((a, b) => a.title.localeCompare(b.title));
} else if (sort === 'date') {
// 模拟按日期排序(这里用ID倒序模拟)
data.sort((a, b) => b.id - a.id);
}
return data;
}
// 处理页码切换
private handlePageChange = (page: number) => {
this.currentPage = page;
};
// 处理排序方式切换
private handleSortChange = (sort: string) => {
this.sortBy = sort;
// 当排序方式改变时,重置到第一页
this.currentPage = 1;
};
}
(1)头部、搜索栏、底部导航
// 鸿蒙端头部组件
@Builder
Header() {
Column({ space: 4 }) {
Text('分页组件演示')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#1e293b');
Text('展示分页功能和数据加载')
.fontSize(14)
.fontColor('#64748b');
}
.padding(20)
.backgroundColor('#ffffff')
.borderBottom({ width: 1, color: '#e2e8f0' })
.width('100%');
}
// 鸿蒙端搜索栏组件
@Builder
SearchBar() {
Row({ space: 10 }) {
Text(this.ICONS.search)
.fontSize(18)
.fontColor('#94a3b8');
Text('搜索数据项...')
.fontSize(16)
.fontColor('#94a3b8')
.flex(1);
}
.backgroundColor('#ffffff')
.margin(16)
.padding({ left: 16, right: 16, top: 12, bottom: 12 })
.borderRadius(8)
.smallCardShadow()
.width('100%');
}
// 鸿蒙端底部导航
@Builder
BottomNav() {
Row({ space: 0 }) {
// 首页
this.NavItem(this.ICONS.home, '首页');
// 搜索
this.NavItem(this.ICONS.search, '搜索');
// 筛选
this.NavItem(this.ICONS.filter, '筛选');
// 设置
this.NavItem(this.ICONS.settings, '设置');
}
.backgroundColor('#ffffff')
.borderTop({ width: 1, color: '#e2e8f0' })
.paddingVertical(12)
.justifyContent(FlexAlign.SpaceAround)
.width('100%');
}
// 通用导航项
@Builder
NavItem(icon: string, text: string) {
Button() {
Column({ space: 4 }) {
Text(icon)
.fontSize(20)
.fontColor('#94a3b8');
Text(text)
.fontSize(12)
.fontColor('#94a3b8');
}
}
.backgroundColor(Color.Transparent)
.stateEffect(false)
.flex(1)
.height('auto');
}
(2)筛选排序组件
// 鸿蒙端筛选排序组件
@Builder
FilterBar() {
const sortOptions = [
{ value: 'id', label: '按ID排序' },
{ value: 'title', label: '按标题排序' },
{ value: 'date', label: '按日期排序' },
];
Row({ space: 12 }) {
Text('排序方式:')
.fontSize(14)
.fontColor('#64748b');
Scroll() {
Row({ space: 8 }) {
ForEach(sortOptions, (option) => {
Button(option.label)
.backgroundColor(this.sortBy === option.value ? '#3b82f6' : '#f1f5f9')
.fontSize(12)
.fontColor(this.sortBy === option.value ? '#ffffff' : '#64748b')
.borderRadius(16)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.onClick(() => {
this.handleSortChange(option.value);
})
.stateEffect(true);
});
}
.width('auto');
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.flex(1);
}
.backgroundColor('#ffffff')
.margin({ left: 16, right: 16, bottom: 16 })
.padding({ left: 16, right: 16, top: 12, bottom: 12 })
.borderRadius(8)
.smallCardShadow()
.width('100%');
}
(3)数据列表
// 鸿蒙端主内容区域(数据列表+分页)
@Builder
Content() {
const currentData = this.generateData(this.currentPage, this.sortBy);
Scroll() {
Column({ space: 12 }) {
Text(`数据列表 (第 ${this.currentPage} 页)`)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#1e293b')
.marginBottom(8)
.width('100%');
Text(`共 ${this.totalItems} 项数据,每页 ${this.itemsPerPage} 项`)
.fontSize(14)
.fontColor('#64748b')
.marginBottom(8)
.width('100%');
// 渲染数据项
ForEach(currentData, (item) => {
Column({ space: 4 }) {
Text(item.title)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#1e293b');
Text(item.description)
.fontSize(14)
.fontColor('#64748b')
.lineHeight(20);
Text(`ID: ${item.id}`)
.fontSize(12)
.fontColor('#94a3b8');
}
.backgroundColor('#ffffff')
.borderRadius(12)
.padding(16)
.cardShadow()
.width('100%');
});
// 分页组件
Column() {
this.Pagination(this.currentPage, Math.ceil(this.totalItems / this.itemsPerPage), this.handlePageChange);
}
.marginTop(20)
.alignItems(Alignment.Center)
.width('100%');
}
.padding(16)
.width('100%');
}
.flex(1)
.width('100%');
}
// 鸿蒙端分页组件
@Builder
Pagination(currentPage: number, totalPages: number, onPageChange: (page: number) => void) {
// 渲染页码的核心方法
const renderPageNumbers = () => {
const pages: Array<BuilderNode> = [];
const maxVisiblePages = 5;
let startPage = Math.max(1, currentPage - 2);
let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
if (endPage - startPage < maxVisiblePages - 1) {
startPage = Math.max(1, endPage - maxVisiblePages + 1);
}
// 处理起始页前面的页码和省略号
if (startPage > 1) {
// 第一页
pages.push(
Button('1')
.backgroundColor('#f1f5f9')
.fontSize(14)
.fontColor('#64748b')
.borderRadius(6)
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
.margin(2)
.onClick(() => onPageChange(1))
.stateEffect(true)
);
// 起始省略号
if (startPage > 2) {
pages.push(
Button(this.ICONS.more)
.backgroundColor('#f1f5f9')
.fontSize(14)
.fontColor('#64748b')
.borderRadius(6)
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
.margin(2)
.stateEffect(false)
);
}
}
// 渲染中间的页码
for (let i = startPage; i <= endPage; i++) {
pages.push(
Button(i.toString())
.backgroundColor(currentPage === i ? '#3b82f6' : '#f1f5f9')
.fontSize(14)
.fontColor(currentPage === i ? '#ffffff' : '#64748b')
.borderRadius(6)
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
.margin(2)
.onClick(() => onPageChange(i))
.stateEffect(true)
);
}
// 处理结束页后面的页码和省略号
if (endPage < totalPages) {
// 结束省略号
if (endPage < totalPages - 1) {
pages.push(
Button(this.ICONS.more)
.backgroundColor('#f1f5f9')
.fontSize(14)
.fontColor('#64748b')
.borderRadius(6)
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
.margin(2)
.stateEffect(false)
);
}
// 最后一页
pages.push(
Button(totalPages.toString())
.backgroundColor('#f1f5f9')
.fontSize(14)
.fontColor('#64748b')
.borderRadius(6)
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
.margin(2)
.onClick(() => onPageChange(totalPages))
.stateEffect(true)
);
}
return pages;
};
// 分页容器
Row({ space: 4 }) {
// 上一页按钮
Button(this.ICONS.arrowLeft)
.backgroundColor('#f1f5f9')
.fontSize(16)
.fontColor('#3b82f6')
.borderRadius(6)
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
.enabled(currentPage > 1)
.onClick(() => {
if (currentPage > 1) {
onPageChange(currentPage - 1);
}
})
.stateEffect(true);
// 页码容器
Row({ space: 2 }) {
...renderPageNumbers();
}
.margin({ left: 8, right: 8 });
// 下一页按钮
Button(this.ICONS.arrowRight)
.backgroundColor('#f1f5f9')
.fontSize(16)
.fontColor('#3b82f6')
.borderRadius(6)
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
.enabled(currentPage < totalPages)
.onClick(() => {
if (currentPage < totalPages) {
onPageChange(currentPage + 1);
}
})
.stateEffect(true);
}
.justifyContent(FlexAlign.Center)
.width('100%');
}
(1)智能分页
React Native 的页码渲染算法可 100% 复用到鸿蒙端,仅需将 React 的 JSX 渲染逻辑转换为鸿蒙的 BuilderNode 数组:
// React Native
const pages = [];
pages.push(<TouchableOpacity key={1} ...>...</TouchableOpacity>);
// 鸿蒙
const pages: Array<BuilderNode> = [];
pages.push(Button('1')...);
(2)横向滚动
React Native 的横向 ScrollView 对应鸿蒙的横向 Scroll 组件:
// React Native
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
>...</ScrollView>
// 鸿蒙
Scroll() {
Row() {...}
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
(3)按钮禁用状态
React Native 的 disabled 属性对应鸿蒙的 enabled 属性:
// React Native
<TouchableOpacity
disabled={currentPage === 1}
>...</TouchableOpacity>
// 鸿蒙
Button()
.enabled(currentPage > 1)
(4)循环渲染
React Native 的 map 循环渲染对应鸿蒙的 ForEach 组件:
// React Native
{currentData.map(item => (
<DataItem key={item.id} {...item} />
))}
// 鸿蒙
ForEach(currentData, (item) => {
Column() {...}
});
(5)样式系统
React Native 的 StyleSheet 样式转换为鸿蒙的内联样式 + @Styles 封装:
// React Native
<View style={[styles.pageButton, currentPage === i && styles.currentPageButton]}>
// 鸿蒙
Button()
.backgroundColor(currentPage === i ? '#3b82f6' : '#f1f5f9')
核心业务逻辑完全复用,仅替换平台特定的组件和 API:
- React Native 的
TouchableOpacity.onPress→ 鸿蒙的Button.onClick; - React Native 的状态更新函数(
setCurrentPage)→ 鸿蒙的直接赋值(this.currentPage = page); - React Native 的条件渲染 → 鸿蒙的三元表达式和
enabled属性; - React Native 的数组 map 渲染 → 鸿蒙的
ForEach组件。
-
分页算法复用:智能分页的核心算法(动态计算起止页、省略号处理、边界调整)可 100% 复用到鸿蒙端,这是该组件的核心价值,无需任何修改;
-
状态管理逻辑:分页状态、排序状态的管理逻辑完全复用,仅需将 React 的
useState转换为鸿蒙的@State; -
数据处理逻辑:模拟数据生成、分页数据截取、排序逻辑等业务逻辑完全复用,无需修改;
-
组件设计思想:组件化封装、单向数据流、父传子/子调用父的通信模式完全适配鸿蒙的组件设计思想。
-
视觉一致性:通过精准的样式属性映射(颜色、尺寸、间距、圆角、阴影),保证分页组件在鸿蒙端的视觉效果与 React Native 端完全一致,特别是当前页的高亮样式和排序选项的选中样式;
-
交互体验一致:保留所有核心交互方式(页码点击、上一页/下一页、排序切换),保证用户操作习惯的一致性;
-
算法逻辑纯净:分页算法仅依赖基础的数学计算和数组操作,不依赖任何平台特定 API,保证了极高的复用率;
-
性能优化适配:鸿蒙的
ForEach相比 React Native 的map渲染性能更优,特别是数据量较大时;鸿蒙的Scroll组件滚动性能更流畅。 -
使用
@Styles封装通用的阴影样式,避免重复代码,提升渲染性能; -
分页组件的页码按钮使用
stateEffect(true/false)控制点击反馈,省略号按钮关闭点击反馈,平衡交互体验和性能; -
横向滚动的排序选项使用
scrollBar(BarState.Off)关闭滚动条,提升视觉体验; -
合理使用
flex(1)和width('100%')保证布局的自适应,避免固定尺寸导致的适配问题。
- 核心算法无缝迁移:该分页组件的核心价值——智能页码渲染算法可完全复用到鸿蒙端,这是跨端适配的最大价值所在,保证了不同平台分页体验的一致性;
- 组件化设计适配高效:React Native 的组件化封装思想与鸿蒙的 Builder 模式高度契合,各功能组件可独立适配,降低了整体适配复杂度;
- 状态管理简单适配:轻量级的状态管理模式(仅两个核心状态)使适配工作极为简单,仅需语法层面的转换;
- 交互体验精准还原:通过按钮禁用状态、点击反馈、视觉样式的精准适配,保证了用户交互体验的一致性。
该通用分页组件的跨端适配实践表明,React Native 中基于算法的通用组件向鸿蒙平台迁移时,可实现极高的代码复用率(核心算法 100% 复用),仅需聚焦于平台特定的组件渲染和样式语法适配,即可高效完成跨端迁移,同时保持一致的视觉效果和用户体验。这种适配模式特别适合分页、表格、日历等算法驱动的通用组件。
真实演示案例代码:
// app.tsx
import React, { useState } from 'react';
import { SafeAreaView, View, Text, StyleSheet, TouchableOpacity, ScrollView, Dimensions, Alert } from 'react-native';
// 图标库
const ICONS = {
home: '🏠',
arrowLeft: '⬅️',
arrowRight: '➡️',
more: '…',
search: '🔍',
filter: '🔍',
sort: '📊',
settings: '⚙️',
};
const { width } = Dimensions.get('window');
// 分页组件
const Pagination = ({
currentPage,
totalPages,
onPageChange
}: {
currentPage: number;
totalPages: number;
onPageChange: (page: number) => void
}) => {
const renderPageNumbers = () => {
const pages = [];
const maxVisiblePages = 5;
let startPage = Math.max(1, currentPage - 2);
let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
if (endPage - startPage < maxVisiblePages - 1) {
startPage = Math.max(1, endPage - maxVisiblePages + 1);
}
if (startPage > 1) {
pages.push(
<TouchableOpacity
key={1}
style={styles.pageButton}
onPress={() => onPageChange(1)}
>
<Text style={styles.pageText}>1</Text>
</TouchableOpacity>
);
if (startPage > 2) {
pages.push(
<TouchableOpacity key="ellipsisStart" style={styles.pageButton}>
<Text style={styles.pageText}>{ICONS.more}</Text>
</TouchableOpacity>
);
}
}
for (let i = startPage; i <= endPage; i++) {
pages.push(
<TouchableOpacity
key={i}
style={[
styles.pageButton,
currentPage === i && styles.currentPageButton
]}
onPress={() => onPageChange(i)}
>
<Text style={[
styles.pageText,
currentPage === i && styles.currentPageText
]}>{i}</Text>
</TouchableOpacity>
);
}
if (endPage < totalPages) {
if (endPage < totalPages - 1) {
pages.push(
<TouchableOpacity key="ellipsisEnd" style={styles.pageButton}>
<Text style={styles.pageText}>{ICONS.more}</Text>
</TouchableOpacity>
);
}
pages.push(
<TouchableOpacity
key={totalPages}
style={styles.pageButton}
onPress={() => onPageChange(totalPages)}
>
<Text style={styles.pageText}>{totalPages}</Text>
</TouchableOpacity>
);
}
return pages;
};
return (
<View style={styles.paginationContainer}>
<TouchableOpacity
style={styles.navigationButton}
onPress={() => currentPage > 1 && onPageChange(currentPage - 1)}
disabled={currentPage === 1}
>
<Text style={styles.navigationText}>{ICONS.arrowLeft}</Text>
</TouchableOpacity>
<View style={styles.pageNumbersContainer}>
{renderPageNumbers()}
</View>
<TouchableOpacity
style={styles.navigationButton}
onPress={() => currentPage < totalPages && onPageChange(currentPage + 1)}
disabled={currentPage === totalPages}
>
<Text style={styles.navigationText}>{ICONS.arrowRight}</Text>
</TouchableOpacity>
</View>
);
};
// 数据项组件
const DataItem = ({ id, title, description }: { id: number; title: string; description: string }) => {
return (
<View style={styles.dataItem}>
<Text style={styles.itemTitle}>{title}</Text>
<Text style={styles.itemDescription}>{description}</Text>
<Text style={styles.itemId}>ID: {id}</Text>
</View>
);
};
// 搜索栏组件
const SearchBar = () => {
return (
<View style={styles.searchBar}>
<Text style={styles.searchIcon}>{ICONS.search}</Text>
<Text style={styles.searchPlaceholder}>搜索数据项...</Text>
</View>
);
};
// 筛选组件
const FilterBar = ({
sortBy,
onSortChange
}: {
sortBy: string;
onSortChange: (sort: string) => void
}) => {
const sortOptions = [
{ value: 'id', label: '按ID排序' },
{ value: 'title', label: '按标题排序' },
{ value: 'date', label: '按日期排序' },
];
return (
<View style={styles.filterContainer}>
<Text style={styles.filterLabel}>排序方式:</Text>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
style={styles.sortOptionsContainer}
>
{sortOptions.map(option => (
<TouchableOpacity
key={option.value}
style={[
styles.sortOption,
sortBy === option.value && styles.activeSortOption
]}
onPress={() => onSortChange(option.value)}
>
<Text style={[
styles.sortOptionText,
sortBy === option.value && styles.activeSortOptionText
]}>
{option.label}
</Text>
</TouchableOpacity>
))}
</ScrollView>
</View>
);
};
const PaginationDemo: React.FC = () => {
const [currentPage, setCurrentPage] = useState(1);
const [sortBy, setSortBy] = useState('id');
const itemsPerPage = 5;
const totalItems = 23;
const totalPages = Math.ceil(totalItems / itemsPerPage);
// 生成模拟数据
const generateData = (page: number, sort: string) => {
const startIndex = (page - 1) * itemsPerPage;
const endIndex = Math.min(startIndex + itemsPerPage, totalItems);
const data = [];
for (let i = startIndex + 1; i <= endIndex; i++) {
data.push({
id: i,
title: `数据项 ${i}`,
description: `这是第 ${i} 个数据项的详细描述,用于演示分页功能。`,
});
}
// 根据排序方式进行排序
if (sort === 'title') {
data.sort((a, b) => a.title.localeCompare(b.title));
} else if (sort === 'date') {
// 模拟按日期排序(这里用ID倒序模拟)
data.sort((a, b) => b.id - a.id);
}
return data;
};
const currentData = generateData(currentPage, sortBy);
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
const handleSortChange = (sort: string) => {
setSortBy(sort);
// 当排序方式改变时,重置到第一页
setCurrentPage(1);
};
return (
<SafeAreaView style={styles.container}>
{/* 头部 */}
<View style={styles.header}>
<Text style={styles.title}>分页组件演示</Text>
<Text style={styles.subtitle}>展示分页功能和数据加载</Text>
</View>
{/* 搜索栏 */}
<SearchBar />
{/* 筛选栏 */}
<FilterBar sortBy={sortBy} onSortChange={handleSortChange} />
{/* 数据列表 */}
<ScrollView style={styles.content}>
<Text style={styles.sectionTitle}>数据列表 (第 {currentPage} 页)</Text>
<Text style={styles.itemsCount}>共 {totalItems} 项数据,每页 {itemsPerPage} 项</Text>
{currentData.map(item => (
<DataItem
key={item.id}
id={item.id}
title={item.title}
description={item.description}
/>
))}
{/* 分页组件 */}
<View style={styles.paginationWrapper}>
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={handlePageChange}
/>
</View>
</ScrollView>
{/* 底部导航 */}
<View style={styles.bottomNav}>
<TouchableOpacity style={styles.navItem}>
<Text style={styles.navIcon}>{ICONS.home}</Text>
<Text style={styles.navText}>首页</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.navItem}>
<Text style={styles.navIcon}>{ICONS.search}</Text>
<Text style={styles.navText}>搜索</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.navItem}>
<Text style={styles.navIcon}>{ICONS.filter}</Text>
<Text style={styles.navText}>筛选</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.navItem}>
<Text style={styles.navIcon}>{ICONS.settings}</Text>
<Text style={styles.navText}>设置</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8fafc',
},
header: {
padding: 20,
backgroundColor: '#ffffff',
borderBottomWidth: 1,
borderBottomColor: '#e2e8f0',
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#1e293b',
marginBottom: 4,
},
subtitle: {
fontSize: 14,
color: '#64748b',
},
searchBar: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#ffffff',
margin: 16,
paddingHorizontal: 16,
paddingVertical: 12,
borderRadius: 8,
elevation: 1,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
},
searchIcon: {
fontSize: 18,
color: '#94a3b8',
marginRight: 10,
},
searchPlaceholder: {
fontSize: 16,
color: '#94a3b8',
flex: 1,
},
filterContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#ffffff',
marginHorizontal: 16,
marginBottom: 16,
paddingHorizontal: 16,
paddingVertical: 12,
borderRadius: 8,
elevation: 1,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
},
filterLabel: {
fontSize: 14,
color: '#64748b',
marginRight: 12,
},
sortOptionsContainer: {
flex: 1,
},
sortOption: {
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 16,
backgroundColor: '#f1f5f9',
marginRight: 8,
},
activeSortOption: {
backgroundColor: '#3b82f6',
},
sortOptionText: {
fontSize: 12,
color: '#64748b',
},
activeSortOptionText: {
color: '#ffffff',
},
content: {
flex: 1,
padding: 16,
},
sectionTitle: {
fontSize: 18,
fontWeight: 'bold',
color: '#1e293b',
marginBottom: 8,
},
itemsCount: {
fontSize: 14,
color: '#64748b',
marginBottom: 16,
},
dataItem: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 16,
marginBottom: 12,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
},
itemTitle: {
fontSize: 16,
fontWeight: 'bold',
color: '#1e293b',
marginBottom: 4,
},
itemDescription: {
fontSize: 14,
color: '#64748b',
lineHeight: 20,
marginBottom: 4,
},
itemId: {
fontSize: 12,
color: '#94a3b8',
},
paginationWrapper: {
marginTop: 20,
alignItems: 'center',
},
paginationContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
navigationButton: {
paddingHorizontal: 12,
paddingVertical: 8,
backgroundColor: '#f1f5f9',
borderRadius: 6,
marginHorizontal: 4,
},
navigationText: {
fontSize: 16,
color: '#3b82f6',
},
pageNumbersContainer: {
flexDirection: 'row',
marginHorizontal: 8,
},
pageButton: {
paddingHorizontal: 12,
paddingVertical: 8,
backgroundColor: '#f1f5f9',
borderRadius: 6,
marginHorizontal: 2,
},
currentPageButton: {
backgroundColor: '#3b82f6',
},
pageText: {
fontSize: 14,
color: '#64748b',
},
currentPageText: {
color: '#ffffff',
},
bottomNav: {
flexDirection: 'row',
justifyContent: 'space-around',
backgroundColor: '#ffffff',
borderTopWidth: 1,
borderTopColor: '#e2e8f0',
paddingVertical: 12,
},
navItem: {
alignItems: 'center',
},
navIcon: {
fontSize: 20,
color: '#94a3b8',
marginBottom: 4,
},
navText: {
fontSize: 12,
color: '#94a3b8',
},
});
export default PaginationDemo;

打包
接下来通过打包命令npn run harmony将reactNative的代码打包成为bundle,这样可以进行在开源鸿蒙OpenHarmony中进行使用。

打包之后再将打包后的鸿蒙OpenHarmony文件拷贝到鸿蒙的DevEco-Studio工程目录去:

最后运行效果图如下显示:

本文深入解析了一个功能完备的React Native分页组件实现,重点介绍了其架构设计和性能优化策略。该组件采用模块化设计,分为Pagination、DataItem、SearchBar和FilterBar四个核心组件,通过useState管理分页和排序状态。智能页码渲染算法动态显示5个页码,并处理边界情况。组件实现了数据分页生成、多种排序方式,并针对跨平台兼容性进行了优化,使用核心RN组件和统一样式。性能方面通过条件渲染、按需数据生成和计算优化提升效率。文章还提出了结合后端API、状态管理库和动画效果的改进建议,适用于电商、新闻等多种数据展示场景。
欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。
更多推荐


所有评论(0)