网易云音乐案例
案例目的了解如何引入网易云的nodeJS服务器通过网易云接口的部分功能实现简单的页面布局通过移动端案例逐渐熟练vant组件库1网易云音乐的本地接口下载网易云音乐node接口项目,在本地启动,为我们vue项目提供数据支持。并且在本地通过Nodemon启动服务,拿到数据,项目下载方式如下。https://binaryify.github.io/NeteaseCloudMusicApi/#/2网易云音乐
案例目的
- 了解如何引入网易云的nodeJS服务器
- 通过网易云接口的部分功能实现简单的页面布局
- 通过移动端案例逐渐熟练vant组件库
1网易云音乐的本地接口
下载网易云音乐node接口项目,在本地启动,为我们vue项目提供数据支持。并且在本地通过Nodemon启动服务,拿到数据,项目下载方式如下。
https://binaryify.github.io/NeteaseCloudMusicApi/#/
2网易云音乐案例的前端项目初始化
- 初始化工程vue create music-demo
- 下载所需第三方包 axios vant vue-router等
- 下载vant自动引入插件babel-plugin-import
- 在babel.config中配置相关参数
plugins: [
[‘import’, {
libraryName: ‘vant’,
libraryDirectory: ‘es’,
style: true
}, ‘vant’]
]
- 引入基础的css/less/js文件,对页面进行基础的布局,并把这些文件引入到main.js文件中
3网易云音乐的前端项目需求页面分析
核心思路是Layout/index负责布局,负责上下导航,中间挂载二级路由切换,实现首页和搜索页面的功能。所以Layout模块是核心模块。
Layout的顶部是一个导航栏,底部是tabbar的切换栏,并且用to属性来进行路由切换,非常方便。
<template>
<div>
<van-nav-bar :title="activeTitle" fixed />
<div class="main">
<!-- 二级路由-挂载点 -->
<router-view></router-view>
</div>
<van-tabbar route>
<van-tabbar-item replace to="/layout/home" icon="home-o"
>首页</van-tabbar-item
>
<van-tabbar-item replace to="/layout/search" icon="search"
>搜索</van-tabbar-item
>
</van-tabbar>
</div>
</template>
其中顶部navbar中接受从路由导航传过来的值router/index.js文件中的值。
{
path: '/layout',
component: Layout,
redirect: '/layout/home',
children: [{
path: 'home',
component: Home,
meta: { // meta保存路由对象额外信息的
title: "首页"
}
},
{
path: 'search',
component: Search,
meta: {
title: "搜索"
}
}
]
}
中间router-view负责挂载二级路由,分别是首页和搜索页面。播放页面play则挂载到一级路由上。以上便是整个项目的基础业务逻辑。其中设计到传值的知识点,也就是说从router/index.js文件中传递过来的title值,我需要接受并且返回给navbar.其中navbar和tabbar都是vant组件库中的组件,因此下面介绍中间的二级路由实现部分。
export default {
data() {
return {
activeTitle: this.$route.meta.title, // "默认"顶部导航要显示的标题 (默认获取当前路由对象里的meta中title值)
};
},
// 路由切换 - 侦听$route对象改变
watch: {
$route() {
this.activeTitle = this.$route.meta.title; // 提取切换后路由信息对象里的title显示
},
},
}
https://vant-contrib.gitee.io/vant/#/zh-CN/nav-bar navbar官方文档
https://vant-contrib.gitee.io/vant/#/zh-CN/tabbar tabbar官方文档
4网络请求api
- 首页Home中,推荐歌单api和推荐最新音乐api。前提是你已经把网易云的项目在本地下载下来,并且用node启动了这个项目,在我的项目中我用Node启动的端口号是3000,因此我可以通过本地3000的端口访问网易云音乐的所有功能
// 网络请求 - 可以先在一个单独的js文件中进行二次封装,然后把这些js文件导入到api目录下的文件中
import axios from 'axios'
axios.defaults.baseURL = "http://localhost:3000"
export default axios
// 首页 - 推荐歌单
export const recommendMusic = params => request({
url: '/personalized',
params
// 将来外面可能传入params的值 {limit: 20}
})
// 首页 - 推荐最新音乐
export const newMusic = params => request({
url: "/personalized/newsong",
params
})
为了养成良好的习惯,我们的做法一般是用axios发送请求,并且我的request就是包装了axios函数的js文件。
- 在搜索页面中,我们也需要用上面的方法导入两个接口,分别为热搜关键字以及搜索结果,后端的所有代码都是引入第三方文件,所以我们需要遵循后端给出的接口条件。
// 热搜关键字
export const hotSearch = params => request({
url: '/search/hot',
params
})
// 搜索结果
export const searchResultList = params => request({
url: '/cloudsearch',
params
})
- 其次是play播放页面,我们需要做的是获取歌曲详情和获取歌词,当然这不是我们案例的主要目的。我们案例主要目的是了解网易云音乐整体vue的思路布局。
// 播放页 - 获取歌曲详情
export const getSongById = (id) => request({
url: `/song/detail?ids=${id}`,
method: "GET"
})
// 播放页 - 获取歌词
export const getLyricById = (id) => request({
url: `/lyric?id=${id}`,
method: "GET"
})
- 最重要的一点是我导入了这么多的接口,那么我们怎么将这些接口向外导出呢?又是如何导出的呢?且听我慢慢道来。
首先我们把这些导入的接口名,全部统一放在一个js文件中
// api文件夹下 各个请求模块js, 都统一来到index.js再向外导出
import {recommendMusic, newMusic} from './Home'
import {hotSearch, searchResultList} from './Search'
import {getSongById, getLyricById} from './Play'
export const recommendMusicAPI = recommendMusic // 请求推荐歌单的方法导出
export const newMusicAPI = newMusic // 首页 - 最新音乐
export const hotSearchAPI = hotSearch // 搜索 - 热搜关键词
export const searchResultListAPI = searchResultList // 搜索 = 搜索结果
export const getSongByIdAPI = getSongById // 歌曲 - 播放地址
export const getLyricByIdAPI = getLyricById // 歌曲 - 歌词数据
然后在主页面中哪个地方需要用到,就在哪个地方引入这个api目录
import { recommendMusicAPI, newMusicAPI } from "@/api";
5移动端vant组件库的自动适配。
目标: Vant组件库自动适配
自动让所有px转成rem (以后我们可以直接写px) - webpack配合postcss和postcss-pxtorem插件就可以翻译css代码, 把px转rem使用
- 下载 postcss postcss-pxtorem@5.1.1 (要和当前脚手架webpack兼容)
- postcss.config.js - 填入插件转换的基准值
- 一定要重启服务器, 观察效果
module.exports = {
plugins: {
'postcss-pxtorem': {
// 能够把所有元素的px单位转成Rem
// rootValue: 转换px的基准值。
// 例如一个元素宽是75px,则换成rem之后就是2rem。
rootValue: 37.5,
propList: ['*']
}
}
}
6首页逻辑
<template>
<div>
<p class="title">推荐歌单</p>
<van-row gutter="6">
<van-col span="8" v-for="obj in reList" :key="obj.id">
<van-image width="100%" height="3rem" fit="cover" :src="obj.picUrl" />
<p class="song_name">{{ obj.name }}</p>
</van-col>
</van-row>
<p class="title">最新音乐</p>
<SongItem v-for="obj in songList"
:key="obj.id"
:name="obj.name"
:author="obj.song.artists[0].name"
:id="obj.id"
></SongItem>
</div>
</template>
其中没有什么深奥的知识,具有vue组件基础的应该很容易看得懂,至于说songItem这个组件,我们放在后面讲。
// 目标: 铺设推荐歌单
// . van-row和van-col来搭建外框布局
// . van-col里内容(van-image和p)
// . 调整间距和属性值
// . 调用封装的api/index.js-推荐歌单api方法
// . 拿到数据保存到data里变量-去上面循环标签
目标: 铺设最新音乐
.van-cell铺设一套标签结构
自定义右侧插槽里小图标, 调整垂直居中
api/Home.js和api/index.js-封装导出获取最新音乐接口方法
获取数据循环铺设页面即可
其中涉及到的数据位reList和SongList都是从后端拿过来的数据
data() {
return {
reList: [], // 推荐歌单数据
songList: [], // 最新音乐数据
};
},
async created() {
const res = await recommendMusicAPI({
limit: 6,
});
console.log(res);
this.reList = res.data.result;
const res2 = await newMusicAPI({
limit: 20
})
console.log(res2);
this.songList = res2.data.result
}
7搜索页面逻辑,最顶部是搜索框,采用vant组件
// 目标: 铺设热搜关键字
// 1. 搜索框van-search组件, 关键词标签和样式
// 2. 找接口, api/Search.js里定义获取搜索关键词的请求方法
// 3. 引入到当前页面, 调用接口拿到数据循环铺设页面
// 4. 点击关键词把值赋予给van-search的v-model变量
<van-search
shape="round"
v-model="value"
placeholder="请输入搜索关键词"
@input="inputFn"
/>
顶部下面采用的是热搜关键字,如果没有搜索结果,默认显示热搜关键字,如果有搜索结果就显示最终搜索结果
<!-- 搜索下容器 -->
<div class="search_wrap" v-if="resultList.length === 0">
<!-- 标题 -->
<p class="hot_title">热门搜索</p>
<!-- 热搜关键词容器 -->
<div class="hot_name_wrap">
<!-- 每个搜索关键词 -->
<span
class="hot_item"
v-for="(obj, index) in hotArr"
:key="index"
@click="fn(obj.first)"
>{{ obj.first }}</span
>
</div>
</div>
搜索结果如下代码所示
<!-- 搜索结果 -->
<div class="search_wrap" v-else>
<!-- 标题 -->
<p class="hot_title">最佳匹配</p>
<van-list
v-model="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<SongItem
v-for="obj in resultList"
:key="obj.id"
:name="obj.name"
:author="obj.ar[0].name"
:id="obj.id"
></SongItem>
</van-list>
</div>
8SongItem代表的是一个播放音乐的列表项
可以清除的看到其中包括title和图标,因此我们用van-call组件来实现
<template>
<van-cell center :title="name" :label="author + ' - ' + name">
<template #right-icon>
<van-icon name="play-circle-o" size="0.6rem" @click="playFn"/>
</template>
</van-cell>
</template>
并且在props中注明传递过来的参数要求
props: {
name: String, // 歌名
author: String, // 歌手
id: Number, // 歌曲id (标记这首歌曲-为将来跳转播放页做准备)
}
现在项目基础部分已经搭建完成,最重要的部分就是数据的传递流动方向。
- 首先来看最简单的热搜关键字模块,在created生命周期中异步调用API接口,并且把值赋值给hotArr
async created() {
const res = await hotSearchAPI();
console.log(res);
this.hotArr = res.data.result.hots;
}
- 其次在输入框中输入信息,得到后台的数据后,我们需要对数据进行铺设,也就是说铺设成SongItem的组件样式
// 目标: 铺设搜索结果
// 1. 找到搜索结果的接口 - api/Search.js定义请求方法
// 2. 再定义methods里getListFn方法(获取数据)
// 3. 在点击事件方法里调用getListFn方法拿到搜索结果数据
// 4. 铺设页面(首页van-cell标签复制过来)
// 5. 把数据保存到data后, 循环van-cell使用即可(切换歌手字段)
// 6. 互斥显示搜索结果和热搜关键词
async getListFn() {
return await searchResultListAPI({
keywords: this.value,
limit: 20,
offset: (this.page - 1) * 20, // 固定公式
}); // 把搜索结果return出去
// (难点):
// async修饰的函数 -> 默认返回一个全新Promise对象
// 这个Promise对象的结果就是async函数内return的值
// 拿到getListFn的返回值用await提取结果
},
拿到getList的返回值,并且用await提取结果。现在我们需要理解page的含义。
一个页面最多可能装20组数据,当滑到最底部时,可能出现两种情况,数据已经没了,或者还有更多的数据。如果还有更多的数据,此时就会调用onLoad的加载方法,把下一页的数据传递到前端。就比如说后端offset=20时,就代表是page=2是第二页,这时候就说明偏移了20个数据。整个页面有Limit+offset(40)个数据。所以我们需要设置一个变量finished来表示是否加载完毕,以及变量loading代表底部是否处于加载中。
其中涉及到的变量有,涉及到的方法为异步的inputfn
data() {
return {
value: "", // 搜索关键词
hotArr: [], // 热搜关键字
resultList: [], // 搜索结果
loading: false, // 加载中 (状态) - 只有为false, 才能触底后自动触发onload方法
finished: false, // 未加载全部 (如果设置为true, 底部就不会再次执行onload, 代表全部加载完成)
page: 1, // 当前搜索结果的页码
timer: null // 输入框-防抖定时器
};
}
当我们提到输入框,我们就需要注意对输入框进行防抖,简单来说就是使得代码慢点执行。计时n秒,最后执行一次,如果再次触发,重新计时。效果就是:用户在n秒内部触发这个事件,我们才会执行逻辑代码,用户输入完之后的n秒才会触发这个事件,并不是立马触发这个事件(这个事件指的就是把输入框的值传递给后端,从后端返回关键字的结果)
async inputFn() {
if (this.timer) clearTimeout(this.timer)
this.timer = setTimeout(async () => {
this.page = 1; // 点击重新获取第一页数据
this.finished = false // 输入框关键字改变-可能有新数据(不一定加载完成了)
// 输入框值改变
if (this.value.length === 0) {
// 搜索关键词如果没有, 就把搜索结果清空阻止网络请求发送(提前return)
this.resultList = [];
return;
}
const res = await this.getListFn();
console.log(res);
// 如果搜索结果响应数据没有songs字段-无数据
if (res.data.result.songs === undefined) {
this.resultList = [];return
}
this.resultList = res.data.result.songs;
this.loading = false;
}, 900)
},
防抖最简单方法就是设置定时器,如果这个定时器存在,那么就清除这个定时器,重新设置一个。当我们获取数据时,默认是获取第一页数据,并且判断有没有后端返回结果,如果没有的话,就直接return出去。如果有的话,就把数据传递给相关的变量。
- 底部加载事件
async onLoad() {
// 触底事件(要加载下一页的数据咯), 内部会自动把loading改为true
this.page++;
const res = await this.getListFn();
if (
res.data.result.songs === undefined
) { // 没有更多数据了
this.finished = true; // 全部加载完成(list不会在触发onload方法)
this.loading = false; // 本次加载完成
return;
}
this.resultList = [...this.resultList, ...res.data.result.songs];
this.loading = false; // 数据加载完毕-保证下一次还能触发onload
}
当到达底部时,就会重新调用getList方法,从后台拿数据,只不过Page的值变化了,如果没有更多的数据了,那就说明全部加载完毕了,设置为finished为true,并且将loading设置为false 表示此次加载完毕,以后van-list这个组件就不会触发load方法,因为finished的限制。如果还有数据,那么我们可以用ES6的快捷语法,采用数组的赋值操作。
- 热搜关键字的搜索
我们在上面说过,页面开始会展示热搜关键字,那么热搜关键字的实现原理是怎样的呢?
调用fn方法
async fn(val) {
// 点击热搜关键词
this.page = 1; // 点击重新获取第一页数据
this.finished = false; // 点击新关键词-可能有新的数据
this.value = val; // 选中的关键词显示到搜索框
const res = await this.getListFn();
console.log(res);
this.resultList = res.data.result.songs;
this.loading = false; // 本次数据加载完毕-才能让list加载更多
其余方法同上类似。以上就是整个代码的基本业务逻辑。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)