案例目的

  1. 了解如何引入网易云的nodeJS服务器
  2. 通过网易云接口的部分功能实现简单的页面布局
  3. 通过移动端案例逐渐熟练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使用

  1. 下载 postcss postcss-pxtorem@5.1.1 (要和当前脚手架webpack兼容)
  2. postcss.config.js - 填入插件转换的基准值
  3. 一定要重启服务器, 观察效果
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加载更多
    

其余方法同上类似。以上就是整个代码的基本业务逻辑。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐