1.解决跨域问题
反向代理:在本地node服务器开启cors,负责请求的转发和数据接受回传

服务器之间不存在跨域问题!!!(自己的接口可以访问网易云音乐服务器上的数据)
1.1本地接口准备(服务器搭建)
node搭建的服务,如何将数据请求回来:
收到请求后,伪造身份(请求头部),请求网易云api拿到数据
目标:启动本地node服务,拿到数据
(下载(见文档))下载完的接口文件夹中没有第三方包(无node-modules),需要一次性安装所有包:yarn(npm i)
启动服务器:node app.js
2.前端项目准备
目标:初始化项目,下载必备包,引入初始文件,配置按需自动引入Vant
- 初始化工程 (vue create music-demo)
- 下载所需第三方包 axios vant vue-router :yarn add axios vant vue-router
- 下载Vant自动按需引入插件 babel-plugin-import:yarn add babel-plugin-import -D
- 在babel.config.js配置 – 看Vant文档
- 引入提前准备好的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 axios2.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.js2.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转成rem2.新建postcss.config.js – 设置相关配置![]()
3.重启服务器, 再次观察Vant组件是否适配
本文详细介绍了如何利用Vue.js创建前端页面,包括首页、搜索和播放页面,同时结合Node.js搭建本地服务器解决跨域问题。通过反向代理在本地接口转发请求,实现数据的获取。在前端,初始化Vue项目,引入Vant UI库,创建底部导航和顶部导航组件,并实现标题切换。此外,还封装了网络请求,将接口调用集中在一处,便于管理和维护。在搜索功能中,实现了热搜关键字的显示、搜索结果的加载以及触底加载更多。文章还涉及到了组件的复用,如SongItem组件的封装,以及歌曲点击跳转到播放页面的功能。整个过程涵盖了前端路由配置、组件交互、数据请求和响应式设计等多个方面。













9780

被折叠的 条评论
为什么被折叠?



