黑马云音乐项目是个不错的练手项目。但网上的素材都是本地的,听歌资源有限。在本文中,猫哥将带大家为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 方法 : 这是一个异步函数,用于从后台获取热歌榜数据。它调用
getHotMusicsAPI,处理响应数据,并更新swiperList状态。如果请求失败或返回错误码,则会显示一个提示框(toast)消息,指示获取失败。 -
getMusicMenus 方法 : 这个方法用于从后台获取推荐歌单数据,调用
getMusicMenuAPI。它会在控制台输出响应消息和状态码。如果发生错误,会输出错误码和错误消息。 -
aboutToAppear 方法 : 这个生命周期方法在组件即将显示在屏幕上时被调用。我们在这里初始化状态变量,并调用
getHotMusics和getMusicMenus方法来获取数据。
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库实现了推荐歌单和热歌榜的获取。我们还通过后台接口获取的数据实现了轮播图功能。这种方法不仅提升了用户体验,还使代码库更加易于管理和扩展。