成品下载
在本地访问使用浏览器直接访问其他服务器资源存在跨域问题
更多详细解释参阅 ajax文档
通过反向代理可解决跨域问题
浏览器会阻拦非同源策略的请求,但可以使用非浏览器的方式的去访问,期间无跨域问题。
使用本地服务器去请求目标服务器数据,网页再从本地服务器获取接收到的数据
思路
- 本地node服务器开启cors,负责请求的转发和数据接收回传
使用第三方 网易云音乐 API,负责请求的回传
Node搭建的服务收到请求后,伪造身份,请求网易云API拿到数据
项目链接:https://github.com/Binaryify/NeteaseCloudMusicApi
文档链接:https://binaryify.github.io/NeteaseCloudMusicApi/#
安装
下载项目依赖 (在项目根目录 右键+shift 运行cmd 输入指令 安装所需依赖)
npm install
运行
node ./app.js
上述操作完成后,服务器 默认运行在本地的 3000端口:直接访问
初始化项目,下载必备包,引入初始文件,配置Vue
搭建基础vue设施,为后面的样式和功能实现的铺设作准备
流程
- 初始化工程 (vue create music-demo)
- 下载所需第三方包 axios vant vue-router
- 下载Vant自动按需引入插件 babel-pugin-import
- 在 babel.config.js 进行 vant 自动引入配置
- 引入提前准备好的 reset.css,flexible.js 到 main.js 使用
操作
创建项目 引入包
# 创建 vue脚手架环境
vue create music-demo
# 下载 axios vant组件(vue2) vue-router路由(vue2)
yarn add axios vant@latest-v2 vue-router@3.5.2
# 下载 应用在开发环境下的 babel-plugin-import 插件
yarn add babel-plugin-import -D
配置自动按需引入插件 babel.config.js
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
],
plugins: [
['import', {
libraryName: 'vant',
libraryDirectory: 'es',
style: true
}, 'vant']
]
}
下载第三方包 flexible.js、reset.css (替换src下的内容)
用途:适配移动端的js框架
mian.js 引用第三方包
// 适配
import '@/mobile/flexible.js'
// 初始化样式
import '@/styles/reset.css'
分析所需要的页面,并创建对应页面文件
根据需求分析要完成的模块大致效果,创建4个页面组件,CSS页面可参考并复制于 文档笔记
文件创建,按照以下格式
- views/Layout/index.vue
- views/Home/index.vue
- views/Search/index.vue
- views/Play/index.vue (下载文件)
参考右侧格式 →
* Play/index.js 不需要创建,只需要单独下载 *
已知问题:Play/index.vue 文件 引用的 “@/api” 需要去除,否则报错
CSS代码
<style scoped>
/* 中间内容区域 - 容器样式(留好上下导航所占位置) */
.main {
padding-top: 46px;
padding-bottom: 50px;
}
</style>
<style scoped>
/* 标题 */
.title {
padding: 0.266667rem 0.24rem;
margin: 0 0 0.24rem 0;
background-color: #eee;
color: #333;
font-size: 15px;
}
/* 推荐歌单 - 歌名 */
.song_name {
font-size: 0.346667rem;
padding: 0 0.08rem;
margin-bottom: 0.266667rem;
word-break: break-all;
text-overflow: ellipsis;
display: -webkit-box; /** 对象作为伸缩盒子模型显示 **/
-webkit-box-orient: vertical; /** 设置或检索伸缩盒对象的子元素的排列方式 **/
-webkit-line-clamp: 2; /** 显示的行数 **/
overflow: hidden; /** 隐藏超出的内容 **/
}
</style>
<style scoped>
/* 搜索容器的样式 */
.search_wrap {
padding: 0.266667rem;
}
/*热门搜索文字标题样式 */
.hot_title {
font-size: 0.32rem;
color: #666;
}
/* 热搜词_容器 */
.hot_name_wrap {
margin: 0.266667rem 0;
}
/* 热搜词_样式 */
.hot_item {
display: inline-block;
height: 0.853333rem;
margin-right: 0.213333rem;
margin-bottom: 0.213333rem;
padding: 0 0.373333rem;
font-size: 0.373333rem;
line-height: 0.853333rem;
color: #333;
border-color: #d3d4da;
border-radius: 0.853333rem;
border: 1px solid #d3d4da;
}
</style>
准备路由配置,显示不同路由页面
为每个页面设置路由在路由配置中配置对应 path路径 等配置
流程
- router/index.js 配置路由规则和路由对应路由界面
- main.js 引入路由对象注入到 vue实例 中
- App.vue 放置 <router-view> 显示路由页面
代码演示
配置路由对象 (下载/引入/注册/规则/路由对象/注入/显示)
import Vue from 'vue';
import VueRouter from 'vue-router';
import Layout from '@/views/Layout/index.vue';
import Home from '@/views/Home/index.vue';
import Search from '@/views/Search/index.vue';
import Play from '@/views/Play/index.vue';
Vue.use(VueRouter);
const routes = [
{
path: '/',
redirect: '/layout',
},
{
path: '/layout',
component: Layout,
children: [
{
path: 'home',
component: Home,
},
{
path: 'search',
component: Search,
}
]
},
{
path: '/play',
component: Play,
}
];
const router = new VueRouter({
routes
});
// 向外导出
export default router;
导入路由配置对象
// 接收 路由对象
import router from '@/router/index.js';
new Vue({
router,
render: h => h(App),
}).$mount('#app')
创建挂载点 (并为对应路由挂载二级路由 挂载点)
<template>
<div>
<router-view></router-view>
</div>
</template>
<template>
<div>
我是layout
<router-view></router-view>
</div>
</template>
效果展示
点击 底部导航,切换路由页面显示
让 Tabbar组件 在 Layout.vue 底部显示 Tabbar组件笔记 、Tabbar相关文档
流程
- 按需引入 Tabbar、TabbarItem 到 main.js,并在 Layout/index.vue 使用
- 点击后,实现 Tabbar组件 配合路由切换页面功能
代码演示
全局引入 Tabber组件
import { Tabbar, TabbarItem } from 'vant';
Vue.use(Tabbar);
Vue.use(TabbarItem);
使用 <van-tabbar> 标签,并加入 router 属性 和 replace + to 集成路由切换功能
<template>
<div>
<router-view></router-view>
<van-tabbar v-model="active" router>
<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>
<script>
export default {
data(){
return{
active:0
}
}
}
</script>
效果展示
实现 顶部标题 展示的组件
NavBar 是头部区域的标题,NavBar组件笔记、NavBar文档
流程
- 按需引入 NavBar 到 main.js,并在 Layout/index.vue 使用
- 实现头部 NavBar 样式的铺设
代码演示
全局引入 NavBar组件
import { NavBar } from 'vant';
Vue.use(NavBar);
使用 <van-nav-bar> 铺设
<template>
<div>
<van-nav-bar title="首页" />
...
</div>
</template>
效果展示
在路由配置中,给路由设置 元信息 meta.title
通过路由切换,侦听路由切换显示对应标题,实现让 NavBar 指示当前组件标题
流程
- 在 main.js ,为需要配置的路由对象设置 meta 元信息,传入 title 值,供组件使用
- NavBar组件 使用 :title 方式 与 this.$route.meta.title 绑定,watch 监听该值变化并同步变化
代码演示
修改 路由配置,增加 meta元信息
const routes = [
...
{
path: '/layout',
component: Layout,
redirect: '/layout/home',
children: [
{
path: 'home',
component: Home,
// meta 保存路由对象额外信息的对象
meta: {
title: '首页'
}
},
{
path: 'search',
component: Search,
meta: {
title: '搜索'
}
}
]
},
...
];
在 Layout/index.vue,为<van-nav-bar> 的 title 绑定 $route.meta.title
<template>
<div>
<van-nav-bar
:title="activeTitle" />
<router-view></router-view>
<van-tabbar
v-model="active"
router>
<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>
<script>
export default {
data() {
return {
active: 0,
activeTitle: this.$route.meta.title,
}
},
watch: {
$route() {
this.activeTitle = this.$route.meta.title;
}
}
}
</script>
效果展示
网络请求不应散落在各个逻辑页面,封装起来方便以后修改
通过业务的不同,创建多个文件夹;其中包含 总请求地址、公共请求导出模块、各页面请求模块
流程
- utils/request.js 对 axios 进行二次封装,进行指定项目的根地址
- api/Home.js 统一管理所有需要的 url地址,封装网络请求的方法并导出
- api/index.js 统一导出接口
代码演示
创建 src/utils/request.js 导出二次封装 axios模块
// 导入 axios包
import axios from 'axios';
// 公共请求地址 (需要存在 网易云音乐api 项目)
axios.defaults.baseURL='http://43.140.202.112:3000'
// 向外导出
export default axios;
创建 src/api 文件夹作为接口文件夹,并创建 src/api/home.js
import request from '@/utils/request.js';
// 首页 - 获取推荐歌单
export const recommendMusic = params => request({
url:'/personalized',
// ES6语法 同名对象成员 传参可简写
params,
});
创建总接口导出模块 src/api/index.js 用于直接导出所有 目录下接口
/*
* 各个请求模块的js 都导入到该模块中 用于向外导出
* 方便 全局直接引用
*/
import { recommendMusic } from './Home.js';
//请求推荐歌单方法导出
export const recommendMusicAPI = recommendMusic;
在 main.js 引入 @/api (同 ./api/index.js) 进行测试,发起请求
import { recommendMusicAPI } from '@/api';
// 定义异步的函数
async function fn() {
// 等待异步请求获取到值 赋值给 res
const res = await recommendMusicAPI();
// 打印结果
console.log(res);
}
fn();
效果展示
从 服务器成功取到数据并打印
使用网络请求数据,并完成首页推荐歌单界面铺设
流程
- 布局 van-row 和 van-col
- van-image 显示图片,p 标签显示歌名
- 引入 api 里的网络请求方法,把数据请求回来,循环铺设
代码演示
全局 按需引入 Col、Row组件,和 Image组件 (需换名)
import { Col, Row , Image as VanImage } from 'vant';
Vue.use(Col);
Vue.use(Row);
铺设 推荐歌单页面 src/Home/index.vue
<template>
<div>
<p class="title">推荐歌单</p>
<van-row gutter="6">
<van-col span="8" v-for="item in reList" :key="item.id">
<van-image
width="100%"
height="3rem"
fit="cover"
:src="item.picUrl" />
<p class="song_name">{{ item.name }}</p>
</van-col>
</van-row>
</div>
</template>
<script>
import { recommendMusicAPI } from '@/api';
export default {
async created() {
// 等待获取成功服务器的数据 赋值给 res
const res = await recommendMusicAPI({
// 指定从服务器获取6条数据
limit: 6,
});
// 将结果赋值给 data 中的 reList;
this.reList = res.data.result;
},
data() {
return {
reList: [],
}
}
}
</script>
效果展示
使用网络请求数据,并完成首页最新歌单界面铺设
流程
- 引入注册使用 van-cell,并且设置一套标签和样式准备
- 使用官方文档说明的插槽方式,为 van-ceil 标签右侧内嵌 Icon组件图标
- api/Home.js 编写最新音乐的接口方法
- 引入到 Home/index.vue 中,数据铺设到页面
代码演示
全局 按需引入 Cell组件 和 Icon组件
import { Cell, Icon } from 'vant';
Vue.use(Cell);
Vue.use(Icon);
在 src/Home/index.vue 按原基础再次铺设样式
<template>
<div>
...
<p class="title">最新音乐</p>
<van-cell title="单元格" label="内容" center>
<!-- 使用 right-icon 插槽来自定义右侧图标 -->
<template #right-icon>
<van-icon name="play-circle-o" size="0.7rem" />
</template>
</van-cell>
</div>
</template>
<script>
...
</script>
<style scoped>
...
/* 给单元格设置底部边框 */
.van-cell {
border-bottom: 1px solid lightgray;
}
</style>
在 src/api/Home.js 添加 最新音乐 请求接口方法
import request from '@/utils/request.js';
...
// 首页 - 推荐最新音乐
export const newMusic = params => request({
// ES6语法 同名对象成员 传参可简写
url: '/personalized/newsong',
params,
})
引入到 总接口文件 api/index.js 方便其他页面使用 @/api 引用
import { recommendMusic, newMusic } from './Home.js';
//请求推荐歌单方法导出
...
export const newMusicAPI = newMusic;
在 Home/index.vue 引入接口方法,并发起数据请求,铺设界面
<p class="title">最新音乐</p>
<div class="content">
<van-cell
:title="item.name"
:label="item.song.artists[0].name + ' - ' + item.name"
center v-for="item in songList" :key="item.id">
<!-- 使用 right-icon 插槽来自定义右侧图标 -->
<template #right-icon>
<van-icon name="play-circle-o" size="0.7rem" />
</template>
</van-cell>
</div>
</div>
</template>
<script>
import { recommendMusicAPI, newMusicAPI } from '@/api';
export default {
async created() {
...
// 等待获取成功服务器的数据 赋值给 res2
const res2 = await newMusicAPI({
limit: 20,
});
this.songList = res2.data.result;
},
data() {
return {
...
songList: [],
}
}
}
</script>
效果展示
完成搜索框和热搜关键字显示
在 Seach/index.vue 铺设搜索栏目的路由页面,并将搜索结果显示页面,与关键词区域互斥显示
流程
- 使用 van-search组件 铺设 Seach/index.vue 界面
- api/Search.js 创建 热搜关键字接口方法
- 将 Search.js 导入到公共API总入口文件,方便 @/api 调用
- 引入接口文件,发起请求,并将返回的结果 铺设到界面
- 搜索框监听 @input 输入事件,实现基本的输入时动态显示内容
代码模拟
按需引入 vant组件库的 van-search组件
import { Search } from 'vant';
Vue.use(Search);
Search/index.vue 使用 van-search 标签,实现搜索框样式
<template>
<div>
<van-search v-model="value" shape="round" placeholder="请输入搜索关键词" />
<!-- 搜索下容器 -->
<div class="search_wrap">
<!-- 标题 -->
<p class="hot_title">热门搜索</p>
<!-- 热搜关键词容器 -->
<div class="hot_name_wrap">
<!-- 关键词 -->
<span class="hot_item">海底</span>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
value: '',
}
}
}
</script>
阅读接口文档规则,创建 api/Search.js 实现热搜关键词接口业务
import request from '@/utils/request.js';
// 热门关键词 请求封装
export const hotSearch = params => request({
url: '/search/hot',
params,
});
// 搜索关键词 请求封装
export const searchResultList = params => request({
url: '/cloudsearch',
params,
})
将 Search.js 导入到 接口总入口 api/index.js,方便使用 @/api 调用
...
import { hotSearch , searchResultList } from './Search.js';
...
// 热搜关键词方法导出
export const hotSearchAPI = hotSearch;
// 热搜关键词搜索结果
export const searchResultListAPI = searchResultList;
使用接口,将返回数据配置并铺设至 Search/index.vue 页面的对应位置
<template>
<div>
<van-search v-model="value" shape="round" placeholder="请输入搜索关键词" />
<!-- 搜索下容器 -->
<div class="search_wrap">
<!-- 标题 -->
<p class="hot_title">热门搜索</p>
<!-- 热搜关键词容器 -->
<div class="hot_name_wrap">
<!-- 关键词 -->
<span class="hot_item"
v-for="(item, index) in hotArr"
:key="index"
@click="fn(item.first)">
{{ item.first }}
</span>
</div>
</div>
</div>
</template>
<script>
import { hotSearchAPI, searchResultListAPI } from '@/api';
export default {
data() {
return {
value: '',
hotArr: [],
}
},
async created() {
const res = await hotSearchAPI();
this.hotArr = res.data.result.hots;
},
methods: {
fn(val) {
this.value = val;
}
}
}
</script>
撰写关键词搜索接口的实现,点击 关键词 或 搜索栏填值 发起关键词详细搜索
最终将结果显示在界面,并关键词与搜索结果的区域互斥显示
<template>
<div>
<van-search v-model="value"
shape="round"
placeholder="请输入搜索关键词"
@input="inputFn" />
<!-- 搜索下容器 -->
<div class="search_wrap" v-if="!resultList.length">
<!-- 标题 -->
<p class="hot_title">热门搜索</p>
<!-- 热搜关键词容器 -->
<div class="hot_name_wrap">
<!-- 关键词 -->
<span class="hot_item"
v-for="(item, index) in hotArr"
:key="index"
@click="fn(item.first)">
{{ item.first }}
</span>
</div>
</div>
<div class="search_wrap" v-else>
<p class="hot_title">最佳匹配</p>
<div class="hot_name_wrap">
<van-cell :title="item.name"
:label="item.ar[0].name + ' - ' + item.name"
v-for="item in resultList"
:key="item.id">
<!-- 使用 right-icon 插槽来自定义右侧图标 -->
<template #right-icon>
<van-icon name="play-circle-o"
size="0.6rem" />
</template>
</van-cell>
</div>
</div>
</div>
</template>
<script>
import { hotSearchAPI, searchResultListAPI } from '@/api';
export default {
data() {
return {
value: '',
hotArr: [],
resultList: [],
}
},
async created() {
// 异步获取请求数据
const res = await hotSearchAPI();
this.hotArr = res.data.result.hots;
},
methods: {
// 向服务器发起对应热门关键词内容的数据请求
async getListFn() {
// 异步发起请求 返回请求数据
return await searchResultListAPI({
keywords: this.value,
limit: 20,
});
},
/*
点击关键词后 发起请求 并将搜索得到的数据赋值到 resultList
数据发生改变后 vue 会将新的内容 铺设到页面
*/
async fn(val) {
this.value = val;
const res = await this.getListFn();
this.resultList = res.data.result.songs;
},
// 输入框发生改变时 若搜索框存在内容时发起请求 否则 清空原数据和页面并结束
async inputFn() {
if (!this.value.trim()) {
this.resultList = [];
return
}
// 异步获取请求数据
const res = await this.getListFn();
this.resultList = res.data.result.songs;
}
}
}
</script>
效果展示
1.实现单击后关键词显示在搜索框
2.单击关键词时发起请求并铺设内容
3.监听输入框输入事件,发起请求
当滑动搜索结果的内容触底后,向服务器提交并加载下一页的内容
在 热搜关键词 Search/index.vue 页面提交关键词返回的数据,在合适的时间向服务器请求更多内容的加载
说明
van-list组件 具有监听滚动触底事件的效果;
常用于展示长列表,当列表即将滚动到底部时,会触发事件并加载更多列表项
List 组件通过 loading
和 finished
两个变量控制加载状态,
当组件滚动到底部时,会触发 load
事件并将 loading
设置成 true
。
此时可以发起异步操作并更新数据,数据更新完毕后,
将 loading
设置成 false
即可。若数据已全部加载完毕,
则直接将 finished
设置成 true
即可。
流程
- 使用 van-list组件,实现监听用户滑动滚动区域的触底。相关文档
- 按需引入 van-list组件 注册全局,并在 Search/index.vue 中以 <van-list> 使用
- 设置 page 值,默认为 1,方便传递参数中 offset 判断取值范围 ( (page-1)*页数量 )
- 发起请求时候,传递额外的参数 offset ,服务器返回 offset 截取顺序往后内容
- 配置 loading 值,在请求数据完成后呈现 显示/隐藏 内置加载动画( 在获取完成数据时改为 false )
代码模拟
引入 van-list 组件到 main.js
// 按需导入
import { List } from 'vant';
...
Vue.use(List);
van-list组件 嵌套在 van-cell组件 外侧,方便实现监听滚动触底
<template>
...
<!-- 当监听到组件滚动到底部时会触发 load 事件并 loading 的值被修改为 true -->
<!-- 可在操作事项完成后将 loading 改为 false 方便组件再次监听滚动到底部事件 -->
<!-- 若内容已经全部加载完毕 可将 finished 设置为 true 呈现数据加载完成效果 -->
<van-list v-model="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad">
<van-cell
:title="item.name"
:label="item.ar[0].name + ' - ' + item.name"
v-for="item in resultList"
:key="item.id">
<!-- 使用 right-icon 插槽来自定义右侧图标 -->
<template #right-icon>
<van-icon name="play-circle-o"
size="0.6rem" />
</template>
</van-cell>
</van-list>
...
</template>
<script>
export default {
data() {
return {
...
loading: false, // 加载中(状态) 只有为 false 才能触底后自动触发 onload 方法
finished: false, // 加载全部完成
}
...
}
</script>
依赖 page页 + offset参数,判断需要取的内容,实现加载更多业务
<script>
import { hotSearchAPI, searchResultListAPI } from '@/api';
export default {
data() {
return {
...
resultList: [],
loading: false, // 加载中(状态) 只有为 false 才能触底后自动触发 onload 方法
finished: false, // 加载全部完成
page: 1, // 当前搜索结果页数
}
},
...
methods: {
// 向服务器发起对应热门关键词内容的数据请求
async getListFn() {
// 异步发起请求 返回请求数据
return await searchResultListAPI({
keywords: this.value,
limit: 20,
// 偏移值 数据截取该值之后下标的内容
offset: (this.page - 1) * 20,
});
},
/*
点击关键词后 发起请求 并将搜索得到的数据赋值到 resultList
数据发生改变后 vue 会将新的内容 铺设到页面
*/
...
async onLoad() {
this.page++;
const res = await this.getListFn();
// ES6 语法 合并对象数组
this.resultList = [...this.resultList, ...res.data.result.songs];
// 数据加载完毕 允许再次响应 滚动触底事件
this.loading = false;
}
}
}
</script>
效果展示
当向服务器发送关键词搜索请求返回无内容时,后续操作应终止
用户输入关键词无内容返回,应清空界面数据,并终止后续业务;
滚动触底发起加载更多的业务请求返回无内容后,应呈现 “没有更多内容” 的效果
流程
- 数据返回后,判断返回的对象内容是否存在有效数据,否则清空界面内容、终止后续代码业务
- 所有包含发起加载更多的业务请求返回无结果后,将 finished 加载完成状态设置为 true
- [初始化] 每次事件函数发起请求前(除滚动触底事件函数) 都将加载完成状态的 finished 设置为 false
- 设置初始化是加载完成状态 是为了重置下次搜索关键词内容时候,允许加载更多的操作
代码模拟
添加判断条件,服务器返回的指定内容为 undefined
if (res.data.result?.songs === undefined) {
// 执行的操作
...
// 结束后续业务
return
}
修改输入框内容发生改变事件函数,与滚动触底时触发的事件函数等
methods: {
...
async fn(val) {
// 初始化状态 - 允许加载更多
this.finished = false;
this.value = val;
const res = await this.getListFn();
this.resultList = res.data.result.songs;
// 数据加载完毕 允许再次响应 滚动触底事件
this.loading = false;
},
// 输入框发生改变时 若搜索框存在内容时发起请求 否则 清空原数据和页面并结束
async inputFn() {
// 初始化状态 - 允许加载更多
this.finished = false;
if (!this.value.trim()) {
this.resultList = [];
return
}
// 异步获取请求数据
const res = await this.getListFn();
// 判断服务器无改该关键词内容内容时 清空并返回
if (res.data.result.songs === undefined) {
this.resultList = [];
return
}
this.resultList = res.data.result.songs;
// 数据加载完毕 允许再次响应 滚动触底事件
this.loading = false;
},
async onLoad() {
this.page++;
const res = await this.getListFn();
// 判断服务器无改该关键词内容内容时 清空并返回
if (res.data.result.songs === undefined) {
// 初始化状态 - 允许加载更多
this.finished = true;
// 数据加载完毕 允许再次响应 滚动触底事件
this.loading = false;
return
}
// ES6 语法 合并对象数组
this.resultList = [...this.resultList, ...res.data.result.songs];
// 数据加载完毕 允许再次响应 滚动触底事件
this.loading = false;
}
}
}
效果展示
1.输入关键词无结果,清空界面内容
2.内容已全部显示,显示加载完成
减少在搜索框每次改变内容时向服务器发送无意义请求的操作
当在搜索框输入内容时,大多数时间都是高频率向服务器发送无意义的请求,需要引入防抖策略
说明
使用 setTimeout 定时器:触发后,计时n秒后执行。如果中途再次触发,重新计时
流程
- 在 data() 中定义 timer,用于 存入/清除 定时器
- 在发起关键词搜索请求事件处理函数的 逻辑代码外 嵌套一层定时器,延时900毫秒
- (由于逻辑代码中存在异步,需要在匿名函数中用 async 修饰)
- 在每次调用定时器包裹的逻辑代码前,执行清除定时器 (模拟再次点击时)
代码模拟
设置定时器,解决频繁请求问题
data() {
return {
...
timer: null, // 定时器 - 防抖
}
},
methods: {
...
// 输入框发生改变时 若搜索框存在内容时发起请求 否则 清空原数据和页面并结束
async inputFn() {
if (this.timer) {
clearTimeout(this.timer);
}
// 在 定时器中的异步请求 同样需要在匿名函数中用 async 修饰
this.timer = setTimeout(async () => {
this.finished = false;
if (!this.value.trim()) {
this.resultList = [];
return
}
// 异步获取请求数据
const res = await this.getListFn();
// 判断服务器无改该关键词内容内容时 清空并返回
if (res.data.result.songs === undefined) {
this.resultList = [];
return
}
this.resultList = res.data.result.songs;
// 数据加载完毕 允许再次响应 滚动触底事件
this.loading = false;
}, 900);
},
async onLoad() {
...
}
}
效果展示
当触发新的关键词搜索时,应重置页码值
经过累加的页码值,在新的请求后也会同时提交,这会导致新的内容不完整或者无内容
说明
滚动触底的操作是配合页码进行的,但是前面的新的请求操作并没有对页码值初始化。导致页码只加不重置,
这会导致新的内容不完整或者无内容
流程
- 在新的关键词搜索提交之前,初始化页码值 (例如:input事件、关键词点击事件)
代码模拟
修改 点击关键词业务代码、输入框发生改变业务代码
data() {
return {
...
page: 1, // 当前搜索结果页数
timer: null, // 定时器 - 防抖
}
},
async fn(val) {
// 重置页码值
this.page = 1;
...
},
// 输入框发生改变时 若搜索框存在内容时发起请求 否则 清空原数据和页面并结束
async inputFn() {
if (this.timer) {
clearTimeout(this.timer);
}
// 在 定时器中的异步请求 同样需要在匿名函数中用 async 修饰
this.timer = setTimeout(async () => {
// 重置页码值
this.page = 1;
...
}, 900);
},
将重复且可封装的界面进行封装,方便多次使用
将 van-list组件二次封装,方便多个地方直接导入,并使用 components 引入使用
代码模拟
找到需要封装的内容,修改后放置在 components 文件夹中
<template>
<div>
<van-cell
:title="name"
:label="author + ' - ' + name"
center>
<template #right-icon>
<van-icon
name="play-circle-o"
size="0.6rem" />
</template>
</van-cell>
</div>
</template>
<script>
export default {
props: {
name: String, // 歌曲
author: String, // 歌手
id: Number, // 歌曲id
}
}
</script>
其他页面直接引用该封装好的 SongItem 组件,使用 v-for 铺设数据
<template>
...
<SongItem
v-for="item in songList"
:key="item.id"
:name="item.name"
:author="item.song.artists[0].name"
:id="item.id"
></SongItem>
...
</template>
<script>
...
import SongItem from '@/components/SongItem.vue';
export default {
components:{
SongItem
},
...
}
</script>
点击播放图标,跳转到播放页面并支持播放音乐
通过预先提供的代码和样式,只需要提供给路由的 歌曲id 的 query参数即可实现
说明
播放器样式已提供,只需要路由跳转并传递参数即可;
原生Dom 提供了 timeupdate 事件,用于监听被绑定的播放器 在当前的播放位置发送改变时 事件,
audio 标签提供了currentTime 属性,获取当前播放音频的进度时间,需要 Math.floor 转换成整数
对 play.vue 的逻辑操作分析
- play组件 获取歌曲后,为 audio 标签的 src 传入路由提供的 id 值,拼接成完整 url,实现播放
- play组件 通过操作 audio 的 play() 与 pause();支持 暂停/开始 播放,并使用CSS中样式呈现 唱片转动与停止
- play组件 获取歌词后,遍历该数组对歌词数据进行处理分割,并使用原生方式获取audio标签的时间进度同步显示
- play组件 处理后的歌词是一串带时间处理成秒数的键 和 处理掉方括号的歌词的值的参数的对象 {时间:”歌词内容”}
- play组件 当播放器播放时,会同步提供给计算属性当前的秒值,以此为依据改变 处理后的歌词对象在界面的显示
- play组件 为避免内容错乱,会存储上一个歌词,若当前时间秒无歌词,会显示上一个歌词,直到新歌词的出现
流程
- 定义 api/play.js 接口文件,并导入到 api/index.js 放置在全局接口 @/api 提供给其他组件
- 给 上面封装的 SongItem.vue组件 的具名插槽 right-icon 设置单击事件
- 当用户单击 right-icon 播放区域图标时,执行路由跳转到 ‘/play’ 路径,并以query参数方式传递 歌曲 id
- play组件接收到 query参数,会拼接音乐地址url,并在dom加载完成时获取歌曲地址与歌词,插入到对应区域
代码模拟
配置 api/play.js 接口文件
import request from '../utils/request'
// 播放页 - 获取歌曲详情
export const getSongById = (id) => request({
url: `/song/detail?ids=${id}`,
method: "GET"
})
// 播放页 - 获取歌词
export const getLyricById = (id) => request({
url: `/lyric?id=${id}`,
method: "GET"
})
导入 api/play.js 到公共接口文件 api/index.js 方便以 @/api 调用
...
import { getSongById, getLyricById } from './Play.js';
...
// 获取歌曲播放地址
export const getSongByIdAPI = getSongById;
// 获取歌曲歌词数据
export const getLyricByIdAPI = getLyricById;
为 components/SongItem.vue 的图标添加 单击事件,并执行路由操作
<template>
<div>
<van-cell
:title="name"
:label="author + ' - ' + name"
center>
<template #right-icon>
<van-icon
name="play-circle-o"
size="0.6rem"
@click="playFn" />
</template>
</van-cell>
</div>
</template>
<script>
export default {
props: {
name: String, // 歌曲
author: String, // 歌手
id: Number, // 歌曲id
},
methods: {
playFn() {
this.$router.push({
path: '/play',
query: {
id: this.id,
}
})
}
}
}
</script>
在 play.vue 文件中其实已经写完了业务,注意引入对应接口即可
<template>
...
</template>
<script>
// 获取歌曲详情和 歌曲的歌词接口
import { getSongByIdAPI, getLyricByIdAPI } from '@/api'
...
</script>
效果展示
Vant组件库采用的是 px 的计算单位,通过 postcss、postcss-pxtorem 实现多端适配
移动端需要使用 em 进行适配操作,通过 postcss、postcss-pxtorem 配合 webpack 自动把 px 转成 rem
导入
yarn add postcss postcss-pxtorem@5.1.1
配置文件
在项目的根目录创建 postcss.config.js,对 postcss 插件进行配置
module.exports = {
plugins: {
'postcss-pxtorem': {
// 能够把所有元素的 px 转换成 rem
// rootValue 转换的基准值 (例如一个元素宽是75px 则转换成 rem 为 2rem)
rootValue: 37.5,
propList: ['*'],
}
}
}
题外话:vw适配
相关文档:https://vant-contrib.gitee.io/vant/v2/#/zh-CN/advanced-usage#viewport-bu-ju
Vant 默认使用 px
作为样式单位,如果需要使用 viewport
单位 (vw, vh, vmin, vmax),
推荐使用 postcss-px-to-viewport 进行转换
// postcss.config.js
module.exports = {
plugins: {
'postcss-px-to-viewport': {
viewportWidth: 375,
},
},
};