React Native for OpenHarmony 实战:多端响应式布局与高可用交互设计

在这里插入图片描述

基于 OpenHarmony 跨平台开发先锋训练营 Day 10 的实战经验,本文系统性地讲解如何实现从"功能原型"到"产品级应用"的跨越,重点聚焦响应式布局设计、异常处理机制以及跨端技术对比。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

一、从功能到产品的思维升级

1.1 产品思维转变

┌─────────────────────────────────────────────────────────┐
│                   开发思维演进路径                         │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  功能实现期          →         体验打磨期                │
│                                                         │
│  "能用就行"                   "好用才行"                 │
│  逻辑正确                    细节精致                     │
│  基础功能                    完整体验                     │
│                                                         │
│  关注点:                     关注点:                   │
│  • 业务逻辑                   • 用户感受                 │
│  • 数据流转                   • 交互反馈                 │
│  • API 调用                   • 异常处理                 │
│                                                         │
└─────────────────────────────────────────────────────────┘

1.2 产品质量评估维度

维度 评估标准 验证方法
可用性 功能完整、逻辑正确 功能测试清单
稳定性 异常处理完善、边界情况覆盖 压力测试、异常注入
适配性 多端布局自适应、响应式设计 多设备真机测试
流畅性 动画帧率稳定、操作响应及时 性能监控工具
友好性 空状态提示、错误引导、加载反馈 UX 走查

二、核心页面深度优化

2.1 首页 (TodoPage) - 智能文本适配

问题分析
// ❌ 硬编码截断 - 不适配多端
Text(item.title.substring(0, 16))

问题:
1. 手机屏:可能显示不全
2. 平板屏:大片留白,空间浪费
3. 折叠屏:无法动态调整
解决方案:弹性布局 + 文本溢出
// entry/src/main/ets/pages/TodoPage.ets

@ComponentV2
struct TodoItem {
  @Param title: string = '';
  @Param timestamp: number = 0;

  build() {
    Row() {
      // 标题:使用 layoutWeight 自动占据剩余空间
      Text(this.title)
        .fontSize(16)
        .fontColor('#333333')
        .layoutWeight(1)  // 关键:贪婪占据剩余空间
        .maxLines(2)      // 最多显示 2 行
        .textOverflow({ overflow: TextOverflow.Ellipsis })  // 优雅省略
        .padding({ right: 12 })

      // 时间戳:固定宽度,不参与弹性分配
      Text(this.formatTime(this.timestamp))
        .fontSize(12)
        .fontColor('#999999')
        .width(60)  // 固定宽度
    }
    .width('100%')
    .alignItems(VerticalAlign.Center)
  }

  private formatTime(timestamp: number): string {
    const date = new Date(timestamp);
    return `${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}`;
  }
}

// ==================== 弹窗约束优化 ====================

@CustomDialog
struct TodoInputDialog {
  controller: CustomDialogController;

  build() {
    Column() {
      Text('新建任务')
        .fontSize(18)
        .fontWeight(FontWeight.Medium)

      TextArea({ placeholder: '写点什么吧~' })
        .width('100%')
        .height(120)
        .margin({ top: 16 })

      Row() {
        Button('取消')
          .onClick(() => this.controller.close())

        Button('确定')
          .onClick(() => this.handleConfirm())
      }
      .justifyContent(FlexAlign.SpaceBetween)
      .width('100%')
      .margin({ top: 16 })
    }
    .width('100%')
    .padding(24)
    // 关键:约束最大宽度,防止平板上过度拉伸
    .constraintSize({ maxWidth: 400 })
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
  }
}
空状态设计
@ComponentV2
struct TodoList {
  @Local private todoList: TodoItem[] = [];

  build() {
    if (this.todoList.length === 0) {
      // 空状态组件
      Column() {
        Image($r('app.media.empty_illustration'))
          .width(200)
          .height(200)
          .margin({ bottom: 24 })

        Text('享受生活~')
          .fontSize(18)
          .fontColor('#666666')
          .margin({ bottom: 8 })

        Text('暂时没有任务,去添加一个吧')
          .fontSize(14)
          .fontColor('#999999')
      }
      .width('100%')
      .height('100%')
      .justifyContent(FlexAlign.Center)
      .alignItems(HorizontalAlign.Center)
    } else {
      // 任务列表
      List() {
        ForEach(this.todoList, (item: TodoItem) => {
          ListItem() {
            TodoItem({ title: item.title, timestamp: item.timestamp })
          }
        })
      }
    }
  }
}

2.2 日历页 (CalendarPage) - 丝滑的时间视图

// entry/src/main/ets/pages/CalendarPage.ets

@Entry
@ComponentV2
struct CalendarPage {
  @Local private viewMode: 'month' | 'day' = 'month';
  @Local private selectedDate: Date = new Date();

  // 单例数据管理器
  private dataManager = TodoDataManager.getInstance();

  build() {
    Column() {
      // 视图切换器
      SegmentedButton({
        options: [
          { label: '月视图', value: 'month' },
          { label: '日视图', value: 'day' }
        ],
        selected: this.viewMode,
        onSelectionChange: (value) => {
          this.viewMode = value as 'month' | 'day';
        }
      })
      .margin({ bottom: 16 })

      // 视图内容
      if (this.viewMode === 'month') {
        this.MonthView()
      } else {
        this.DayView()
      }
    }
    .width('100%')
    .height('100%')
    .padding(16)
  }

  @Builder
  MonthView() {
    // 月视图实现
    Grid() {
      ForEach(this.generateMonthDays(), (day: Date) => {
        GridItem() {
          this.DayCell(day)
        }
      })
    }
    .columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr')
    .rowsGap(8)
    .columnsGap(8)
  }

  @Builder
  DayView() {
    // 日视图实现 - 显示当天的所有任务
    List() {
      ForEach(this.dataManager.getTodosByDate(this.selectedDate), (todo: TodoItem) => {
        ListItem() {
          this.TodoCard(todo)
        }
      })
    }
    .divider({ strokeWidth: 1, color: '#F0F0F0' })
  }

  @Builder
  DayCell(day: Date) {
    Text(day.getDate().toString())
      .width(40)
      .height(40)
      .textAlign(TextAlign.Center)
      .fontSize(16)
      .fontColor(this.isToday(day) ? '#007AFF' : '#333')
      .backgroundColor(this.isToday(day) ? '#E5F1FF' : 'transparent')
      .borderRadius(20)
      .onClick(() => {
        this.selectedDate = day;
        this.viewMode = 'day';
      })
  }

  private isToday(date: Date): boolean {
    const today = new Date();
    return date.getDate() === today.getDate() &&
           date.getMonth() === today.getMonth() &&
           date.getFullYear() === today.getFullYear();
  }
}

2.3 心情广场 (DiscoverPage) - 情感化设计

// entry/src/main/ets/pages/DiscoverPage.ets

@Entry
@ComponentV2
struct DiscoverPage {
  @Local private moodList: MoodItem[] = [];
  @Local private isPosting: boolean = false;
  @Local private inputContent: string = '';

  private readonly backgroundColors = [
    '#FFF5E6', // 温暖黄
    '#E6F7FF', // 清新蓝
    '#F0FFF4', // 治愈绿
    '#FFF0F5', // 温柔粉
    '#F5F0FF', // 梦幻紫
  ];

  build() {
    Column() {
      // 发布区域
      this.PublishSection()

      // 心情列表
      List() {
        ForEach(this.moodList, (mood: MoodItem) => {
          ListItem() {
            this.MoodCard(mood)
          }
        })
      }
      .width('100%')
      .layoutWeight(1)
      .padding({ top: 16 })
    }
    .width('100%')
    .height('100%')
    .padding(16)
    .backgroundColor('#F5F5F5')
  }

  @Builder
  PublishSection() {
    Column() {
      TextArea({
        placeholder: '记录当下的心情...',
        text: this.inputContent
      })
      .width('100%')
      .height(100)
      .onChange((value: string) => {
        this.inputContent = value;
      })

      Row() {
        // Emoji 选择器
        Row() {
          ForEach(['😊', '😢', '😡', '🥳', '😴'], (emoji: string) => {
            Text(emoji)
              .fontSize(24)
              .padding(8)
              .onClick(() => {
                // 处理 emoji 选择
              })
          })
        }

        Blank()

        // 发布按钮
        Button('发布')
          .enabled(!this.isPosting && this.inputContent.trim().length > 0)
          .onClick(() => this.handlePublish())
      }
      .width('100%')
      .margin({ top: 12 })
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
  }

  @Builder
  MoodCard(mood: MoodItem) {
    Column() {
      Text(mood.content)
        .fontSize(16)
        .fontColor('#333')
        .maxLines(4)
        .textOverflow({ overflow: TextOverflow.Ellipsis })

      Row() {
        Text(mood.moodEmoji)
        Text(mood.timestamp)
          .fontSize(12)
          .fontColor('#999')
          .margin({ left: 8 })

        Blank()

        Text(`❤️ ${mood.likes}`)
          .fontSize(14)
          .fontColor('#666')
      }
      .width('100%')
      .margin({ top: 12 })
    }
    .width('100%')
    .padding(16)
    .backgroundColor(mood.bgColor)
    .borderRadius(12)
    .margin({ bottom: 12 })
  }

  /**
   * 发布处理 - 包含完整的异常处理流程
   */
  private async handlePublish(): Promise<void> {
    // 第一级:输入防护
    const trimmed = this.inputContent.trim();
    if (trimmed.length === 0) {
      PromptAction.showToast({
        message: '写点什么吧~',
        duration: 1500
      });
      return;
    }

    // 第二级:过程反馈
    this.isPosting = true;

    try {
      // 模拟网络请求(20% 失败率用于测试)
      const shouldFail = Math.random() < 0.2;

      await new Promise((resolve, reject) => {
        setTimeout(() => {
          if (shouldFail) {
            reject(new Error('Network error'));
          } else {
            resolve(true);
          }
        }, 1500);
      });

      // 成功:添加到列表
      const newMood: MoodItem = {
        id: Date.now().toString(),
        content: trimmed,
        moodEmoji: '😊',
        timestamp: this.formatTime(new Date()),
        likes: 0,
        bgColor: this.backgroundColors[Math.floor(Math.random() * this.backgroundColors.length)]
      };

      this.moodList.unshift(newMood);
      this.inputContent = '';  // 仅在成功后清空输入

      PromptAction.showToast({
        message: '发布成功!',
        duration: 1500
      });

    } catch (error) {
      // 第三级:结果兜底 - 保留用户输入
      PromptAction.showToast({
        message: '发布失败,请重试',
        duration: 1500
      });
      // 注意:不清空 this.inputContent,保留用户输入
    } finally {
      this.isPosting = false;
    }
  }

  private formatTime(date: Date): string {
    const now = new Date();
    const diff = now.getTime() - date.getTime();
    const minutes = Math.floor(diff / 60000);

    if (minutes < 1) return '刚刚';
    if (minutes < 60) return `${minutes}分钟前`;
    if (minutes < 1440) return `${Math.floor(minutes / 60)}小时前`;
    return `${date.getMonth() + 1}${date.getDate()}`;
  }
}

interface MoodItem {
  id: string;
  content: string;
  moodEmoji: string;
  timestamp: string;
  likes: number;
  bgColor: string;
}

2.4 个人中心 (MinePage) - 栅格化响应式重构

// entry/src/main/ets/pages/MinePage.ets

@Entry
@ComponentV2
struct MinePage {
  private menuItems: MenuItem[] = [
    { icon: '⚙️', title: '设置', route: 'Settings' },
    { icon: '📊', title: '统计', route: 'Statistics' },
    { icon: '🔔', title: '消息', route: 'Messages' },
    { icon: '📁', title: '归档', route: 'Archive' },
    { icon: '💾', title: '备份', route: 'Backup' },
    { icon: 'ℹ️', title: '关于', route: 'About' },
  ];

  build() {
    Scroll() {
      Column() {
        // 用户信息卡片
        this.UserInfoCard()

        // 栅格化菜单 - 响应式布局的核心
        GridRow({
          columns: {
            // 断点配置:不同屏幕尺寸下的列数
            sm: 1,  // 手机 (< 600dp): 单列
            md: 2,  // 折叠屏 (600-840dp): 双列
            lg: 3   // 平板 (> 840dp): 三列
          },
          gutter: {
            x: 12,  // 水平间距
            y: 12   // 垂直间距
          }
        }) {
          ForEach(this.menuItems, (item: MenuItem) => {
            GridCol() {
              this.MenuItemCard(item)
            }
          })
        }
        .width('100%')
        .margin({ top: 24 })
      }
      .width('100%')
      .padding({
        left: new BreakpointType<Padding>({
          sm: { left: '5%', right: '5%' },
          md: { left: '5%', right: '5%' },
          lg: { left: '10%', right: '10%' }
        }).getValue(this.currentBreakpoint),
        right: new BreakpointType<Padding>({
          sm: { left: '5%', right: '5%' },
          md: { left: '5%', right: '5%' },
          lg: { left: '10%', right: '10%' }
        }).getValue(this.currentBreakpoint),
        top: 24,
        bottom: 24
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }

  @Builder
  UserInfoCard() {
    Column() {
      Image($r('app.media.default_avatar'))
        .width(80)
        .height(80)
        .borderRadius(40)
        .margin({ bottom: 12 })

      Text('用户昵称')
        .fontSize(20)
        .fontWeight(FontWeight.Medium)
        .margin({ bottom: 4 })

      Text('这个人很懒,什么都没写')
        .fontSize(14)
        .fontColor('#999')
    }
    .width('100%')
    .padding(24)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
    .alignItems(HorizontalAlign.Center)
  }

  @Builder
  MenuItemCard(item: MenuItem) {
    Row() {
      Text(item.icon)
        .fontSize(24)

      Text(item.title)
        .fontSize(16)
        .fontColor('#333')
        .margin({ left: 12 })

      Blank()

      Text('>')
        .fontSize(14)
        .fontColor('#CCC')
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
    .onClick(() => {
      // 导航到对应页面
    })
  }
}

interface MenuItem {
  icon: string;
  title: string;
  route: string;
}

interface Padding {
  left?: string;
  right?: string;
}

// ==================== 断点工具类 ====================

class BreakpointType<T> {
  private smValue: T;
  private mdValue: T;
  private lgValue: T;

  constructor(config: { sm: T; md: T; lg: T }) {
    this.smValue = config.sm;
    this.mdValue = config.md;
    this.lgValue = config.lg;
  }

  getValue(breakpoint: string): T {
    switch (breakpoint) {
      case 'sm':
        return this.smValue;
      case 'md':
        return this.mdValue;
      case 'lg':
        return this.lgValue;
      default:
        return this.smValue;
    }
  }
}

三、多终端适配技术体系

3.1 响应式架构全景

┌─────────────────────────────────────────────────────────┐
│              鸿蒙响应式适配技术金字塔                     │
├─────────────────────────────────────────────────────────┤
│                                                         │
│                    应用层                                │
│            (页面布局自适应、组件响应式)                    │
│                                                         │
│  ┌─────────────────────────────────────────────────┐   │
│  │              栅格系统 (Grid System)               │   │
│  │     GridRow / GridCol / layoutWeight             │   │
│  └─────────────────────────────────────────────────┘   │
│                          ↑                              │
│  ┌─────────────────────────────────────────────────┐   │
│  │            断点感知 (Breakpoint)                   │   │
│  │     BreakpointType / currentBreakpoint            │   │
│  └─────────────────────────────────────────────────┘   │
│                          ↑                              │
│  ┌─────────────────────────────────────────────────┐   │
│  │           避让区域 (AvoidArea)                     │   │
│  │     AppStorage / systemBar / safeArea             │   │
│  └─────────────────────────────────────────────────┘   │
│                          ↑                              │
│  ┌─────────────────────────────────────────────────┐   │
│  │            系统层 (System Layer)                  │   │
│  │     窗口管理 / 安全区域 / 屏幕密度                  │   │
│  └─────────────────────────────────────────────────┘   │
│                                                         │
└─────────────────────────────────────────────────────────┘

3.2 断点规范

断点 屏幕宽度 典型设备 列数推荐
sm < 600dp 手机竖屏 1 列
md 600-840dp 折叠屏、平板竖屏 2 列
lg > 840dp 平板横屏、桌面 3-4 列

3.3 避让区域处理

// entry/src/main/ets/utils/SafeArea.ets

/**
 * 安全区域工具类
 */
export class SafeAreaManager {
  private static instance: SafeAreaManager;

  private constructor() {
    this.initSafeArea();
  }

  public static getInstance(): SafeAreaManager {
    if (!SafeAreaManager.instance) {
      SafeAreaManager.instance = new SafeAreaManager();
    }
    return SafeAreaManager.instance;
  }

  /**
   * 初始化安全区域监听
   */
  private initSafeArea(): void {
    // 从 AppStorage 获取系统避让区域
    AppStorage.setOrCreate('safeAreaInsets', {
      top: 0,
      bottom: 0,
      left: 0,
      right: 0
    });

    // 监听窗口变化
    // 在实际应用中,这里会监听系统回调
  }

  /**
   * 获取顶部安全区域高度
   */
  public getTopInset(): number {
    const insets = AppStorage.get('safeAreaInsets') as SafeAreaInsets;
    return insets?.top || 0;
  }

  /**
   * 获取底部安全区域高度
   */
  public getBottomInset(): number {
    const insets = AppStorage.get('safeAreaInsets') as SafeAreaInsets;
    return insets?.bottom || 0;
  }
}

interface SafeAreaInsets {
  top: number;
  bottom: number;
  left: number;
  right: number;
}

// ==================== 使用示例 ====================

@ComponentV2
struct SafeAreaPage {
  private safeAreaManager = SafeAreaManager.getInstance();

  build() {
    Column() {
      Text('内容区域')
        .width('100%')
        .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
    .padding({
      top: this.safeAreaManager.getTopInset(),
      bottom: this.safeAreaManager.getBottomInset()
    })
  }
}

四、异常处理三级防护体系

4.1 防护架构

┌─────────────────────────────────────────────────────────┐
│                  异常处理三级防护体系                      │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  第一级:输入防护 (Input Guard)                          │
│  ├─ 空值检测 .trim()                                    │
│  ├─ 格式验证 正则表达式                                   │
│  └─ 即时反馈 Toast 提示                                  │
│                    ↓                                     │
│  第二级:过程反馈 (Process Feedback)                     │
│  ├─ 加载状态 loading = true                              │
│  ├─ 按钮禁用 enabled = false                             │
│  └─ 进度提示 进度条/Loading 指示器                      │
│                    ↓                                     │
│  第三级:结果兜底 (Fallback Strategy)                    │
│  ├─ 空状态 EmptyState 组件                               │
│  ├─ 错误恢复 保留用户输入/重试机制                        │
│  └─ 日志记录 错误上报/本地存储                           │
│                                                         │
└─────────────────────────────────────────────────────────┘

4.2 完整异常处理模板

// entry/src/main/ets/utils/ErrorHandler.ets

/**
 * 异常处理工具类
 */
export class ErrorHandler {
  /**
   * 包装异步操作,提供统一的异常处理
   */
  static async handleAsync<T>(
    operation: () => Promise<T>,
    options: {
      loadingRef?: { value: boolean };
      onSuccess?: (result: T) => void;
      onError?: (error: Error) => void;
      errorMessage?: string;
    } = {}
  ): Promise<T | null> {
    const {
      loadingRef,
      onSuccess,
      onError,
      errorMessage = '操作失败,请重试'
    } = options;

    try {
      // 设置加载状态
      if (loadingRef) {
        loadingRef.value = true;
      }

      // 执行操作
      const result = await operation();

      // 成功回调
      if (onSuccess) {
        onSuccess(result);
      }

      return result;

    } catch (error) {
      const err = error instanceof Error ? error : new Error(String(error));

      // 错误回调
      if (onError) {
        onError(err);
      } else {
        // 默认错误提示
        PromptAction.showToast({
          message: errorMessage,
          duration: 2000
        });
      }

      // 记录错误日志
      this.logError(err);

      return null;

    } finally {
      // 清除加载状态
      if (loadingRef) {
        loadingRef.value = false;
      }
    }
  }

  /**
   * 记录错误日志
   */
  private static logError(error: Error): void {
    console.error(`[ErrorHandler] ${error.message}`, error.stack);

    // 在实际应用中,这里可以将错误上报到服务器
    // 或保存到本地文件供后续分析
  }
}

// ==================== 使用示例 ====================

@ComponentV2
struct DataListPage {
  @Local private dataList: Item[] = [];
  @Local private isLoading: boolean = false;

  build() {
    Column() {
      if (this.isLoading) {
        this.LoadingView()
      } else if (this.dataList.length === 0) {
        this.EmptyView()
      } else {
        this.ListView()
      }
    }
  }

  /**
   * 加载数据 - 使用异常处理工具
   */
  private async loadData(): Promise<void> {
    await ErrorHandler.handleAsync(
      () => this.fetchDataFromAPI(),
      {
        loadingRef: { value: this.isLoading },
        onSuccess: (data) => {
          this.dataList = data;
        },
        errorMessage: '加载失败,请稍后重试'
      }
    );
  }

  private async fetchDataFromAPI(): Promise<Item[]> {
    // 实际的 API 调用
    return [];
  }

  @Builder
  LoadingView() {
    Row() {
      LoadingProgress()
        .width(24)
        .height(24)
        .color('#007AFF')

      Text('加载中...')
        .fontSize(14)
        .fontColor('#666')
        .margin({ left: 8 })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }

  @Builder
  EmptyView() {
    Column() {
      Image($r('app.media.empty_state'))
        .width(200)
        .height(200)
        .margin({ bottom: 24 })

      Text('暂无数据')
        .fontSize(16)
        .fontColor('#999')

      Button('重新加载')
        .margin({ top: 16 })
        .onClick(() => this.loadData())
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }

  @Builder
  ListView() {
    List() {
      ForEach(this.dataList, (item: Item) => {
        ListItem() {
          Text(item.title)
        }
      })
    }
  }
}

interface Item {
  title: string;
}

五、React Native vs ArkTS 技术对比

5.1 底部导航对比

特性 React Native ArkTS
实现方式 @react-navigation/bottom-tabs Tabs + TabContent
状态保持 需配置 unmountOnBlur: false 默认保持,不销毁页面
性能开销 JS Bridge 调度 原生组件,零桥接
定制能力 高度可定制 系统级动画,深度集成
RN 实现
// React Native 底部导航
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';

const Tab = createBottomTabNavigator();

function App() {
  return (
    <Tab.Navigator
      screenOptions={{
        unmountOnBlur: false,  // 关键配置
        lazy: false,
      }}
    >
      <Tab.Screen name="Todo" component={TodoScreen} />
      <Tab.Screen name="Calendar" component={CalendarScreen} />
    </Tab.Navigator>
  );
}
ArkTS 实现
// ArkTS 底部导航
@Entry
@ComponentV2
struct MainTabs {
  @Local private currentIndex: number = 0;

  build() {
    Tabs({ barPosition: BarPosition.End }) {
      TabContent() {
        TodoPage()
      }
      .tabBar(this.TabBuilder('待办', 0))

      TabContent() {
        CalendarPage()
      }
      .tabBar(this.TabBuilder('日历', 1))
    }
    .onChange((index: number) => {
      this.currentIndex = index;
    })
  }

  @Builder
  TabBuilder(title: string, index: number) {
    Column() {
      Text(title)
        .fontSize(this.currentIndex === index ? 16 : 14)
        .fontColor(this.currentIndex === index ? '#007AFF' : '#666')
    }
  }
}

5.2 响应式布局对比

特性 React Native ArkTS
核心工具 Flexbox + useWindowDimensions GridRow/GridCol + BreakpointType
断点管理 手动计算 声明式断点配置
性能 JS 线程计算 原生渲染管线
RN 实现
// React Native 响应式
import { useWindowDimensions } from 'react-native';

function ResponsiveComponent() {
  const { width } = useWindowDimensions();
  const numColumns = width > 768 ? 3 : 1;

  return (
    <FlatList
      data={items}
      numColumns={numColumns}
      renderItem={renderItem}
    />
  );
}
ArkTS 实现
// ArkTS 响应式 - 栅格系统
GridRow({
  columns: { sm: 1, md: 2, lg: 3 },
  gutter: { x: 12, y: 12 }
}) {
  ForEach(items, (item) => {
    GridCol() {
      ItemCard(item)
    }
  })
}

5.3 异常处理对比

特性 React Native ArkTS
状态管理 useState / Redux @State / @Observed
错误边界 ErrorBoundary 组件 编译期类型检查
弹窗提示 Alert.alert / Modal PromptAction.showToast

六、React Native 高级技巧

6.1 架构级优化:JSI 替代 Bridge

传统 RN 架构:
JS Thread ──[JSON 序列化]──> Bridge ──[反序列化]──> Native
   │                                    │
   └── 异步通信,有延迟                   └── 性能瓶颈

新架构 (JSI):
JS Thread ──[直接引用]──> C++ Object ──[同步调用]──> Native
   │                                    │
   └── 零拷贝,同步调用                   └── 性能提升 30x+

6.2 列表性能极限调优

// React Native 高性能列表
import { FlashList } from '@shopify/flash-list';

function OptimizedList({ data }: { data: Item[] }) {
  return (
    <FlashList
      data={data}
      estimatedItemSize={80}
      renderItem={({ item }) => <ItemCard item={item} />}
      keyExtractor={(item) => item.id}
      // 性能优化配置
      removeClippedSubviews={true}
      maxToRenderPerBatch={10}
      windowSize={5}
    />
  );
}

// 使用 React.memo 优化列表项
const ItemCard = React.memo<ItemProps>(({ item }) => (
  <View>
    <Text>{item.title}</Text>
  </View>
), (prevProps, nextProps) => {
  return prevProps.item.id === nextProps.item.id &&
         prevProps.item.updated === nextProps.item.updated;
});

6.3 乐观更新模式

// React Native 乐观更新
import { useMutation, useQueryClient } from '@tanstack/react-query';

function LikeButton({ postId, initialLiked }: Props) {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: async () => {
      const response = await fetch(`/api/posts/${postId}/like`, {
        method: 'POST',
      });
      return response.json();
    },

    // 乐观更新
    onMutate: async () => {
      // 取消正在进行的查询
      await queryClient.cancelQueries({ queryKey: ['post', postId] });

      // 保存当前状态
      const previousPost = queryClient.getQueryData(['post', postId]);

      // 乐观更新
      queryClient.setQueryData(['post', postId], (old: any) => ({
        ...old,
        liked: !old.liked,
        likes: old.liked ? old.likes - 1 : old.likes + 1,
      }));

      return { previousPost };
    },

    // 失败时回滚
    onError: (err, variables, context) => {
      queryClient.setQueryData(['post', postId], context?.previousPost);
    },

    // 成功后刷新
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['post', postId] });
    },
  });

  return (
    <Button
      title={initialLiked ? '已赞' : '点赞'}
      onPress={() => mutation.mutate()}
    />
  );
}

七、构建故障排查

7.1 Hvigor 配置文件缺失

问题现象
> hvigor ERROR: 00304035 Not Found
Error Message: Cannot find project build file build-profile.json5
根因分析
问题定位路径:
1. 错误代码 → 00304035 (文件未找到)
2. 文件路径 → 工程根目录
3. 现场勘查 → build-profile.json5 不存在
4. 根因判定 → 文件被意外删除或 Git 同步丢失
解决方案
// build-profile.json5 (完整配置模板)
{
  "apiType": "stageMode",
  "app": {
    "signingConfigs": [],
    "compileSdkVersion": 10,
    "compatibleSdkVersion": "10",
    "products": [
      {
        "name": "default",
        "signingConfig": "default",
        "compileSdkVersion": 10,
        "compatibleSdkVersion": "10",
        "runtimeOS": "HarmonyOS"
      }
    ]
  },
  "modules": [
    {
      "name": "entry",
      "srcPath": "./entry",
      "targets": [
        {
          "name": "default",
          "applyToProducts": [
            "default"
          ]
        }
      ]
    }
  ],
  "buildOption": {
    "externalNativeOptions": {
      "path": "./src/main/cpp/CMakeLists.txt",
      "arguments": "",
      "cppFlags": ""
    }
  }
}
预防措施
# 1. 检查 .gitignore 配置
cat .gitignore | grep build-profile.json5

# 2. 确保关键文件未被忽略
# 如果在 .gitignore 中,应该移除

# 3. 添加文件保护钩子
# .git/hooks/pre-commit
#!/bin/bash
if ! git diff --cached --name-only | grep -q "build-profile.json5"; then
  if [ ! -f "build-profile.json5" ]; then
    echo "警告:build-profile.json5 不存在!"
    exit 1
  fi
fi

八、开发总结与展望

8.1 质量升级路径

┌─────────────────────────────────────────────────────────┐
│                  应用成熟度演进模型                        │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  Level 1: 可用               Level 2: 稳定               │
│  • 基础功能实现                • 异常处理完善             │
│  • 基本界面展示                • 边界情况覆盖             │
│  • 数据能够流转                • 错误友好提示             │
│                                                         │
│           ↓                         ↓                   │
│                                                         │
│  Level 3: 完善               Level 4: 卓越               │
│  • 细节精心打磨                • 性能极致优化             │
│  • 多端完美适配                • 交互如丝般顺滑           │
│  • 体验流畅自然                • 用户愉悦惊喜             │
│                                                         │
└─────────────────────────────────────────────────────────┘

8.2 下一步规划

阶段 目标 技术重点
当前 多端适配、异常处理 Grid System、ErrorHandler
下一阶段 数据持久化 RelationalStore、Preferences
未来 性能优化、分布式 数据缓存、跨设备同步

8.3 核心收获

  1. 响应式设计不是简单的"适配",而是根据设备特性改变信息呈现效率
  2. 异常处理是用户体验的重要组成部分,需要系统性地设计
  3. 跨端对比帮助我们理解不同技术栈的优劣,做出更好的技术选型
  4. 工程化思维比单纯的编码能力更重要,包括问题排查、配置管理、团队协作

九、资源链接

  • HarmonyOS 开发者文档: https://developer.huawei.com/consumer/cn/doc/harmonyos-guides
  • React Navigation 官方文档: https://reactnavigation.org
  • React Native 性能优化: https://reactnative.dev/docs/performance
  • 社区论坛: https://openharmonycrossplatform.csdn.net

结语:从"功能实现"到"产品级应用",不仅是代码质量的提升,更是产品思维的升华。好的应用应该像水一样,自然地适应各种容器,同时在每一个细节处都体现出对用户的体贴。期待与你在下一章节继续探讨数据持久化与性能优化的奥秘!

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐