前言

本次参加开源鸿蒙跨平台开发学习活动,选择了 React Native 开发 HarmonyOS技术栈,在学习的同时顺便整理成一份系列笔记,记录从环境到开发的全过程。本篇作为第七篇,在前几篇内容中,我们已经完成了 React Native 在 HarmonyOS 端的基础环境搭建与底部 TabBar 的构建。本篇将继续推进 GitCode 口袋工具的开发,重点整理 仓库列表展示(Star 列表)、网络层封装、仓库卡片组件 RepoItem、首页渲染与路由组织结构。

本篇内容完全基于 React Native 0.72 与 @react-native-oh/react-native-harmony 运行环境,代码均可直接在 HarmonyOS NEXT 中运行。

一、项目背景与目标

为了更好地验证 HarmonyOS 上的 React Native 生态与体验,我制作了一个 GitCode 口袋工具 Demo:

  • 首页:展示 GitCode 用户信息与“已 Star 仓库列表”

  • 自研 TabBar,纯手写 Navigation 与路由结构

  • 重点:
    ✔ 统一网络层(axios 拦截器、错误处理)
    ✔ RepoItem 仓库卡片组件复用
    ✔ FlatList 合理渲染长列表
    ✔ HarmonyOS 端 Metro 联调 + 离线包打包流程

开发环境依赖如下:

react-native@0.72.5
react@18.2.0
@react-native-oh/react-native-harmony@^0.72.90
Metro Bundler 需要额外合并 Harmony 特殊配置,这部分前文已处理,本篇不再重复。

二、统一网络层设计(Axios 拦截器)

移动应用的网络请求往往涉及 Token 注入、错误提示规范化等逻辑,没有统一管理很容易导致代码分散。本项目构建了一个 http 实例,集中处理:

  • 基础域名 baseURL

  • 超时时间

  • 注入 private-token

  • 错误统一文案格式化

代码如下:

import axios from 'axios';

const DEFAULT_TOKEN = 'YOUR_PRIVATE_TOKEN'; 
let privateToken = DEFAULT_TOKEN;

export function setPrivateToken(token?: string) {
  if (token) privateToken = token;
}

export const http = axios.create({
  baseURL: 'https://api.gitcode.com/api/v5/',
  timeout: 10000,
});

http.interceptors.request.use(config => {
  const headers = config.headers ?? {};
  headers['private-token'] = privateToken;
  config.headers = headers;
  return config;
});

http.interceptors.response.use(res => res, err => Promise.reject(err));

export function getErrorMessage(error: unknown): string {
  if (axios.isAxiosError(error)) {
    const status = error.response?.status;
    const data = error.response?.data as any;
    const msg = typeof data === 'string' ? data : data?.message || data?.error || error.message;
    return status ? `${status} ${msg}` : msg;
  }
  return String((error as any)?.message || error);
}

页面中只需关注 拿数据,无需关心 Token、错误处理等杂项,大幅减少模板代码。

三、数据类型与 API 封装

GitCode API 的仓库、用户结构较大,项目中只选取需要字段:

export type RepoOwner = {
  id?: string | number;
  login?: string;
  name?: string;
  avatar_url?: string;
  html_url?: string;
  type?: string;
};

export type Repo = {
  id: string | number;
  name?: string;
  path?: string;
  full_name?: string;
  description?: string;
  language?: string;
  stargazers_count?: number;
  watchers_count?: number;
  commits_count?: number;
  web_url?: string;
  html_url?: string;
  owner?: RepoOwner;
};

API 请求:

import {http} from './client';
import {UserProfile} from '../types/user';
import {Repo} from '../types/repo';

const PROFILE_PATH = 'users/qiaomu8559968';

export async function fetchUserProfile(): Promise<UserProfile> {
  const res = await http.get<UserProfile>(PROFILE_PATH);
  return res.data;
}

export async function fetchStarred(username: string): Promise<Repo[]> {
  const res = await http.get<Repo[]>(`users/${username}/starred`, {
    params: {access_token: 'YOUR_ACCESS_TOKEN'},
  });
  return res.data;
}

四、仓库卡片组件 RepoItem 封装

GitCode 的仓库卡片要展示:Logo、名称、Star 按钮、描述、语言、Stars、Commits。
我们保持组件结构干净、参数明确,方便后续复用。

export type RepoItemProps = {
  logo: string;
  name: string;
  description: string;
  language: string;
  stars: number;
  commits: number;
  isStarred: boolean;
  onToggleStar: () => void;
};

核心渲染内容如下(省略样式):

<View style={styles.container}>
  <Image source={{uri: logo}} style={styles.logo} />
  <View style={styles.content}>
    <View style={styles.row}>
      <Text style={styles.name}>{name}</Text>
      <TouchableOpacity onPress={onToggleStar} style={styles.starBtn}>
        <Text style={styles.starText}>{isStarred ? '★ Starred' : '☆ Star'}</Text>
      </TouchableOpacity>
    </View>

    {!!description && <Text style={styles.desc}>{description}</Text>}

    <View style={styles.footer}>
      {!!language && <Text style={styles.lang}>{language}</Text>}
      <Text style={styles.meta}>⭐ {stars}</Text>
      <Text style={styles.meta}>🔗 {commits}</Text>
    </View>
  </View>
</View>

注意:仓库卡片的根容器必须设置 width: '100%',否则 FlatList 会出现空白间距。

五、首页整合:并发加载 + FlatList 渲染

为了避免 ScrollView 嵌套列表导致的虚拟化错误,本项目采用 FlatList 作为页面根容器,顶部用户信息用 ListHeaderComponent 渲染。

示例代码:

useEffect(() => {
  let mounted = true;
  setLoading(true);
  Promise.all([fetchUserProfile(), fetchStarred('qiaomu8559968')])
    .then(([d, s]) => {
      if (!mounted) return;
      setUser(d);
      setStarred((s || []).map(r => ({...r, isStarred: true})));
    })
    .catch(e => setError(getErrorMessage(e)))
    .finally(() => setLoading(false));

  return () => { mounted = false; };
}, []);

FlatList:

<FlatList
  data={starred}
  keyExtractor={(item, index) => String(item.id ?? index)}
  renderItem={({item}) => (
    <RepoItem
      logo={item.owner?.avatar_url || 'https://cdn-img.gitcode.com/logo.png'}
      name={item.name || item.path || item.full_name || '未知仓库'}
      description={item.description || ''}
      language={item.language || ''}
      stars={item.stargazers_count || 0}
      commits={item.commits_count || item.watchers_count || 0}
      isStarred={!!item.isStarred}
      onToggleStar={() => {
        setStarred(prev =>
          prev.map(r =>
            (r.id ?? r.name) === (item.id ?? item.name)
              ? {...r, isStarred: !r.isStarred}
              : r,
          ),
        );
      }}
    />
  )}
  ListHeaderComponent={<UserInfoHeader data={user} />}
  contentContainerStyle={styles.listContent}
/>

六、HarmonyOS 调试与打包关键点

1. 开发调试(Metro)

npm run start
hdc rport tcp:8081 tcp:8081

设备上点击 Reload 拉取 bundle:

http://localhost:8081/index.bundle?platform=harmony

2. 打离线包

npx react-native bundle-harmony --dev false

编译产物:

harmony/entry/src/main/resources/rawfile/bundle.harmony.js

确保文件正确包含,或推送:

/data/storage/el2/base/files/bundle.harmony.js

七、常见问题与解决方案

1. ScrollView 嵌套 FlatList 触发虚拟化错误

解决:整个页面使用 FlatList,顶部内容用 ListHeaderComponent

2. 列表项宽度撑不满

解决:

  • RepoItem 设置 width: '100%'

  • FlatList 设置合适的 contentContainerStyle

3. Token 安全

解决:不要硬编码 Token,正式环境使用安全注入或环境变量。

最后实现效果:

八、可扩展方向

  • 分页加载 / 下拉刷新

  • Skeleton 骨架屏提升加载体验

  • 封装 useRequest 统一管理加载、错误、刷新逻辑

  • 抽象 RepoItem 为更通用的列表项组件

总结

本篇主要完成了 GitCode 口袋工具的核心内容:

  • 网络层统一封装
  • RepoItem 组件化设计
  • FlatList 高性能渲染
  • HarmonyOS Metro 调试与离线包流程

随着仓库列表结构已搭建完毕,下一篇将继续围绕导航体系、更多页面拆分与项目结构优化展开,为后续的功能扩展打下基础。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐