vant组件库——网易云音乐案例

本文详细介绍了如何利用Vue.js创建前端页面,包括首页、搜索和播放页面,同时结合Node.js搭建本地服务器解决跨域问题。通过反向代理在本地接口转发请求,实现数据的获取。在前端,初始化Vue项目,引入Vant UI库,创建底部导航和顶部导航组件,并实现标题切换。此外,还封装了网络请求,将接口调用集中在一处,便于管理和维护。在搜索功能中,实现了热搜关键字的显示、搜索结果的加载以及触底加载更多。文章还涉及到了组件的复用,如SongItem组件的封装,以及歌曲点击跳转到播放页面的功能。整个过程涵盖了前端路由配置、组件交互、数据请求和响应式设计等多个方面。

1.解决跨域问题

反向代理:本地node服务器开启cors,负责请求的转发和数据接受回传

 服务器之间不存在跨域问题!!!(自己的接口可以访问网易云音乐服务器上的数据)

1.1本地接口准备(服务器搭建)

node搭建的服务,如何将数据请求回来

收到请求后,伪造身份(请求头部),请求网易云api拿到数据

目标:启动本地node服务,拿到数据

文档:网易云音乐 NodeJS 版 API

(下载(见文档))下载完的接口文件夹中没有第三方包(无node-modules),需要一次性安装所有包:yarn(npm i)

启动服务器:node app.js


 2.前端项目准备

目标:初始化项目,下载必备包,引入初始文件,配置按需自动引入Vant

  1. 初始化工程 (vue create music-demo)
  2. 下载所需第三方包 axios vant vue-router :yarn add axios vant vue-router
  3. 下载Vant自动按需引入插件 babel-plugin-import:yarn add babel-plugin-import -D
  4. 在babel.config.js配置 – 看Vant文档
  5. 引入提前准备好的reset.css, flexible.js 到 main.js使用
    import "@/mobile/flexible" //移动端适配
    import "@/styles/reset.css" //初始化样式

3.需求分析

 

创建4个页面组件:

头部及底部导航:views/Layout/index.vue

首页:views/Home/index.vue
搜索:views/Search/index.vue
播放:views/Play/index.vue (预先准备)

4.路由准备

目标:准备路由配置,显示不同路由页面

router/index.js – 配置路由规则和对应路由页面

// 路由-相关模块
import Vue from 'vue'
import VueRouter from 'vue-router'
import Layout from '@/views/Layout'
import Home from '@/views/Home'
import Search from '@/views/Search'
import Play from '@/views/Play'

Vue.use(VueRouter)
const routes = [{
        path: '/',
        redirect: '/layout'
    },
    {
        path: '/layout',
        component: Layout,
        redirect: '/layout/home', //马上重定向到二级路由home
        children: [{
                path: 'home',
                component: Home,
                }
            },
            {
                path: 'search',
                component: Search,
            }
        ]
    },
    {
        path: '/play',
        component: Play
    }
]

const router = new VueRouter({
    routes
})

export default router

二级路由挂在到一级页面(src/views/layout/index.vue):

  <!-- 二级路由-挂载点 -->
      <router-view></router-view>
  main.js – 引入路由对象注入到vue中
import router from '@/router' //路由对象


new Vue({
    router,
    render: h => h(App),
}).$mount('#app')
App.vue – 留好router-view显示路由页面
<template>
  <div>
    <!-- 一级路由显示的地方 -->
    <router-view></router-view>
  </div>
</template>

5.Tabbar组件 

目标:点击底部导航,切换路由页面显示(详见文档)

 

src/views/layout/index.vue:

  


6.NavBar导航组件 

目标:实现顶部标题展示,点击底部按钮顶部标题切换显示

src/views/layout/index.vue:

 

6.1标题切换 

网页打开默认显示

1.在路由规则中添加meta元信息

 {
        path: '/layout',
        component: Layout,
        redirect: '/layout/home', //马上重定向到二级路由home
        children: [{
                path: 'home',
                component: Home,
                meta: { // meta保存路由对象额外信息的
                    title: "首页"
                }
            },
            {
                path: 'search',
                component: Search,
                meta: {
                    title: "搜索"
                }
            }
        ]
    },

2.src/views/layout/index.vue:

如何取到当前路由对象:this.$route  !!

<van-nav-bar :title="activeTitle" fixed />
export default {
  data() {
    return {
      activeTitle: this.$route.meta.title, // "默认"顶部导航要显示的标题 (默认获取当前路由对象里的meta中title值)
    };
  },
};

3.侦听路由切换显示对应标题

  // 路由切换 - 侦听$route对象改变
  watch: {
    $route() {
      this.activeTitle = this.$route.meta.title; // 提取切换后路由信息对象里的title显示
    },
  },

7.网络请求封装*

目标:网络请求,不散落在各个逻辑页面中,封装起来方便以后修改

1.utils/request.js – 对axios进行二次封装, 并且制定项目的根地址,导出axios函数

// 网络请求 - 二次封装
import axios from 'axios'
axios.defaults.baseURL = "http://localhost:3000"
export default axios
2.api/Home.js – 统一管理所有需要的 url地址 , 封装 网络请求 方法 导出
// 文件名-尽量和模块页面文件名统一(方便查找)
import request from '@/utils/request'

// 首页 - 推荐歌单
export const recommendMusic = params => request({
    url: '/personalized',
    params
    // 将来外面可能传入params的值 {limit: 20}
})

// 首页 - 推荐最新音乐
export const newMusic = params => request({
    url: "/personalized/newsong",
    params
})
3.api/index.js – 统一导出 接口
// api文件夹下 各个请求模块js, 都统一来到index.js再向外导出
import {recommendMusic, newMusic} from './Home'

export const recommendMusicAPI = recommendMusic // 请求推荐歌单的方法导出
export const newMusicAPI = newMusic // 首页 - 最新音乐

4.在main.js – 引入API方法请求测试,async+await等待axios的结果回来***

//测试封装的api方法
import { recommendMusicAPI } from '@/api'
async function fn() {
    const res = await recommendMusicAPI()
        //api方法原地会得到axios请求在原地的Promise对象(里面有一个ajax请求)
    console.log(res);
}
fn()

8.首页——推荐歌单  

1.布局van-row和van-col

 

 

van-image显示图片, p标签显示歌名
2.引入api里的网络请求方法, 把数据请求回来, 循环铺设
import { recommendMusicAPI} from "@/api";
export default {
  data() {
    return {
      reList: [], // 推荐歌单数据
    };
  },
  async created() {
    const res = await recommendMusicAPI({
      limit: 6,
    });
    console.log(res);
    this.reList = res.data.result;
}
    <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>

9.首页——最新音乐 

目标:完成最新音乐单元格列铺设

1.引入注册使用van-cell, 并且设置一套标签和样式准备(此处要将右边按钮变成字体图标利用了插槽

 

 2.引入icon播放按钮图标

 

 3.在api/Home.js –最新音乐的接口方法

// 首页 - 推荐最新音乐
export const newMusic = params => request({
    url: "/personalized/newsong",
    params
})

4.在api/index.js向外导出

// api文件夹下 各个请求模块js, 都统一来到index.js再向外导出
import {recommendMusic, newMusic} from './Home'
export const newMusicAPI = newMusic // 首页 - 最新音乐

5.引入到Home/index.vue中, 数据铺设到页面

<p class="title">最新音乐</p>
    <van-cell 
      center   
      :title="obj.name"
      v-for="obj in songList"
      :key="obj.id"
      :label="obj.song.artists[0].name + ' - '+ obj.name"
    >
       <template #right-icon>
           <van-icon name="play-circle-o" size= "0.6rem"/>
       </template>
</van-cell>
import { recommendMusicAPI, newMusicAPI } from "@/api";
export default {
  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;
  },

10.热搜关键字 

目标:完成搜索框和热搜关键字显示

1.搜索框 – van-search组件

 

2. api/Search.js – 热搜关键字 - 接口方法

// 搜索模块
import request from '@/utils/request'

// 热搜关键字
export const hotSearch = params => request({
    url: '/search/hot',
    params
})

3.在api/index.js向外导出

import {hotSearch} from './Search'
export const hotSearchAPI = hotSearch // 搜索 - 热搜关键词
4.Search/index.vue引入-获取热搜关键字 - 铺设页面
点击文字填充到输入框
    <van-search
      shape="round"
      v-model="value"
      placeholder="请输入搜索关键词"
    />
    <!-- 搜索下容器 -->
    <div class="search_wrap">
      <!-- 标题 -->
      <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>
import { hotSearchAPI} from "@/api";
export default {
  data() {
    return {
      value: "", // 搜索关键词
      hotArr: [], // 热搜关键字
    };
  },
  async created() {
    const res = await hotSearchAPI();
    console.log(res);
    this.hotArr = res.data.result.hots;
  },
  methods: {
    fn(val) {
      // 点击热搜关键词
      this.value = val; // 选中的关键词显示到搜索框
    },

11.搜索结果——点击获取

目标:匹配结果显示

1.api/Search.js - 搜索结果, 接口方法

// 搜索结果
export const searchResultList = params => request({
    url: '/cloudsearch',
    params
})
// api文件夹下 各个请求模块js, 都统一来到index.js再向外导出
import {hotSearch, searchResultList} from './Search'

export const hotSearchAPI = hotSearch // 搜索 - 热搜关键词
export const searchResultListAPI = searchResultList // 搜索 = 搜索结果

3.Search/index.vue引入-获取搜索结果 - 铺设页面

和热搜关键字容器 – 互斥显示 : v-if/v-else
点击文字填充到输入框, 请求搜索结果铺设
    <!-- 搜索下容器 -->
    <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-cell 
      center   
      :title="obj.name"
      v-for="obj in resultList"
      :key="obj.id"
      :label="obj.ar[0].name + ' - '+ obj.name"
    >
       <template #right-icon>
           <van-icon name="play-circle-o" size= "0.6rem"/>
       </template>
</van-cell>
    </div>
import { hotSearchAPI, searchResultListAPI } from "@/api";
export default {
  data() {
    return {
      value: "", // 搜索关键词
      hotArr: [], // 热搜关键字
      resultList: [], // 搜索结果
    };
  },
  async created() {
    const res = await hotSearchAPI();
    console.log(res);
    this.hotArr = res.data.result.hots;
  },
  methods: {
    async getListFn() {
      return await searchResultListAPI({
        keywords: this.value,
        limit: 20,
      }); // 把搜索结果return出去
      // (难点):
      // async修饰的函数 -> 默认返回一个全新Promise对象
      // 这个Promise对象的结果就是async函数内return的值
      // 拿到getListFn的返回值用await提取结果
    },
    async fn(val) {
      // 点击热搜关键词
      this.value = val; // 选中的关键词显示到搜索框
      const res = await this.getListFn();
      console.log(res);
      this.resultList = res.data.result.songs;
}
}

  


 12.输入框——搜索结果

目标:监测输入框改变

1.观察van-search组件是否支持和实现input事件

 2.绑定@input事件和方法

    <van-search
      shape="round"
      v-model="value"
      placeholder="请输入搜索关键词"
      @input="inputFn"
    />
 async inputFn() {
        // 输入框值改变
        if (this.value.length === 0) {
          // 搜索关键词如果没有, 就把搜索结果清空阻止网络请求发送(提前return)
          this.resultList = [];
          return;
        }
        const res = await this.getListFn();
        console.log(res);
        this.resultList = res.data.result.songs;
    },
3.在事件处理方法中获取对应的值使用
4.如果搜索不存在的数据-要注意接口返回字段不同
输入框没有值的时候不能显示请求列表数据 :
if (this.value.length === 0) {
    this.resultList = [];
              return;
   }

 13.搜索结果——加载更多

目标:触底后加载下一页数据

1.van-list组件监测触底执行onload事件

1.1引入、注册List组件(略)

如何分页?查看接口文档:

 offset: (this.page - 1) * 20, // 固定公式 (如:第二页会偏移20个数据)

 1.2使用

<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>
export default {
  data() {
    return {
      value: "", // 搜索关键词
      hotArr: [], // 热搜关键字
      resultList: [], // 搜索结果
      loading: false, // 加载中 (状态) - 只有为false, 才能触底后自动触发onload方法
      finished: false, // 未加载全部 (如果设置为true, 底部就不会再次执行onload, 代表全部加载完成)
      page: 1, // 当前搜索结果的页码
      timer: null // 输入框-防抖定时器
    };
  },
  methods: {
    async getListFn() {
      return await searchResultListAPI({
        keywords: this.value,
        limit: 20,
        offset: (this.page - 1) * 20, // 固定公式
      });
    async onLoad() {
      // 触底事件(要加载下一页的数据咯), 内部会自动把loading改为true
      this.page++;
      const res = await this.getListFn()
      this.resultList = [...this.resultList, ...res.data.result.songs];
      this.loading = false; // 数据加载完毕-保证下一次还能触发onload
    },
  },

2.配合后台接口, 传递下一页的标识

 this.page++;

3.拿到下一页数据后追加到当前数组末尾,即: 触底后, 请求下一页数据, 拼接到当前页面
 
 const res = await this.getListFn()
 this.resultList = [...this.resultList, ...res.data.result.songs];

 14.加载更多——bug修复 

目标:修复搜索无数据情况

1.无数据/只有一页数据, finished为true

    async inputFn() {
        // 输入框值改变
        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;
    },
    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
    },

2.防止list组件触底再加载更多 ,还要测试-按钮点击/输入框有数据情况的加载更多

(另外:加载更多时, page已经往后计数了,重新获取时, page不是从第一页获取的,

点击搜索/输入框搜索时, 把page改回1
 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加载更多
    },

总结:搜索结果分为三个模块:点击热词、输入框输入、list加载更多 


 16.防抖使用 

目标:修复输入框删除过快-效果错误

1.输入框输入"asdfghjkl"

2.接着快速的删除
     每次改变-马上发送网络请求
     网络请求异步耗时 – 数据回来后还是铺设到页面上
3. 解决:
    引入防抖功能
async inputFn() {
      // 目标: 输入框改变-逻辑代码-慢点执行
      // 解决: 防抖
      // 概念: 计时n秒, 最后执行一次, 如果再次触发, 重新计时
      // 效果: 用户在n秒内不触发这个事件了, 才会开始执行逻辑代码
      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)
    },

 17.SongItem封装

目标:搜索结果和首页使用了相同的标签结构

 

首页的最新音乐和搜索结果的音乐 ,标签样式功能相同

封装SongItem.vue到这2处复用即可
SongItem.vue:
<template>
  <van-cell center :title="name" :label="author + ' - ' + name">
    <template #right-icon>
      <van-icon name="play-circle-o" size="0.6rem" />
    </template>
  </van-cell>
</template>

<script>
export default {
  props: {
    name: String, // 歌名
    author: String, // 歌手
    id: Number, // 歌曲id (标记这首歌曲-为将来跳转播放页做准备)
  },
 
};
</script>

<style scoped>
/* 给单元格设置底部边框 */
.van-cell {
  border-bottom: 1px solid lightgray;
}
</style>

复用到首页中(Home/index.vue):

 <SongItem v-for="obj in songList"
    :key="obj.id"
    :name="obj.name"
    :author="obj.song.artists[0].name"
    :id="obj.id"
    ></SongItem>
import SongItem from '@/components/SongItem'
  components: {
    SongItem
  }

复用到搜索中(Search/index.vue):略


18.跳转播放 

目标:点击播放按钮——播放页面

1.组件SongItem里 – 点击事件

<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>

<script>
export default {
  props: {
    name: String, // 歌名
    author: String, // 歌手
    id: Number, // 歌曲id (标记这首歌曲-为将来跳转播放页做准备)
  },
  methods: {
    playFn(){
      this.$router.push({
        path: '/play',
        query: {
          id: this.id // 歌曲id, 通过路由跳转传递过去
        }
      })
    }
  }
};
</script>

<style scoped>
/* 给单元格设置底部边框 */
.van-cell {
  border-bottom: 1px solid lightgray;
}
</style>
2.api/Play.js – 提前准备好 – 接口方法
2.1接口方法放入api/play.js
2.2向外导出(api/index.js):
import {getSongById, getLyricById} from './Play'
export const getSongByIdAPI = getSongById // 歌曲 - 播放地址
export const getLyricByIdAPI = getLyricById // 歌曲 - 歌词数据

3.跳转到Play页面 – 把歌曲id带过进去

 跳转路由传参:path+query方法

  methods: {
    playFn(){
      this.$router.push({
        path: '/play',
        query: {
          id: this.id // 歌曲id, 通过路由跳转传递过去
        }
      })
    }

19.Vant组件适配

1.下载包:yarn add postcss postcss-pxtorem

       postcss – 配合webpack翻译css代码

       postcss-pxtorem – 配合webpack, 自动把px转成rem
2.新建postcss.config.js – 设置相关配置

 3.重启服务器, 再次观察Vant组件是否适配

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值