参考文章

【开源鸿蒙跨平台开发学习笔记】Day04:React Native 开发 HarmonyOS-GitCode口袋工具开发-2-CSDN博客

开源鸿蒙-基于React搭建GitCode口袋工具-2-CSDN博客

本文记录操作流程以及遇到的问题

一、GitCode的API认证

在api文件夹内,我们创建一个client.ts文件用来封装网络请求:

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);
}

1.1 更改user.tsx文件

在AwesomeProject\src\api,更改user.tsx文件

原文件内容:

import axios from 'axios';
import {UserProfile} from '../types/user';
 
const BASE_URL = 'https://api.gitcode.com/api/v5/users';
 
export async function fetchUserProfile(username: string): Promise<UserProfile> {
  const res = await axios.get<UserProfile>(`${BASE_URL}/${encodeURIComponent(username)}`, {timeout: 10000});
  return res.data;
}

更改后:

import {http} from './client';
import {UserProfile} from '../types/user';
 
const PROFILE_PATH = 'users/weinxi_74220422';
 
export async function fetchUserProfile(username: string): 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;
}

1.2 更改HomeScreen.tsx或者ExploreScreen.tsx文件

在AwesomeProject\src\screens目录下,更改文件

看你喜欢放在哪个页面就改哪个页面的代码,我这里把原文件都粘下来,方便换

我这里选择把修改的代码放到ExploreScreen.tsx文件,如果放到HomeScreen.tsx要记得更改引用的路径

HomeScreen.tsx原文件:

import React from 'react';
import {View, Text, StyleSheet} from 'react-native';
 
interface HomeScreenProps {}
 
export function HomeScreen(_: HomeScreenProps) {
  return (
    <View style={styles.container}>
      <Text style={styles.title}>Hello World</Text>
      <Text style={styles.subTitle}>React Native + Harmony</Text>
    </View>
  );
}
 
const styles = StyleSheet.create({
  container: {alignItems: 'center', justifyContent: 'center'},
  title: {fontSize: 22, fontWeight: '700', color: '#111827'},
  subTitle: {marginTop: 8, fontSize: 16, color: '#374151'},
});

ExploreScreen.tsx原文件:

import React, {useEffect, useState} from 'react';
import {View, Text, StyleSheet, Image, ActivityIndicator, ScrollView, TouchableOpacity, Linking} from 'react-native';
import {fetchUserProfile} from '../api';
import {UserProfile} from '../types/user';
 
interface ExploreScreenProps {
  username?: string;
}
 
export function ExploreScreen({username = 'weinxi_74220422'}: ExploreScreenProps) {
  const [data, setData] = useState<UserProfile | null>(null);
  const [isLoading, setIsLoading] = useState<boolean>(false);
  const [hasError, setHasError] = useState<string>('');
 
  useEffect(function load() {
    let mounted = true;
    setIsLoading(true);
    setHasError('');
    fetchUserProfile(username)
      .then(d => {
        if (!mounted) return;
        setData(d);
      })
      .catch(e => {
        if (!mounted) return;
        setHasError(String(e?.message || e));
      })
      .finally(() => {
        if (!mounted) return;
        setIsLoading(false);
      });
    return function cleanup() {
      mounted = false;
    };
  }, [username]);
 
  if (isLoading) {
    return (
      <View style={styles.center}>
        <ActivityIndicator />
        <Text style={styles.loadingText}>加载中</Text>
      </View>
    );
  }
 
  if (hasError) {
    return (
      <View style={styles.center}>
        <Text style={styles.errorText}>请求失败:{hasError}</Text>
      </View>
    );
  }
 
  if (!data) {
    return (
      <View style={styles.center}>
        <Text style={styles.errorText}>暂无数据</Text>
      </View>
    );
  }
 
  return (
    <ScrollView contentContainerStyle={styles.scrollContent} style={styles.scroll}>
      <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>}
      <TouchableOpacity onPress={() => Linking.openURL(String(data.html_url))} style={styles.linkButton} activeOpacity={0.9}>
        <Text style={styles.linkText}>打开主页</Text>
      </TouchableOpacity>
    </ScrollView>
  );
}
 
const styles = StyleSheet.create({
  center: {flex: 1, alignItems: 'center', justifyContent: 'center', backgroundColor: '#FFFFFF'},
  loadingText: {marginTop: 8, fontSize: 14, color: '#666'},
  errorText: {fontSize: 14, color: '#d00'},
  scroll: {flex: 1, backgroundColor: '#FFFFFF'},
  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'},
});

修改代码:

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';
 
// 已经是 ExploreScreen 和默认导出
export function ExploreScreen(): 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('Deng666'), fetchStarred('Deng666')])
      .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;
    };
  }, []);
 
  // 安全的打开链接函数
  const safeOpenURL = (url: string | undefined) => {
    if (url && typeof url === 'string' && url.trim() !== '') {
      Linking.openURL(url);
    } else {
      console.warn('无效的 URL:', url);
    }
  };
 
  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={() => safeOpenURL(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={() => safeOpenURL(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'},
});

二、编译

执行编译的命令npm run dev,将生成的"\AwesomeProject\harmony\entry\src\main\resources\rawfile"目录下的"bundle.harmony.js"拷贝到鸿蒙项目里的"rawfile"目录下

出现报错

HomeScreen.tsx文件

原因可能是用户名缺失或者传少了一个username参数

解决方法:

修改user.tsx文件的代码,顺便看看HomeScreen.tsx文件的用户参数有没有写漏

我这里就是user.tsx文件传少了一个参数,HomeScreen.tsx文件少了引用了一个用户参数

import {http} from './client';
import {UserProfile} from '../types/user';
 
const PROFILE_PATH = 'users/weinxi_74220422';
 
export async function fetchUserProfile(username: string): 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;
}

HomeScreen.tsx文件:

解决方法:

使用非空断言

<Pressable
  onPress={() => Linking.openURL(data.html_url!)}
  style={styles.linkButton}>
  <Text style={styles.linkText}>打开主页</Text>
</Pressable>

三、成功完成

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐