欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。
开源鸿蒙跨平台开发者社区诚邀开发者共建生态,提供React Native侧边栏组件代码示例。该组件包含可折叠侧边栏功能,支持自定义图标(以Base64格式SVG实现)、徽章提示和回调事件。示例代码展示了如何实现包含首页、用户、设置等菜单项的侧边栏,使用TypeScript接口定义数据结构,支持头部和底部自定义内容。通过Animated实现平滑动画效果,适合跨平台应用开发。欢迎访问社区官网参与鸿蒙生态建设。
React Native 侧边导航组件设计与鸿蒙跨端适配深度解析
在移动跨端开发领域,侧边导航(Sidebar)作为应用核心的导航载体,其交互体验与跨端适配能力直接决定了应用的整体使用感受。本文以一个功能完整的 React Native 侧边导航组件为例,从组件架构设计、核心功能实现、TypeScript 类型约束、动画交互优化等维度展开深度解读,并系统探讨该组件向鸿蒙(HarmonyOS)跨端适配的核心技术路径,为跨端导航组件开发提供可落地的实践参考。
一、组件整体架构与设计理念
该 React Native 侧边导航组件采用「容器-内容-交互」三层架构设计,遵循 React 组件化思想与跨端开发的「逻辑复用、UI 适配」核心原则,整体结构可拆解为:基础常量层(图标资源)、类型定义层(TypeScript 接口)、核心逻辑层(Sidebar 组件)、示例展示层(SidebarComponentApp 容器)、样式层(StyleSheet)。
这种分层设计的核心优势在于:将导航渲染、折叠动画、选中状态管理等核心逻辑与 UI 样式、平台特定 API 解耦,既保证了 React Native 端的高性能运行,也为鸿蒙跨端适配提供了清晰的逻辑复用路径。组件具备可折叠、徽章数字提示、选中状态高亮、自定义样式、头部/底部插槽等企业级应用所需的核心功能,完全满足中后台管理系统、电商应用等场景的导航需求。
二、React Native 端核心技术实现解析
1. TypeScript 类型约束:构建健壮的组件接口
作为企业级组件,类型安全是保证代码可维护性的基础,该组件通过 TypeScript 接口定义实现了全链路的类型约束,核心类型设计如下:
(1)导航项类型定义
typescript
// Sidebar Item Interface
interface SidebarItem {
id: string;
title: string;
icon: keyof typeof SIDEBAR_ICONS;
badge?: number;
}
SidebarItem 接口精准定义了导航项的核心属性:id 作为唯一标识保证列表渲染的稳定性;title 为导航文本;icon 通过 keyof typeof SIDEBAR_ICONS 实现「图标键值」的强类型约束,避免传入不存在的图标名称;badge 为可选属性,用于消息数字提示,这种可选属性设计既满足了功能灵活性,又通过类型约束避免了非法值传入。
(2)组件 Props 类型定义
typescript
// Sidebar Component
interface SidebarProps {
items: SidebarItem[];
selectedIndex: number;
onSelect: (index: number) => void;
header?: React.ReactNode;
footer?: React.ReactNode;
collapsible?: boolean;
width?: number;
backgroundColor?: string;
activeBackgroundColor?: string;
textColor?: string;
activeTextColor?: string;
}
SidebarProps 接口覆盖了组件的所有配置项:必选属性 items(导航列表)、selectedIndex(当前选中索引)、onSelect(选中回调)保证组件的核心功能;可选属性 header/footer 采用 React.ReactNode 类型,支持传入任意 React 节点,实现头部/底部内容的自定义插槽;collapsible 控制折叠功能开关,各类样式属性(如 backgroundColor)则通过可选字符串类型,支持组件样式的个性化定制。
TypeScript 的强类型约束在该组件中不仅避免了「传参类型错误」「属性不存在」等运行时错误,也为开发者提供了清晰的接口提示,同时为鸿蒙跨端适配时的接口对齐提供了明确的参考标准。
2. 图标资源处理:Base64 内嵌的跨端优势
组件中所有导航图标均采用 Base64 编码内嵌的方式存储在 SIDEBAR_ICONS 对象中,而非传统的本地图片或远程图片:
typescript
// Base64 Icons for Sidebar component
const SIDEBAR_ICONS = {
home: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJMMiA5TDQgOWMxLjEgMCAyIC45IDIgMnY3YzAgMS4xLjkgMiAyIDJoMTJjMS4xIDAgMi0uOSAyLTJ2LTdjMC0xLjEuOS0yIDItMmgxbC0xMC03WiIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiLz4KPHBhdGggZD0iTTE2IDE1SDgiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+CjxwYXRoIGQ9Ik0xNiAxMVg4IiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8L3N2Zz4K',
// 其他图标省略...
};
这种处理方式在跨端开发中具备三大核心优势:
- 资源路径无关性 :React Native 端无需处理 iOS/Android 不同分辨率图片的命名规范(如
@2x/@3x),鸿蒙端也无需配置资源目录,只需解析 Base64 字符串即可渲染图标; - 加载性能优化:Base64 图标随 JS 代码一同加载,避免了图片的异步请求,减少了组件渲染的白屏时间;
- 跨端兼容性 :SVG 格式的 Base64 图标在 React Native 和鸿蒙端均具备良好的渲染支持,且支持通过
tintColor(React Native)/fill(鸿蒙)动态修改图标颜色,满足选中/未选中状态的样式切换需求。
3. 折叠动画实现:Animated 驱动的流畅交互
组件的折叠功能基于 React Native 内置的 Animated 库实现,核心逻辑如下:
typescript
const [collapsed, setCollapsed] = useState(false);
const animatedWidth = useRef(new Animated.Value(width)).current;
const toggleCollapse = () => {
if (collapsible) {
const newCollapsed = !collapsed;
setCollapsed(newCollapsed);
Animated.spring(animatedWidth, {
toValue: newCollapsed ? 80 : width,
useNativeDriver: false,
}).start();
}
};
- 动画值管理 :通过
useRef保存Animated.Value实例,避免组件重渲染时重复创建,保证动画状态的连续性; - 弹簧动画配置 :使用
Animated.spring而非Animated.timing,模拟物理弹簧的弹性效果,让侧边栏的展开/折叠更符合真实世界的交互体验; - 原生驱动限制 :由于动画目标是
width属性(非布局属性),因此设置useNativeDriver: false,这是 React Native 动画开发中的常见细节,若错误设置为true会导致动画失效。
动画值最终绑定到侧边栏容器的宽度属性上:
typescript
<Animated.View
style={[
styles.sidebarContainer,
{
width: animatedWidth,
backgroundColor
}
]}
>
{/* 导航内容 */}
</Animated.View>
这种基于声明式动画的实现方式,相较于原生开发的 imperative 动画,更符合 React 的开发范式,且动画逻辑与 UI 渲染解耦,便于跨端适配时替换为鸿蒙的动画 API。
4. 核心渲染逻辑:状态驱动的条件渲染
组件的导航项渲染、选中状态高亮、徽章显示等核心功能均基于 React 的状态驱动设计:
(1)导航项渲染与选中状态
typescript
const renderItem = (item: SidebarItem, index: number) => {
const isActive = index === selectedIndex;
return (
<TouchableOpacity
key={item.id}
style={[
styles.sidebarItem,
isActive && styles.activeSidebarItem,
{ backgroundColor: isActive ? activeBackgroundColor : 'transparent' }
]}
onPress={() => onSelect(index)}
>
<View style={styles.itemIconContainer}>
<Image
source={{ uri: SIDEBAR_ICONS[item.icon] }}
style={[
styles.itemIcon,
{ tintColor: isActive ? activeTextColor : textColor }
]}
/>
{renderBadge(item.badge || 0)}
</View>
{!collapsed && (
<Text style={[
styles.itemText,
{ color: isActive ? activeTextColor : textColor }
]}>
{item.title}
</Text>
)}
{!collapsed && isActive && (
<View style={styles.activeIndicator} />
)}
</TouchableOpacity>
);
};
核心设计要点:
- 选中状态联动 :通过
isActive = index === selectedIndex判断当前项是否选中,联动修改背景色、图标颜色、文本颜色,实现视觉上的高亮反馈; - 折叠状态条件渲染 :折叠状态下(
collapsed = true)隐藏导航文本和选中指示器,仅保留图标,最大化屏幕空间利用率; - TouchableOpacity 封装 :使用
TouchableOpacity包裹导航项,既实现了点击交互,又自带点击透明度变化的反馈效果,提升用户体验。
(2)徽章数字渲染
typescript
const renderBadge = (badgeCount: number) => {
if (!badgeCount) return null;
return (
<View style={styles.badgeContainer}>
<View style={styles.badge}>
<Text style={styles.badgeText}>
{badgeCount > 99 ? '99+' : badgeCount}
</Text>
</View>
</View>
);
};
徽章组件采用「条件渲染 + 数字格式化」的设计:无数字时不渲染,数字超过99时显示「99+」,避免徽章尺寸过大影响UI美观;通过绝对定位(badgeContainer 的 position: 'absolute')将徽章置于图标右上角,符合移动端导航的交互习惯。
5. 样式设计:Flex 布局的跨端适配基础
组件样式基于 React Native 的 StyleSheet.create 构建,核心采用 Flex 布局:
typescript
const styles = StyleSheet.create({
sidebarContainer: {
height: '100%',
borderRightWidth: 1,
borderRightColor: '#1e293b',
},
sidebarItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 16,
paddingHorizontal: 20,
position: 'relative',
},
// 其他样式省略...
});
Flex 布局是 React Native、鸿蒙、Web 等多端通用的布局标准,该组件的样式设计完全基于 Flex 体系:
- 横向排列 :导航项通过
flexDirection: 'row'实现图标与文本的横向排列; - 垂直居中 :
alignItems: 'center'保证导航项内容垂直居中; - 高度自适应 :侧边栏容器设置
height: '100%',适配不同设备的屏幕高度。
这种样式设计为鸿蒙跨端适配提供了极大便利,只需将 React Native 的 StyleSheet 属性映射为鸿蒙的 ComponentStyle 属性,即可快速实现样式迁移。
三、鸿蒙跨端适配核心技术路径
1. 技术栈映射:React Native 与鸿蒙 ArkTS 核心对应关系
要实现组件的跨端适配,首先需明确 React Native 与鸿蒙 ArkTS 的核心技术栈映射关系,这是逻辑复用和 UI 迁移的基础:
| React Native 技术点 | 鸿蒙 ArkTS 对应技术点 | 适配说明 |
|---|---|---|
| 函数式组件 + Props | 结构化组件 + @Prop/@Link | React 的 Props 对应鸿蒙的 @Prop(单向传递)/@Link(双向绑定) |
| useState/useRef | @State/@Ref | React 的状态钩子对应鸿蒙的状态装饰器,useRef 对应 @Ref |
| Animated 动画 | Animator 动画 | React Native 的声明式动画对应鸿蒙的属性动画/显式动画 |
| View/Text/Image | Column/Text/Image | 基础UI组件一一对应,功能完全兼容 |
| TouchableOpacity | Button + onClick / GestureDetector | 点击交互可通过 Button 组件或手势检测器实现 |
| StyleSheet | ComponentStyle + 内联样式 | Flex 布局属性完全通用,样式属性名称略有差异(如 backgroundColor 对应 background.color) |
| ScrollView | Scroll | 滚动容器功能一致,API 设计略有不同 |
2. 核心逻辑复用:抽离无平台依赖的纯函数
跨端适配的核心原则是「逻辑复用,UI 重写」,该组件中可直接复用的纯逻辑包括:导航项渲染逻辑、徽章数字格式化、折叠状态判断等,这些逻辑不依赖任何 React Native 特定 API,只需少量修改即可在鸿蒙端运行。
(1)类型定义复用(适配鸿蒙 ArkTS)
typescript
// 鸿蒙 ArkTS 类型定义(复用 React Native 端的类型逻辑)
type SIDEBAR_ICONS_KEY = 'home' | 'user' | 'settings' | 'message' | 'chart' | 'folder' | 'calendar' | 'help' | 'logout';
interface SidebarItem {
id: string;
title: string;
icon: SIDEBAR_ICONS_KEY;
badge?: number;
}
interface SidebarProps {
items: SidebarItem[];
selectedIndex: number;
onSelect: (index: number) => void;
header?: ReactNode;
footer?: ReactNode;
collapsible?: boolean;
width?: number;
backgroundColor?: string;
activeBackgroundColor?: string;
textColor?: string;
activeTextColor?: string;
}
鸿蒙 ArkTS 同样支持 TypeScript 类型语法,只需将 keyof typeof SIDEBAR_ICONS 替换为显式的联合类型(鸿蒙对 keyof 的支持与 React Native 一致,此处为兼容性优化),即可实现类型约束的复用。
(2)徽章渲染逻辑复用
typescript
// 鸿蒙 ArkTS 徽章渲染逻辑(完全复用 React Native 端代码)
@Builder
renderBadge(badgeCount: number) {
if (!badgeCount) return;
Column() {
Text(badgeCount > 99 ? '99+' : badgeCount.toString())
.fontSize(10)
.fontWeight(FontWeight.Bold)
.fontColor('#ffffff');
}
.backgroundColor('#ef4444')
.borderRadius(10)
.minWidth(20)
.height(20)
.justifyContent(FlexAlign.Center)
.alignItems(ItemAlign.Center)
.padding({ left: 6, right: 6 })
.position({ top: -6, right: -6 });
}
鸿蒙的 @Builder 装饰器对应 React 的自定义渲染函数,核心逻辑(数字判断、格式化、样式)完全复用,仅需将 React Native 的 View/Text 替换为鸿蒙的 Column/Text,样式属性调整为鸿蒙的 API 格式。
3. 动画交互适配:鸿蒙 Animator 替代 Animated
React Native 的 Animated 动画在鸿蒙端可通过 Animator 动画实现,核心适配逻辑如下:
(1)动画状态管理
typescript
// 鸿蒙 ArkTS 动画状态管理
@State collapsed: boolean = false;
@State sidebarWidth: number = 280; // 初始宽度
@Ref animator: Animator = new Animator();
// 初始化动画配置
aboutToAppear() {
this.animator = new Animator({
duration: 300,
easing: Easing.Spring(0.8, 100), // 模拟弹簧效果
fill: FillMode.Forwards,
});
}
// 折叠/展开切换逻辑
toggleCollapse() {
if (!this.collapsible) return;
this.collapsed = !this.collapsed;
const targetWidth = this.collapsed ? 80 : 280;
// 绑定动画目标属性
this.animator.update({
values: [this.sidebarWidth, targetWidth],
onUpdate: (value: number) => {
this.sidebarWidth = value;
},
});
this.animator.play();
}
鸿蒙的 Animator 采用命令式 API,相较于 React Native 的声明式 Animated,核心差异在于:
- 动画实例需手动创建和配置,通过
update方法更新动画目标值; - 通过
onUpdate回调更新组件状态,驱动 UI 重渲染; - 弹簧效果通过
Easing.Spring配置,参数(阻尼、刚度)可精细调整动画的弹性效果。
(2)动画绑定到UI
typescript
// 鸿蒙 ArkTS 动画绑定
Column() {
// 导航内容
}
.width(this.sidebarWidth)
.height('100%')
.backgroundColor(this.backgroundColor)
.borderRight({ width: 1, color: '#1e293b' });
将动画驱动的 sidebarWidth 绑定到 Column 容器的宽度属性,实现与 React Native 端一致的折叠/展开效果。
4. 完整鸿蒙适配示例(核心组件)
以下是该侧边导航组件在鸿蒙 ArkTS 中的完整适配实现,保留了 React Native 端的所有核心功能:
typescript
import { ReactNode } from 'react';
import { Column, Row, Text, Image, TouchableOpacity, Scroll, StyleSheet, Animator, Easing, FillMode, FlexAlign, ItemAlign, Position } from '@ohos/react';
// Base64 Icons(完全复用 React Native 端)
const SIDEBAR_ICONS = {
home: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJMMiA5TDQgOWMxLjEgMCAyIC45IDIgMnY3YzAgMS4xLjkgMiAyIDJoMTJjMS4xIDAgMi0uOSAyLTJ2LTdjMC0xLjEuOS0yIDItMmgxbC0xMC03WiIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiLz4KPHBhdGggZD0iTTE2IDE1SDgiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+CjxwYXRoIGQ9Ik0xNiAxMVg4IiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8L3N2Zz4K',
user: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJDNy41OCAyIDQgNS41OCA0IDEwdjRjMCA0LjQyIDMuNTggOCA4IDhzOCAzLjU4IDggOHYtNGMwLTQuNDItMy41OC04LTgtOFYxMFoiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+CjxwYXRoIGQ9Ik04IDEwYzAtMi4yIDEuOC00IDQtNCAyLjIgMCA0IDEuOCA0IDQgMCAyLjItMS44IDQtNCA0cy00LTEuOC00LTRaIiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8L3N2Zz4K',
settings: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJaIiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8cGF0aCBkPSJNMTIgNHYxNCIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8cGF0aCBkPSJNNyAxMmgxMCIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K',
message: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0IDJIMTBjLTEuMSAwLTIgLjktMiAydjRoLTJjLTEuMSAwLTIgLjktMiAydjEwYzAgMS4xLjkgMiAyIDJoMTJjMS4xIDAgMi0uOSAyLTJWNmMwLTEuMS0uOS0yLTItMmgtMlYyem0wIDR2MmgydjJINHYtNFoiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+Cjwvc3ZnPgo=',
chart: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJMNiA4aDR2MTJINnoiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+CjxwYXRoIGQ9Ik0xOCA4aC00djEyaDR6IiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8L3N2Zz4K',
folder: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEwIDRINmEyIDIgMCAwIDAtMiAydjEyaDE2YTIgMiAwIDAgMi0yLTIiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+CjxwYXRoIGQ9Ik0yMiA4LjV2LTIuN2EyLjIgMi4yIDAgMCAwLS43LTEuN2wtNS40LTIuNmEyLjIgMi4yIDAgMCAwLTEuNy0uN0gyYTIgMiAwIDAgMC0yIDJ2MTJhMiAyIDAgMCAwIDIgMmg3IiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8L3N2Zz4K',
calendar: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE5IDRINWMtMS4xIDAtMiAuOS0yIDJ2MTRjMCAxLjEuOSAyIDIgMmgxNGMxLjEgMCAyLS45IDItMlY2YzAtMS4xLS45LTItMi0yem0wIDE2SDVWNmgxNHYxNHoiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+CjxwYXRoIGQ9Ik0xNiAydjRoIiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8cGF0aCBkPSI4IDJ2NCIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiLz4KPHBhdGggZD0iMyAxMGgxOCIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiLz4KPC9zdmc+Cg==',
help: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJaIiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMjIi8+CjxwYXRoIGQ9Ik0xMiAxNnYzIi8+CjxwYXRoIGQ9Ik0xMiA4aDAuMDEiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+Cjwvc3ZnPgo=',
logout: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE1IDN2NGgyYTEgMSAwIDAgMSAxIDF2OGExIDEgMCAwIDEtMSAxaC0yYTIgMiAwIDAgMCAwIDRoM2EyIDIgMCAwIDAgMi0yVjVhMiAyIDAgMCAwLTItMmgtM3oiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+CjxwYXRoIGQ9Ik04IDEydjRsLTQtNC00IDR2LTRhMiAyIDAgMCAxIDItMmg4YTIgMiAwIDAgMSAyIDJ6IiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8L3N2Zz4K'
};
// 类型定义(适配鸿蒙 ArkTS)
type SIDEBAR_ICONS_KEY = keyof typeof SIDEBAR_ICONS;
interface SidebarItem {
id: string;
title: string;
icon: SIDEBAR_ICONS_KEY;
badge?: number;
}
interface SidebarProps {
items: SidebarItem[];
selectedIndex: number;
onSelect: (index: number) => void;
header?: ReactNode;
footer?: ReactNode;
collapsible?: boolean;
width?: number;
backgroundColor?: string;
activeBackgroundColor?: string;
textColor?: string;
activeTextColor?: string;
}
// 鸿蒙 Sidebar 组件
const Sidebar: React.FC<SidebarProps> = ({
items,
selectedIndex,
onSelect,
header,
footer,
collapsible = false,
width = 280,
backgroundColor = '#0f172a',
activeBackgroundColor = '#3b82f6',
textColor = '#94a3b8',
activeTextColor = '#ffffff'
}) => {
// 状态管理
const [collapsed, setCollapsed] = React.useState(false);
const [sidebarWidth, setSidebarWidth] = React.useState(width);
const animatorRef = React.useRef<Animator>(new Animator());
// 初始化动画
React.useEffect(() => {
const animator = new Animator({
duration: 300,
easing: Easing.Spring(0.8, 100),
fill: FillMode.Forwards,
onUpdate: (value: number) => {
setSidebarWidth(value);
}
});
animatorRef.current = animator;
return () => {
animator.stop();
};
}, []);
// 折叠/展开切换
const toggleCollapse = () => {
if (!collapsible) return;
const newCollapsed = !collapsed;
setCollapsed(newCollapsed);
const targetWidth = newCollapsed ? 80 : width;
animatorRef.current.update({
values: [sidebarWidth, targetWidth]
});
animatorRef.current.play();
};
// 徽章渲染
const renderBadge = (badgeCount: number) => {
if (!badgeCount) return null;
return (
<Column style={styles.badgeContainer}>
<Column style={styles.badge}>
<Text style={styles.badgeText}>
{badgeCount > 99 ? '99+' : badgeCount}
</Text>
</Column>
</Column>
);
};
// 导航项渲染
const renderItem = (item: SidebarItem, index: number) => {
const isActive = index === selectedIndex;
return (
<TouchableOpacity
key={item.id}
style={[
styles.sidebarItem,
{ backgroundColor: isActive ? activeBackgroundColor : 'transparent' },
isActive && styles.activeSidebarItem
]}
onPress={() => onSelect(index)}
>
<Column style={styles.itemIconContainer}>
<Image
source={{ uri: SIDEBAR_ICONS[item.icon] }}
style={[
styles.itemIcon,
{ tintColor: isActive ? activeTextColor : textColor }
]}
/>
{renderBadge(item.badge || 0)}
</Column>
{!collapsed && (
<Text style={[
styles.itemText,
{ color: isActive ? activeTextColor : textColor }
]}>
{item.title}
</Text>
)}
{!collapsed && isActive && (
<Column style={styles.activeIndicator} />
)}
</TouchableOpacity>
);
};
return (
<Column
style={[
styles.sidebarContainer,
{
width: sidebarWidth,
backgroundColor
}
]}
>
{header && (
<Column style={styles.sidebarHeader}>
<Column style={styles.headerContent}>
{header}
</Column>
{collapsible && (
<TouchableOpacity
style={styles.collapseButton}
onPress={toggleCollapse}
>
<Text style={[styles.collapseText, { color: textColor }]}>
{collapsed ? '>>' : '<<'}
</Text>
</TouchableOpacity>
)}
</Column>
)}
<Scroll style={styles.sidebarContent}>
{items.map((item, index) => renderItem(item, index))}
</Scroll>
{footer && (
<Column style={styles.sidebarFooter}>
{footer}
</Column>
)}
</Column>
);
};
// 样式定义(适配鸿蒙)
const styles = StyleSheet.create({
sidebarContainer: {
height: '100%',
borderRightWidth: 1,
borderRightColor: '#1e293b',
flexDirection: 'column'
},
sidebarHeader: {
padding: 20,
borderBottomWidth: 1,
borderBottomColor: '#1e293b',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center'
},
headerContent: {
flex: 1
},
collapseButton: {
padding: 8,
borderRadius: 20
},
collapseText: {
fontSize: 18,
fontWeight: 'bold'
},
sidebarContent: {
flex: 1
},
sidebarItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 16,
paddingHorizontal: 20,
position: 'relative'
},
activeSidebarItem: {
borderRightWidth: 3,
borderRightColor: '#ffffff'
},
itemIconContainer: {
width: 24,
height: 24,
marginRight: 16,
position: 'relative'
},
itemIcon: {
width: 24,
height: 24
},
badgeContainer: {
position: 'absolute',
top: -6,
right: -6
},
badge: {
backgroundColor: '#ef4444',
borderRadius: 10,
minWidth: 20,
height: 20,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 6
},
badgeText: {
color: '#ffffff',
fontSize: 10,
fontWeight: 'bold'
},
itemText: {
fontSize: 16,
fontWeight: '500',
flex: 1
},
activeIndicator: {
position: 'absolute',
right: 0,
top: 0,
bottom: 0,
width: 3,
backgroundColor: '#ffffff'
},
sidebarFooter: {
padding: 20,
borderTopWidth: 1,
borderTopColor: '#1e293b'
}
});
export default Sidebar;
四、跨端适配最佳实践总结
1. 架构设计层面
- 逻辑与UI彻底解耦:开发跨端组件时,需将分页算法、徽章格式化、状态判断等核心逻辑抽离为纯函数,不依赖任何平台特定 API;UI 渲染层则针对不同平台的组件特性单独实现,确保逻辑复用率最大化。
- 接口标准化:通过 TypeScript/ArkTS 定义统一的组件接口(Props),保证 React Native 端与鸿蒙端的入参、回调函数格式一致,降低跨端开发的沟通成本。
- 资源统一管理:优先采用 Base64 内嵌图标、远程配置样式等方式,避免平台间资源路径、命名规范的差异导致的适配问题。
2. 技术实现层面
- 动画交互适配 :React Native 的
Animated动画可映射为鸿蒙的Animator动画,核心是保留动画的时序、缓动效果配置,替换动画驱动的 API 实现; - 样式适配 :Flex 布局属性可完全复用,只需将 React Native 的 StyleSheet 属性(如
backgroundColor)映射为鸿蒙的 ComponentStyle 属性(如background.color); - 交互组件适配 :React Native 的
TouchableOpacity可替换为鸿蒙的Button组件或GestureDetector,核心是保留点击回调逻辑,替换交互触发的载体。
3. 性能优化层面
- 避免过度渲染 :在 React Native 端使用
useMemo/useCallback缓存计算结果和回调函数,鸿蒙端则通过@Memo装饰器优化组件重渲染; - 动画性能优化 :优先使用原生驱动的动画(React Native 的
useNativeDriver: true、鸿蒙的属性动画),避免 JS 线程与 UI 线程的频繁通信; - 资源加载优化:Base64 图标虽便捷,但过多的 Base64 编码会增加 JS 包体积,可结合按需加载、图片压缩等方式平衡性能与适配成本。
五、总结
本文以 React Native 侧边导航组件为例,系统解读了其核心技术实现,并详细探讨了向鸿蒙跨端适配的完整路径。该组件的设计充分体现了跨端开发的「逻辑复用、UI 适配」核心思想,通过 TypeScript 类型约束保证了代码的健壮性,通过 Base64 图标、Flex 布局等技术降低了跨端适配成本,通过 Animated 动画提升了用户交互体验。
真实演示案例代码:
js
import React, { useState, useRef } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Dimensions, Image, Animated } from 'react-native';
// Base64 Icons for Sidebar component
const SIDEBAR_ICONS = {
home: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJMMiA5TDQgOWMxLjEgMCAyIC45IDIgMnY3YzAgMS4xLjkgMiAyIDJoMTJjMS4xIDAgMi0uOSAyLTJ2LTdjMC0xLjEuOS0yIDItMmgxbC0xMC03WiIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiLz4KPHBhdGggZD0iTTE2IDE1SDgiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+CjxwYXRoIGQ9Ik0xNiAxMVg4IiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8L3N2Zz4K',
user: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJDNy41OCAyIDQgNS41OCA0IDEwdjRjMCA0LjQyIDMuNTggOCA4IDhzOCAzLjU4IDggOHYtNGMwLTQuNDItMy41OC04LTgtOFYxMFoiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+CjxwYXRoIGQ9Ik04IDEwYzAtMi4yIDEuOC00IDQtNCAyLjIgMCA0IDEuOCA0IDQgMCAyLjItMS44IDQtNCA0cy00LTEuOC00LTRaIiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8L3N2Zz4K',
settings: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJaIiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8cGF0aCBkPSJNMTIgNHYxNCIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8cGF0aCBkPSJNNyAxMmgxMCIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K',
message: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0IDJIMTBjLTEuMSAwLTIgLjktMiAydjRoLTJjLTEuMSAwLTIgLjktMiAydjEwYzAgMS4xLjkgMiAyIDJoMTJjMS4xIDAgMi0uOSAyLTJWNmMwLTEuMS0uOS0yLTItMmgtMlYyem0wIDR2MmgydjJINHYtNFoiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+Cjwvc3ZnPgo=',
chart: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJMNiA4aDR2MTJINnoiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+CjxwYXRoIGQ9Ik0xOCA4aC00djEyaDR6IiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8L3N2Zz4K',
folder: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEwIDRINmEyIDIgMCAwIDAtMiAydjEyaDE2YTIgMiAwIDAgMi0yLTIiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+CjxwYXRoIGQ9Ik0yMiA4LjV2LTIuN2EyLjIgMi4yIDAgMCAwLS43LTEuN2wtNS40LTIuNmEyLjIgMi4yIDAgMCAwLTEuNy0uN0gyYTIgMiAwIDAgMC0yIDJ2MTJhMiAyIDAgMCAwIDIgMmg3IiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8L3N2Zz4K',
calendar: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE5IDRINWMtMS4xIDAtMiAuOS0yIDJ2MTRjMCAxLjEuOSAyIDIgMmgxNGMxLjEgMCAyLS45IDItMlY2YzAtMS4xLS45LTItMi0yem0wIDE2SDVWNmgxNHYxNHoiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+CjxwYXRoIGQ9Ik0xNiAydjRoIiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8cGF0aCBkPSI4IDJ2NCIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiLz4KPHBhdGggZD0iMyAxMGgxOCIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiLz4KPC9zdmc+Cg==',
help: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJaIiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8cGF0aCBkPSJNMTIgMTZ2LTMiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+CjxwYXRoIGQ9Ik0xMiA4aDAuMDEiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+Cjwvc3ZnPgo=',
logout: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE1IDN2NGgyYTEgMSAwIDAgMSAxIDF2OGExIDEgMCAwIDEtMSAxaC0yYTIgMiAwIDAgMCAwIDRoM2EyIDIgMCAwIDAgMi0yVjVhMiAyIDAgMCAwLTItMmgtM3oiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+CjxwYXRoIGQ9Ik04IDEydjRsLTQtNC00IDR2LTRhMiAyIDAgMCAxIDItMmg4YTIgMiAwIDAgMSAyIDJ6IiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8L3N2Zz4K'
};
// Sidebar Item Interface
interface SidebarItem {
id: string;
title: string;
icon: keyof typeof SIDEBAR_ICONS;
badge?: number;
}
// Sidebar Component
interface SidebarProps {
items: SidebarItem[];
selectedIndex: number;
onSelect: (index: number) => void;
header?: React.ReactNode;
footer?: React.ReactNode;
collapsible?: boolean;
width?: number;
backgroundColor?: string;
activeBackgroundColor?: string;
textColor?: string;
activeTextColor?: string;
}
const Sidebar: React.FC<SidebarProps> = ({
items,
selectedIndex,
onSelect,
header,
footer,
collapsible = false,
width = 280,
backgroundColor = '#0f172a',
activeBackgroundColor = '#3b82f6',
textColor = '#94a3b8',
activeTextColor = '#ffffff'
}) => {
const [collapsed, setCollapsed] = useState(false);
const animatedWidth = useRef(new Animated.Value(width)).current;
const toggleCollapse = () => {
if (collapsible) {
const newCollapsed = !collapsed;
setCollapsed(newCollapsed);
Animated.spring(animatedWidth, {
toValue: newCollapsed ? 80 : width,
useNativeDriver: false,
}).start();
}
};
const renderBadge = (badgeCount: number) => {
if (!badgeCount) return null;
return (
<View style={styles.badgeContainer}>
<View style={styles.badge}>
<Text style={styles.badgeText}>
{badgeCount > 99 ? '99+' : badgeCount}
</Text>
</View>
</View>
);
};
const renderItem = (item: SidebarItem, index: number) => {
const isActive = index === selectedIndex;
return (
<TouchableOpacity
key={item.id}
style={[
styles.sidebarItem,
isActive && styles.activeSidebarItem,
{ backgroundColor: isActive ? activeBackgroundColor : 'transparent' }
]}
onPress={() => onSelect(index)}
>
<View style={styles.itemIconContainer}>
<Image
source={{ uri: SIDEBAR_ICONS[item.icon] }}
style={[
styles.itemIcon,
{ tintColor: isActive ? activeTextColor : textColor }
]}
/>
{renderBadge(item.badge || 0)}
</View>
{!collapsed && (
<Text style={[
styles.itemText,
{ color: isActive ? activeTextColor : textColor }
]}>
{item.title}
</Text>
)}
{!collapsed && isActive && (
<View style={styles.activeIndicator} />
)}
</TouchableOpacity>
);
};
return (
<Animated.View
style={[
styles.sidebarContainer,
{
width: animatedWidth,
backgroundColor
}
]}
>
{header && (
<View style={styles.sidebarHeader}>
{header}
{collapsible && (
<TouchableOpacity
style={styles.collapseButton}
onPress={toggleCollapse}
>
<Text style={[styles.collapseText, { color: textColor }]}>
{collapsed ? '>>' : '<<'}
</Text>
</TouchableOpacity>
)}
</View>
)}
<ScrollView style={styles.sidebarContent}>
{items.map((item, index) => renderItem(item, index))}
</ScrollView>
{footer && (
<View style={styles.sidebarFooter}>
{footer}
</View>
)}
</Animated.View>
);
};
// Main App Component
const SidebarComponentApp = () => {
const [selectedIndex, setSelectedIndex] = useState(0);
const sidebarItems: SidebarItem[] = [
{ id: '1', title: '首页', icon: 'home', badge: 0 },
{ id: '2', title: '个人中心', icon: 'user', badge: 3 },
{ id: '3', title: '消息中心', icon: 'message', badge: 12 },
{ id: '4', title: '数据分析', icon: 'chart', badge: 0 },
{ id: '5', title: '文件管理', icon: 'folder', badge: 5 },
{ id: '6', title: '日程安排', icon: 'calendar', badge: 0 },
{ id: '7', title: '系统设置', icon: 'settings', badge: 0 },
{ id: '8', title: '帮助中心', icon: 'help', badge: 0 }
];
const handleSelect = (index: number) => {
setSelectedIndex(index);
console.log(`选择了: ${sidebarItems[index].title}`);
};
const renderHeader = () => (
<View style={styles.headerContent}>
<View style={styles.logoContainer}>
<Text style={styles.logoText}>管理系统</Text>
</View>
</View>
);
const renderFooter = () => (
<TouchableOpacity
style={styles.logoutButton}
onPress={() => console.log('退出登录')}
>
<Image
source={{ uri: SIDEBAR_ICONS.logout }}
style={styles.logoutIcon}
/>
<Text style={styles.logoutText}>退出登录</Text>
</TouchableOpacity>
);
return (
<View style={styles.container}>
<View style={styles.header}>
<Text style={styles.headerTitle}>侧边导航组件</Text>
<Text style={styles.headerSubtitle}>绑定当前选中项的索引</Text>
</View>
<View style={styles.content}>
<Sidebar
items={sidebarItems}
selectedIndex={selectedIndex}
onSelect={handleSelect}
header={renderHeader()}
footer={renderFooter()}
collapsible={true}
width={280}
/>
<View style={styles.mainContent}>
<View style={styles.contentCard}>
<Text style={styles.contentTitle}>
当前选中: {sidebarItems[selectedIndex]?.title}
</Text>
<Text style={styles.contentDescription}>
这是 {sidebarItems[selectedIndex]?.title} 页面的内容区域。
您可以通过侧边导航切换不同的功能模块。
</Text>
</View>
<View style={styles.featuresSection}>
<Text style={styles.featuresTitle}>功能特性</Text>
<View style={styles.featureList}>
<View style={styles.featureItem}>
<Text style={styles.featureBullet}>•</Text>
<Text style={styles.featureText}>可折叠侧边栏</Text>
</View>
<View style={styles.featureItem}>
<Text style={styles.featureBullet}>•</Text>
<Text style={styles.featureText}>徽章数字显示</Text>
</View>
<View style={styles.featureItem}>
<Text style={styles.featureBullet}>•</Text>
<Text style={styles.featureText}>活跃状态指示</Text>
</View>
<View style={styles.featureItem}>
<Text style={styles.featureBullet}>•</Text>
<Text style={styles.featureText}>丰富的Base64图标</Text>
</View>
<View style={styles.featureItem}>
<Text style={styles.featureBullet}>•</Text>
<Text style={styles.featureText}>自定义样式支持</Text>
</View>
</View>
</View>
<View style={styles.usageSection}>
<Text style={styles.usageTitle}>使用说明</Text>
<Text style={styles.usageText}>
侧边导航组件用于应用程序的主要导航结构,
支持可折叠、徽章显示、活跃状态等多种功能。
</Text>
</View>
</View>
</View>
<View style={styles.footer}>
<Text style={styles.footerText}>© 2023 侧边导航组件 | 现代化UI组件库</Text>
</View>
</View>
);
};
const { width, height } = Dimensions.get('window');
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f1f5f9',
},
header: {
backgroundColor: '#0f172a',
paddingTop: 30,
paddingBottom: 25,
paddingHorizontal: 20,
borderBottomWidth: 1,
borderBottomColor: '#1e293b',
},
headerTitle: {
fontSize: 28,
fontWeight: '700',
color: '#f8fafc',
textAlign: 'center',
marginBottom: 5,
},
headerSubtitle: {
fontSize: 16,
color: '#94a3b8',
textAlign: 'center',
},
content: {
flex: 1,
flexDirection: 'row',
},
mainContent: {
flex: 1,
padding: 20,
},
sidebarContainer: {
height: '100%',
borderRightWidth: 1,
borderRightColor: '#1e293b',
},
sidebarHeader: {
padding: 20,
borderBottomWidth: 1,
borderBottomColor: '#1e293b',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
headerContent: {
flex: 1,
},
logoContainer: {
alignItems: 'center',
},
logoText: {
fontSize: 20,
fontWeight: 'bold',
color: '#f8fafc',
},
collapseButton: {
padding: 8,
borderRadius: 20,
},
collapseText: {
fontSize: 18,
fontWeight: 'bold',
},
sidebarContent: {
flex: 1,
},
sidebarItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 16,
paddingHorizontal: 20,
position: 'relative',
},
activeSidebarItem: {
borderRightWidth: 3,
borderRightColor: '#ffffff',
},
itemIconContainer: {
width: 24,
height: 24,
marginRight: 16,
position: 'relative',
},
itemIcon: {
width: 24,
height: 24,
},
badgeContainer: {
position: 'absolute',
top: -6,
right: -6,
},
badge: {
backgroundColor: '#ef4444',
borderRadius: 10,
minWidth: 20,
height: 20,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 6,
},
badgeText: {
color: '#ffffff',
fontSize: 10,
fontWeight: 'bold',
},
itemText: {
fontSize: 16,
fontWeight: '500',
flex: 1,
},
activeIndicator: {
position: 'absolute',
right: 0,
top: 0,
bottom: 0,
width: 3,
backgroundColor: '#ffffff',
},
sidebarFooter: {
padding: 20,
borderTopWidth: 1,
borderTopColor: '#1e293b',
},
logoutButton: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 12,
},
logoutIcon: {
width: 20,
height: 20,
tintColor: '#94a3b8',
marginRight: 12,
},
logoutText: {
fontSize: 16,
color: '#94a3b8',
fontWeight: '500',
},
contentCard: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 20,
marginBottom: 25,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.08,
shadowRadius: 2,
},
contentTitle: {
fontSize: 20,
fontWeight: '600',
color: '#0f172a',
marginBottom: 12,
},
contentDescription: {
fontSize: 16,
color: '#64748b',
lineHeight: 24,
},
featuresSection: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 20,
marginBottom: 25,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.08,
shadowRadius: 2,
},
featuresTitle: {
fontSize: 18,
fontWeight: '600',
color: '#0f172a',
marginBottom: 15,
},
featureList: {
paddingLeft: 15,
},
featureItem: {
flexDirection: 'row',
marginBottom: 10,
},
featureBullet: {
fontSize: 16,
color: '#3b82f6',
marginRight: 8,
},
featureText: {
fontSize: 16,
color: '#64748b',
flex: 1,
},
usageSection: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 20,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.08,
shadowRadius: 2,
},
usageTitle: {
fontSize: 18,
fontWeight: '600',
color: '#0f172a',
marginBottom: 10,
},
usageText: {
fontSize: 16,
color: '#64748b',
lineHeight: 24,
},
footer: {
paddingVertical: 20,
alignItems: 'center',
backgroundColor: '#0f172a',
},
footerText: {
color: '#94a3b8',
fontSize: 14,
},
});
export default SidebarComponentApp;

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

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

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


