欢迎大家加入[开源鸿蒙跨平台开发者社区](https://openharmonycrossplatform.csdn.net),一起共建开源鸿蒙跨平台生态。

前言

在参加「开源鸿蒙跨平台开发学习活动」的过程中,我选择了 React Native + HarmonyOS 这一技术路线。一方面是希望验证 React Native 在 OpenHarmony 场景下的实际可行性,另一方面也想借这个机会,把自己从环境搭建 → 基础开发 → 实战功能的完整过程系统性地整理下来,形成一套可复用的学习笔记。

到目前为止,这个系列已经完成了前几篇内容,包括:

  • 开发环境与工程结构搭建

  • React Native 在 HarmonyOS 中的基础运行

  • GitCode 用户信息等「无需认证接口」的联调

本篇作为第四篇,重点进入一个更接近真实 App 开发的阶段:
👉 GitCode API 的认证接入,以及需要授权的接口联调实践。

我们将以「GitCode 口袋工具 App」为例,实现已 Star 仓库列表的获取与展示,这一步也是整个 App 从“Demo”向“工具型产品”过渡的重要节点。

一、GitCode的API认证和用户star列表展示

上一篇中,我们已经成功联调了无需鉴权的用户信息接口,这一阶段更多是为了验证:

  • 网络请求是否能正常跑通

  • React Native 页面渲染是否稳定

  • HarmonyOS 设备上的网络权限是否配置正确

而从这一篇开始,就必须正式引入认证机制。

1. Star 仓库接口说明

GitCode 提供了如下接口,用于获取某个用户已经 Star 的仓库列表:

https://api.gitcode.com/api/v5/users/:username/starred

该接口的参数结构如下:

路径参数

参数名 类型 必填 说明
username string GitCode 用户名

请求参数

参数名 类型 必填 说明
access_token string 用户授权令牌
sort string created / last_push
direction string asc / desc
page number 当前页码
per_page number 每页数量(最大 100)

可以看到,这里第一次出现了必须携带 access_token 的接口。

也正是从这里开始,如果继续在每个接口里手动拼接 token,不仅代码冗余,而且后续维护成本会迅速上升。

因此,在真正请求这个接口之前,先对网络请求层做一次统一封装,是一个更合理的选择。

因为后续还需要对接很多需要access_token的接口所以我们要封装一下请求请求这块,然后续请求都携带这个access_token;

二、统一封装 GitCode 网络请求客户端

1. 为什么要做统一封装?

在实际项目中,网络请求通常会面临几个共性问题:

  • 多个接口都需要携带相同的认证信息

  • 错误处理逻辑高度重复

  • 后期 token 可能需要动态更新(登录 / 刷新)

如果每个接口都直接使用 axios.getfetch,这些问题会迅速放大。

因此,这里我选择在 src/api 目录下,单独抽出一个 client.ts,作为整个 App 的网络请求入口。

2. 项目目录结构回顾

根据上一篇我们创建的目录结构:

├── src/                         # 业务代码主目录
│   ├── api/                     # 网络请求层(Axios 封装、接口模块)
│   ├── assets/                  # 静态资源(图片、图标、字体、Lottie 动画等)
│   ├── components/              # 公共 UI 组件(按钮、卡片、头部栏等可复用组件)
│   ├── hooks/                   # 自定义 React Hooks(如 useAuth、useRequest)
│   ├── navigation/              # 导航配置(Stack、Tab、Drawer 路由)
│   ├── screens/                 # 页面(HomeScreen、LoginScreen、DetailScreen)
│   ├── store/                   # 状态管理(Redux/Zustand/MobX 等)
│   └── utils/                   # 工具函数(格式转换、常量、日期工具、权限工具)

其中,所有与 GitCode API 相关的请求,都统一放在 api 目录中。

3. client.ts:Axios 客户端封装

api 目录下新建 client.ts,用于完成以下几件事情:

  • 统一设置 GitCode API 的 baseURL

  • 自动在请求头中注入 private-token

  • 对网络异常进行统一格式化

import axios from 'axios';
/**
 * 统一的 Axios 客户端:
 * - 基地址指向 GitCode v5 API
 * - 请求拦截器自动注入 `private-token` 头,便于后续请求复用
 * - 响应拦截器透传数据/错误,错误统一由 `getErrorMessage` 格式化
 * - 提供 `setPrivateToken` 在运行时动态更新私有令牌
 */

// 默认令牌(仅用于开发调试),正式环境建议改为安全的配置或环境变量注入
const DEFAULT_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;
  }
  const e = error as any;
  return String(e?.message || e);
}

这样一来:

  • 所有接口自动携带 token

  • 错误信息在 UI 层可直接展示

  • 后续如果接入登录流程,只需要在登录成功后调用 setPrivateToken

三、封装用户相关 API 接口

有了统一的客户端后,具体接口就可以变得非常“干净”。

user.ts 中,我们只关注接口本身的语义:

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

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<any[]> {
  const res = await http.get<any[]>(`users/${username}/starred`);
  return res.data;
}

此时:

  • 不需要再关心 token

  • 不需要关心 baseURL

  • 接口文件只负责“数据获取”

四、首页联调:用户信息 + Star 仓库列表展示

1. 并行请求的设计思路

在首页 Home.tsx 中,我选择使用 Promise.all 同时请求:

  • 用户基本信息

  • 已 Star 仓库列表

这样可以减少整体等待时间,也更贴近真实 App 的加载体验。

Promise.all([fetchUserProfile(), fetchStarred('qiaomu8559968')])

同时通过 mounted 标记,避免组件卸载后仍然更新状态,这是 React Native 中一个非常实用的小细节。

2. UI 展示策略

UI 上的思路比较简单:

  • 顶部展示用户头像、昵称、简介

  • 下方以卡片形式展示已 Star 仓库

  • 每个仓库支持跳转到 GitCode 页面

即使样式还比较朴素,但已经具备了完整的产品雏形。

我们在user.ts中也添加了本次要用到的“users/${username}/starred”接口来获取用户star的仓库列表,然后再改造首页Home.tsx

import React, {useEffect, useState} from 'react';
import {
  View,
  Text,
  StyleSheet,
  Image,
  ActivityIndicator,
  ScrollView,
  Pressable,
  Linking,
} from 'react-native';
import {fetchUserProfile, fetchStarred} from '../api';
import {UserProfile} from '../types/user';
import {getErrorMessage} from '../api/client';

export default function Home(): JSX.Element {
  const [data, setData] = useState<UserProfile | null>(null);
  const [loading, setLoading] = useState<boolean>(false);
  const [error, setError] = useState<string>('');
  const [starred, setStarred] = useState<any[]>([]);

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

  if (loading) {
    return (
      <View style={styles.center}>
        <ActivityIndicator />
        <Text style={styles.loadingText}>加载中</Text>
      </View>
    );
  }

  if (error) {
    return (
      <View style={styles.center}>
        <Text style={styles.errorText}>请求失败:{error}</Text>
      </View>
    );
  }

  if (!data) {
    return (
      <View style={styles.center}>
        <Text style={styles.errorText}>暂无数据</Text>
      </View>
    );
  }

  return (
    <ScrollView contentContainerStyle={styles.scrollContent}>
      <Image source={{uri: data.avatar_url}} style={styles.avatar} />
      <Text style={styles.title}>{data.name || data.login}</Text>
      <Text style={styles.subtitle}>类型:{data.type}</Text>
      <Text style={styles.subtitle}>
        粉丝:{data.followers},关注:{data.following}
      </Text>
      {Boolean(data.bio) && <Text style={styles.bio}>{data.bio}</Text>}
      <Pressable
        onPress={() => Linking.openURL(data.html_url)}
        style={styles.linkButton}>
        <Text style={styles.linkText}>打开主页</Text>
      </Pressable>
      <View style={styles.listHeader}>
        <Text style={styles.listHeaderText}>已 Star 的仓库</Text>
      </View>
      {starred.map((item, idx) => {
        const name =
          item?.name || item?.path || item?.project_name || '未知仓库';
        const desc = item?.description || '';
        const link = item?.html_url || item?.web_url || item?.url || '';
        return (
          <View key={`${name}-${idx}`} style={styles.repoCard}>
            <Text style={styles.repoName}>{name}</Text>
            {!!desc && <Text style={styles.repoDesc}>{desc}</Text>}
            {!!link && (
              <Pressable
                onPress={() => Linking.openURL(link)}
                style={styles.repoLinkBtn}>
                <Text style={styles.repoLinkText}>访问仓库</Text>
              </Pressable>
            )}
          </View>
        );
      })}
    </ScrollView>
  );
}

const styles = StyleSheet.create({
  center: {flex: 1, alignItems: 'center', justifyContent: 'center'},
  loadingText: {marginTop: 8, fontSize: 14, color: '#666'},
  errorText: {fontSize: 14, color: '#d00'},
  scrollContent: {alignItems: 'center', paddingVertical: 24},
  avatar: {width: 120, height: 120, borderRadius: 60, backgroundColor: '#eee'},
  title: {marginTop: 16, fontSize: 24, fontWeight: '700'},
  subtitle: {marginTop: 8, fontSize: 16, color: '#666'},
  bio: {
    marginTop: 12,
    fontSize: 14,
    color: '#333',
    paddingHorizontal: 24,
    textAlign: 'center',
  },
  linkButton: {
    marginTop: 16,
    paddingHorizontal: 16,
    paddingVertical: 10,
    borderRadius: 6,
    backgroundColor: '#007aff',
  },
  linkText: {color: '#fff', fontSize: 14, fontWeight: '600'},
  listHeader: {width: '100%', paddingHorizontal: 24, paddingTop: 24},
  listHeaderText: {fontSize: 18, fontWeight: '600'},
  repoCard: {
    width: '92%',
    marginTop: 12,
    padding: 12,
    borderRadius: 8,
    backgroundColor: '#f7f7f7',
  },
  repoName: {fontSize: 16, fontWeight: '600'},
  repoDesc: {marginTop: 6, fontSize: 14, color: '#555'},
  repoLinkBtn: {
    marginTop: 10,
    alignSelf: 'flex-start',
    paddingHorizontal: 12,
    paddingVertical: 8,
    borderRadius: 6,
    backgroundColor: '#34c759',
  },
  repoLinkText: {color: '#fff', fontSize: 14, fontWeight: '600'},
});

就可以在首页加载出当前用户已经star的仓库列表了,加上之前我们做的用户信息展示,就已经很不错了呢。

五、效果与阶段性成果

到这里为止,我们已经完成了:

  • GitCode 授权接口的实际使用

  • 统一网络请求封装

  • React Native 在 HarmonyOS 下的接口联调

  • 首页数据完整展示

相比最初只能跑起一个页面,现在这个 App 已经真正“活”了起来。

✿✿ヽ(°▽°)ノ✿

总结

本篇主要围绕网络请求与认证接口联调展开,重点包括:

  • 统一请求头(private-token)的设计

  • 网络异常的集中处理

  • Star 仓库列表接口的完整落地

完成这些之后,「GitCode 口袋工具 App」已经不再只是一个 Demo,而是开始具备工具型应用的基本形态了。

下一步,就可以继续向分页、缓存、组件拆分、状态管理等更真实的业务方向演进了。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐