HarmonyOS黑马云音乐项目增加网络听歌功能(一、轮播图的实现)

黑马云音乐项目是个不错的练手项目。但网上的素材都是本地的,听歌资源有限。在本文中,猫哥将带大家为HarmonyOS黑马云音乐项目增加网络听歌功能,丰富听歌的体验。

使用nutpi/axios三方库来调用后台音乐接口. 后台接口由猫哥提供,仅用于学习研究。同时本文猫哥将介绍如何使用后台接口获取数据来实现轮播图功能。欢迎大家继续完善打造良好的免费听歌新体验。

猫哥提供的免费鸿蒙后台接口地址:

https://blog.csdn.net/yyz_1987/article/details/154404554

猫哥提交的黑马云音乐项目地址:

https://gitee.com/yyz116/heimayun

nutpi/axios库介绍

nutpi/axios库是一个坚果派贡献的三方库,基于axios库的封装。比封装之前的方式更好用了。在HarmonyOS项目中,它非常适合用于发起网络请求。通过封装一个axiosClient,我们可以统一管理和配置请求,使代码更加简洁和易于维护。

三方库安装

要在HarmonyOS项目中安装nutpi/axios库,需要在项目的oh-package.json5文件中添加依赖。以下是添加依赖的方法:

json 复制代码
"dependencies": {
    "@nutpi/axios": "1.0.4"
}
实现音乐API

我们将定义两个API来从后台获取热歌榜和推荐歌单。

API接口定义

javascript 复制代码
//api/music.ets
import { HttpPromise } from '@nutpi/axios';
import { axiosClient } from '../../ulits/axiosClient';
import { HotMusicResp } from '../model/HotMusicResp';
import { MusicMenuResp } from '../model/MusicMenuResp';

// 1. 获取新歌热歌榜接口
export const getHotMusics = (start: number, count: number): HttpPromise<HotMusicResp> => 
    axiosClient.post({ url: '/musicmenus', data: { start: start, count: count, kind: 'topWyMusic' } });

// 2. 获取推荐歌单接口
export const getMusicMenu = (start: number, count: number): HttpPromise<MusicMenuResp> => 
    axiosClient.get({ url: '/getsongmenu', params: { start: start, count: count } });

以上实现是不是很简单?两行代码即实现了上述两个接口。

以上的HotMusicResp 和MusicMenuResp 根本不用写,都是根据接口响应的json自动转换生成的。如何生成的?使用jsonFormat插件。

如何使用jsonFormat插件?

拷贝接口响应的json内容,然后使用jsonFormat插件自动生成MusicMenuResp.ets的代码。网络接口部分是最简单的,是不是一分钟不到就写完了一个接口?


最终生成的MusicMenuResp.ets文件内容如下:

bash 复制代码
export interface MusicMenuResp {
	code: number;
	message: string;
	data: MusicMenuRespData[];
	count: number;
	start: number;
	total: number;
	title: string;
}
export interface MusicMenuRespDataSongs {
	sid: string;
	song: string;
	sing: string;
	url: string;
	cover: string;
}
export interface MusicMenuRespData {
	title: string;
	cover: string;
	user: string;
	note: string;
	songs: MusicMenuRespDataSongs[];
}
在recommend.ets页面中使用API

recommend.ets文件是我们将使用定义的API来获取和显示数据的地方。

接口定义

首先,我们定义一个 interface SwiperData 来结构化轮播图的数据:

typescript 复制代码
interface SwiperData {
  id: string;
  cover: string;
  url: string;
  song: string;
  sing: string;
}

在组件中使用接口

接下来,我们在recommend 组件中使用接口:

typescript 复制代码
@Preview
@Component
struct recommend {
  // 轮播图数据
  @State swiperList: SwiperData[] = []
  @State dailyRecommend: recommendDailyType[] = []
  @State recommendList: recommendListType[] = []

  /**
   * 获取新歌榜
   */
  async getHotMusics() {
    try {
      console.info('获取新歌榜:');

      const res = await getHotMusics(0, 10);
      if (res.data.code === 0 && res.data.data) {
        this.swiperList = []
        for (const it of res.data.data) {
          this.swiperList.push({ id: it.sid, cover: it.cover, url: it.url, song: it.song, sing: it.sing } as SwiperData)
        }
      } else {
        promptAction.showToast({
          message: '获取新歌榜失败',
          duration: 2000
        });
      }
    } catch (error) {
      console.error('获取新歌榜失败:', JSON.stringify(error as Error));
      promptAction.showToast({
        message: '获取新歌榜失败',
        duration: 2000
      });
    }
  }

  /**
   * 获取推荐歌单
   */
  getMusicMenus() {
    getMusicMenu(0, 10).then((res) => {
      Log.debug(res.data.message)
      Log.debug("request", "res.data.code:%{public}d", res.data.code)
    })
      .catch((err: BusinessError) => {
        Log.debug("request", "err.data.code:%d", err.code)
        Log.debug("request", err.message)
      });
  }

  aboutToAppear(): void {
    this.dailyRecommend = dailyRecommend
    this.recommendList = recommendList
    this.getHotMusics()
    this.getMusicMenus()
  }
}

解释

  • getHotMusics 方法 : 这是一个异步函数,用于从后台获取热歌榜数据。它调用 getHotMusics API,处理响应数据,并更新 swiperList 状态。如果请求失败或返回错误码,则会显示一个提示框(toast)消息,指示获取失败。

  • getMusicMenus 方法 : 这个方法用于从后台获取推荐歌单数据,调用 getMusicMenu API。它会在控制台输出响应消息和状态码。如果发生错误,会输出错误码和错误消息。

  • aboutToAppear 方法 : 这个生命周期方法在组件即将显示在屏幕上时被调用。我们在这里初始化状态变量,并调用 getHotMusicsgetMusicMenus 方法来获取数据。

axiosClient .ets客户端封装

axiosClient .ets是使用nutpi/axios库封装的一个客户端类。

为什么使用了nutpi/axios网络库后还要再这么封一下?因为拦截器和自定义全局配置,每个人的不一样,还需要灵活的改。当然如果你不用拦截器的话就不需要封装它了,直接new AxiosHttpRequest就可以用了。

该封装完成后,放到utils文件夹下,作为一个通用的封装直接拿来使用即可。

typescript 复制代码
//axiosClient.ets
import {
  AxiosError,
  AxiosHeaders,
  AxiosHttpRequest,
  AxiosRequestHeaders,
  AxiosResponse,
  FileUploadConfig,
  HttpPromise,
  UploadFile
} from '@nutpi/axios';
import { Log } from './logutil';
import { promptAction, router } from '@kit.ArkUI';

function showToast(msg: string) {
  Log.debug(msg)
  promptAction.showToast({ message: msg })
}

function showLoadingDialog(msg: string) {
  Log.debug(msg)
  // promptAction.showToast({ message: msg })
}

function hideLoadingDialog() {

}

// 移除硬编码的token,改为动态获取
// AppStorage.SetOrCreate('Authorization', "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Miwibmlja25hbWUiOm51bGwsImF2YXRhciI6bnVsbCwiaWF0IjoxNzU0NjY2ODk3LCJleHAiOjE3NTQ3NTMyOTd9.32Kp1pCt-HIqKglOpLrRhOMdSjyonutEDkifQRaoZCo");

// 图片地址的前缀
//export const IMAGE_BASE_URL = 'https://source.felin.maotuai.com' // 正式环境
// export const IMAGE_BASE_URL = 'https://source.felintest.maotuai.com'   // 测试环境

// 通用响应数据结构(兼容 code 与 success 协议)
interface CommonResponse {
  code?: number;
  success?: boolean;
  message?: string;
}

/**
 * axios请求客户端创建
 */
const axiosClient = new AxiosHttpRequest({
  baseURL: "http://120.27.146.247:8000/api/v1", //正式环境
  // baseURL: "http://xqytest.maotuai.com/api",  //测试环境
  timeout: 10 * 1000,
  checkResultCode: false,
  showLoading: true,
  headers: new AxiosHeaders({
    'Content-Type': 'application/json'
  }) as AxiosRequestHeaders,
  interceptorHooks: {
    requestInterceptor: async (config) => {
      // 在发送请求之前做一些处理,例如打印请求信息
      // console.log('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>')
      console.log('🌏网络请求Request 请求方法:', `${config.method}`);
      console.log('🌏网络请求Request 请求链接:', `${config.baseURL}${config.url}`);
      console.log('🌏网络请求Request Params:', `${JSON.stringify(config.params)}`);
      console.log('🌏网络请求Request Data:', `${JSON.stringify(config.data)}`);
      // console.log('<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<')

      // 动态获取token
      //const token = AppAuthManager.getToken();
      // if (token && config.headers) {
      //   config.headers['Authorization'] = 'Bearer ' + token;
      //   // console.log('token','Bearer ' + token)
      // }

      // 添加设备ID到请求头,用于多点登录和安全机制辅助校验
      if (config.headers) {
        try {
          //const deviceId = await DeviceManager.getDeviceId();
          //config.headers['uid'] = deviceId;
          // console.log('🔧 [登录系统] 网络请求添加设备ID (uid):', deviceId);
          //Log.debug('网络请求添加设备ID:', deviceId);
        } catch (error) {
          console.error('❌ [登录系统] 获取设备ID失败:', JSON.stringify(error));
          Log.error('获取设备ID失败:', JSON.stringify(error));
        }
      }

      axiosClient.config.showLoading = config.showLoading
      if (config.showLoading) {
        showLoadingDialog("加载中...")
      }
      if (config.checkLoginState) {
        // 检查登录状态
        // if (!AppAuthManager.getToken())
        // {
        //   if (config.needJumpToLogin) {
        //     // 可以在这里跳转到登录页面
        //     // router.replaceUrl({ url: 'pages/LoginPage' });
        //   }
        //   throw new Error("请登录")
        // }
      }

      return config;
    },
    requestInterceptorCatch: (err:Error) => {
      console.error("网络请求RequestError", err.toString())
      if (axiosClient.config.showLoading) {
        hideLoadingDialog()
      }
      return err;
    },
    responseInterceptor: (response:AxiosResponse<CommonResponse>) => {
      // 优先执行自己的请求响应拦截器,再执行通用请求的
      if (axiosClient.config.showLoading) {
        hideLoadingDialog()
      }
      console.log('🌏网络请求response:', `${JSON.stringify(response)}`);
      console.log('🌏网络请求response Data:', `${JSON.stringify(response.data)}`);
      if (response.status === 200) {
        const data: CommonResponse = response.data as CommonResponse;
        const ok = (data.code === 0 || data.success === true);
        if (!ok) {
          errorHandle(response);
          return Promise.reject(response);
        }
        return Promise.resolve(response);
      }
      return Promise.reject(response);
    },
    responseInterceptorCatch: (error:Error) => {
      if (axiosClient.config.showLoading) {
        hideLoadingDialog()
      }
      console.error("网络请求响应异常", error.toString());
      errorHandler(error);
      return Promise.reject(error);
    },
  }
});

// 处理业务错误
async function errorHandle(response:AxiosResponse<CommonResponse>) {
  const data: CommonResponse = response.data as CommonResponse;
  switch (data.code) {
    case 401:
      await handle401Error();
      break;
    // 403 token过期
    // 登录过期对用户进行提示
    // 清除本地token和清空vuex中token对象
    // 跳转登录页面
    case 403:
      showToast("登录过期,请重新登录")
      handleLogout();
      break;
    // 404请求不存在
    case 404:
      showToast("网络请求不存在")
      break;
    default:
      const message: string = data.message ?? '';
      if (message && !message.includes('用户未加入任何家庭')) {
        showToast(message);
      }
      break
  }
}

// 处理401错误 - 尝试续票
async function handle401Error() {
  try {
    Log.info('检测到401错误,开始续票处理');
    // if (!renewalSuccess) {
    //   Log.warn('401错误续票失败,跳转到登录页面');
    //   await handleLogout();
    // } else {
    //   Log.info('401错误续票成功,用户可继续使用');
    // }
  } catch (error) {
    Log.error('处理401错误失败:', error);
    await handleLogout();
  }
}

// 统一的登出处理
async function handleLogout() {
  try {
    // 清除token
    // 跳转到登录页面
    router.replaceUrl({ url: 'pages/LoginPage' });
  } catch (error) {
    console.error('登出处理失败:', JSON.stringify(error));
    // 即使出错也要跳转到登录页面
    router.replaceUrl({ url: 'pages/LoginPage' });
  }
}

interface DataItem {
  message: string;
}
interface ErrorItem {
  status: number;
  data:DataItem;
}
interface CustomError extends Error {
  response?: ErrorItem;
}
async function errorHandler(error: Error) {
  if (error instanceof AxiosError) {
    //showToast(error.message)
  } else if (error != undefined ) {
    const err:CustomError = error as CustomError
    if(err.response != undefined && err.response.status){
      switch (err.response.status) {
        // 401: 未登录
        // 未登录则跳转登录页面,并携带当前页面的路径
        // 在登录成功后返回当前页面,这一步需要在登录页操作。
        case 401:
          await handle401Error();
          break;
        // 403 token过期
        // 登录过期对用户进行提示
        // 清除本地token和清空vuex中token对象
        // 跳转登录页面
        case 403:
          showToast("登录过期,请重新登录")
          handleLogout();
          break;
        // 404请求不存在
        case 404:
          showToast("网络请求不存在")
          break;

        // 其他错误,直接抛出错误提示
        default:
          if (err.response.data.message) {
            // 过滤掉不需要显示的提示信息
            const message:string = err.response.data.message;
            if (message && !message.includes('用户未加入任何家庭')) {
              showToast(message);
            }
          }
      }
    }
  }
}

// 导出供其他模块使用
export { axiosClient, HttpPromise, UploadFile, FileUploadConfig };
结论

在本文中,我们成功地为HarmonyOS黑马云音乐项目增加了网络听歌功能,并使用nutpi/axios库实现了推荐歌单和热歌榜的获取。我们还通过后台接口获取的数据实现了轮播图功能。这种方法不仅提升了用户体验,还使代码库更加易于管理和扩展。

相关推荐
jenchoi4132 小时前
【2025-11-03】软件供应链安全日报:最新漏洞预警与投毒预警情报汇总
网络·安全·web安全·网络安全
java 乐山2 小时前
蓝牙网关(备份)
linux·网络·算法
金鸿客2 小时前
鸿蒙线性布局Row和Column详解
harmonyos
ifeng09183 小时前
HarmonyOS实战项目:打造沉浸式AR导航应用(空间计算与虚实融合)
ar·harmonyos·空间计算
EasyGBS3 小时前
EasyGBS助力智慧医院打造全方位视频监控联网服务体系
网络·音视频
z10_143 小时前
海外住宅ip怎么区分干净程度以及怎么选择海外住宅ip
服务器·网络·网络协议·tcp/ip
进击的圆儿4 小时前
10个Tcp三次握手四次挥手题目整理
网络·tcp/ip
KKKlucifer4 小时前
身份安全纵深防御:内网隐身、动态授权与全链路审计的协同技术方案
网络·安全
沐浴露z5 小时前
详解 零拷贝(Zero Copy):mmap、sendfile、DMA gather、splice
java·网络·操作系统