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库实现了推荐歌单和热歌榜的获取。我们还通过后台接口获取的数据实现了轮播图功能。这种方法不仅提升了用户体验,还使代码库更加易于管理和扩展。

相关推荐
老前端的功夫1 小时前
前端浏览器缓存深度解析:从网络请求到极致性能优化
前端·javascript·网络·缓存·性能优化
4***14901 小时前
HarmonyOS在智能电视中的语音交互
华为·harmonyos·智能电视
p66666666683 小时前
vmware虚拟机的三种网络配置详细介绍,包能解决虚拟机网络问题
网络
赖small强4 小时前
【Linux 网络基础】Linux 平台 DHCP 运作原理与握手过程详解
linux·网络·dhcp
Mu.3874 小时前
计算机网络模型
网络·网络协议·计算机网络·安全·http·https
xixixi777776 小时前
解析一下传输安全——“它是什么”,更是关于“它为何存在”、“如何实现”以及“面临何种挑战与未来”
网络·安全·通信
威哥爱编程7 小时前
鸿蒙6开发中,UI相关应用崩溃常见问题与解决方案
harmonyos·arkts·arkui
威哥爱编程7 小时前
鸿蒙6开发视频播放器的屏幕方向适配问题
harmonyos·arkts·arkui
威哥爱编程7 小时前
HarmonyOS 6.0 蓝牙实现服务端和客户端通讯案例详解
harmonyos
威哥爱编程7 小时前
鸿蒙6开发中,通过文本和字节数组生成码图案例
harmonyos·arkts·arkui