Vue笔记 更多内容

网易云音乐项目

成品下载

跨域

在本地访问使用浏览器直接访问其他服务器资源存在跨域问题

更多详细解释参阅 ajax文档

通过反向代理可解决跨域问题

浏览器会阻拦非同源策略的请求,但可以使用非浏览器的方式的去访问,期间无跨域问题。

使用本地服务器去请求目标服务器数据,网页再从本地服务器获取接收到的数据

%title插图%num

思路

  • 本地node服务器开启cors,负责请求的转发和数据接收回传
本地部署接口

使用第三方 网易云音乐 API,负责请求的回传

Node搭建的服务收到请求后,伪造身份,请求网易云API拿到数据

%title插图%num

项目链接:https://github.com/Binaryify/NeteaseCloudMusicApi
文档链接:https://binaryify.github.io/NeteaseCloudMusicApi/#

安装

直接下载

下载项目依赖 (在项目根目录 右键+shift 运行cmd 输入指令 安装所需依赖)

PowerShell
npm install

运行

PowerShell
node ./app.js

上述操作完成后,服务器 默认运行在本地的 3000端口:直接访问

%title插图%num
初始化项目

初始化项目,下载必备包,引入初始文件,配置Vue

搭建基础vue设施,为后面的样式和功能实现的铺设作准备

流程

  • 初始化工程 (vue create music-demo)
  • 下载所需第三方包 axios vant vue-router
  • 下载Vant自动按需引入插件 babel-pugin-import
  • 在 babel.config.js 进行 vant 自动引入配置
  • 引入提前准备好的 reset.css,flexible.js 到 main.js 使用

操作

创建项目 引入包

PowerShell
# 创建 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

JavaScript
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 引用第三方包

JavaScript
// 适配
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” 需要去除,否则报错

%title插图%num

CSS代码

Layout/index.vue
<style scoped>
/* 中间内容区域 - 容器样式(留好上下导航所占位置) */
.main {
  padding-top: 46px;
  padding-bottom: 50px;
}
</style>
Home/index.vue
<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>
Search/index.vue
<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> 显示路由页面

代码演示

配置路由对象 (下载/引入/注册/规则/路由对象/注入/显示)

router/index.js
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;

导入路由配置对象

main.js
// 接收 路由对象
import router from '@/router/index.js';

new Vue({
  router,
  render: h => h(App),
}).$mount('#app')

创建挂载点 (并为对应路由挂载二级路由 挂载点)

App.vue
<template>
  <div>
    <router-view></router-view>
  </div>
</template>
Layout.vue
<template>
  <div>
    我是layout
    <router-view></router-view>
  </div>
</template>

效果展示

%title插图%num
Tabbar组件

点击 底部导航,切换路由页面显示

%title插图%num

让 Tabbar组件 在 Layout.vue 底部显示 Tabbar组件笔记Tabbar相关文档

流程

  • 按需引入 Tabbar、TabbarItem 到 main.js,并在 Layout/index.vue 使用
  • 点击后,实现 Tabbar组件 配合路由切换页面功能

代码演示

全局引入 Tabber组件

main.js
import { Tabbar, TabbarItem } from 'vant';
Vue.use(Tabbar);
Vue.use(TabbarItem);

使用 <van-tabbar> 标签,并加入 router 属性 和 replace + to 集成路由切换功能

Layout/index.vue
<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>

效果展示

%title插图%num
NavBar组件

实现 顶部标题 展示的组件

%title插图%num

NavBar 是头部区域的标题,NavBar组件笔记NavBar文档

流程

  • 按需引入 NavBar 到 main.js,并在 Layout/index.vue 使用
  • 实现头部 NavBar 样式的铺设

代码演示

全局引入 NavBar组件

main.js
import { NavBar } from 'vant';
Vue.use(NavBar);

使用 <van-nav-bar> 铺设

Layout/index.vue
<template>
  <div>
    <van-nav-bar title="首页" />
    ...
  </div>
</template>

效果展示

%title插图%num
NavBar 标题切换

在路由配置中,给路由设置 元信息 meta.title

通过路由切换,侦听路由切换显示对应标题,实现让 NavBar 指示当前组件标题

流程

  • 在 main.js ,为需要配置的路由对象设置 meta 元信息,传入 title 值,供组件使用
  • NavBar组件 使用 :title 方式 与 this.$route.meta.title 绑定,watch 监听该值变化并同步变化

代码演示

修改 路由配置,增加 meta元信息

main.js
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

Layout/index.vue
<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>

效果展示

%title插图%num
网络请求封装

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

通过业务的不同,创建多个文件夹;其中包含 总请求地址、公共请求导出模块、各页面请求模块

流程

  • utils/request.js 对 axios 进行二次封装,进行指定项目的根地址
  • api/Home.js 统一管理所有需要的 url地址,封装网络请求的方法并导出
  • api/index.js 统一导出接口

代码演示

创建 src/utils/request.js 导出二次封装 axios模块

JavaScript
// 导入 axios包
import axios from 'axios';
// 公共请求地址 (需要存在 网易云音乐api 项目)
axios.defaults.baseURL='http://43.140.202.112:3000'
// 向外导出
export default axios;

创建 src/api 文件夹作为接口文件夹,并创建 src/api/home.js

JavaScript
import request from '@/utils/request.js';

// 首页 - 获取推荐歌单
export const recommendMusic = params => request({
   url:'/personalized',
   // ES6语法 同名对象成员 传参可简写
   params,
});

创建总接口导出模块 src/api/index.js 用于直接导出所有 目录下接口

JavaScript
/*
   * 各个请求模块的js 都导入到该模块中 用于向外导出
   * 方便 全局直接引用
*/
import { recommendMusic } from './Home.js';
//请求推荐歌单方法导出
export const recommendMusicAPI = recommendMusic;

在 main.js 引入 @/api (同 ./api/index.js) 进行测试,发起请求

JavaScript
import { recommendMusicAPI } from '@/api';

// 定义异步的函数
async function fn() {
  // 等待异步请求获取到值 赋值给 res
  const res = await recommendMusicAPI();
  // 打印结果
  console.log(res);
}

fn();

效果展示

%title插图%num

从 服务器成功取到数据并打印

推荐歌单铺设

使用网络请求数据,并完成首页推荐歌单界面铺设

%title插图%num

流程

  • 布局 van-row 和 van-col
  • van-image 显示图片,p 标签显示歌名
  • 引入 api 里的网络请求方法,把数据请求回来,循环铺设

代码演示

全局 按需引入 Col、Row组件,和 Image组件 (需换名)

main.js
import { Col, Row , Image as VanImage } from 'vant';

Vue.use(Col);
Vue.use(Row);

铺设 推荐歌单页面 src/Home/index.vue

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>

效果展示

%title插图%num
最新音乐铺设

使用网络请求数据,并完成首页最新歌单界面铺设

%title插图%num

流程

  • 引入注册使用 van-cell,并且设置一套标签和样式准备
  • 使用官方文档说明的插槽方式,为 van-ceil 标签右侧内嵌 Icon组件图标
  • api/Home.js 编写最新音乐的接口方法
  • 引入到 Home/index.vue 中,数据铺设到页面

代码演示

全局 按需引入 Cell组件 和 Icon组件

main.js
import { Cell, Icon } from 'vant';

Vue.use(Cell);
Vue.use(Icon);

在 src/Home/index.vue 按原基础再次铺设样式

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 添加 最新音乐 请求接口方法

api/Home.js
import request from '@/utils/request.js';

...

// 首页 - 推荐最新音乐
export const newMusic = params => request({
   // ES6语法 同名对象成员 传参可简写
   url: '/personalized/newsong',
   params,
})

引入到 总接口文件 api/index.js 方便其他页面使用 @/api 引用

JavaScript
import { recommendMusic, newMusic } from './Home.js';
//请求推荐歌单方法导出
...
export const newMusicAPI = newMusic;

在 Home/index.vue 引入接口方法,并发起数据请求,铺设界面

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

效果展示

%title插图%num
热搜关键词铺设

完成搜索框热搜关键字显示

%title插图%num

在 Seach/index.vue 铺设搜索栏目的路由页面,并将搜索结果显示页面,与关键词区域互斥显示

流程

  • 使用 van-search组件 铺设 Seach/index.vue 界面
  • api/Search.js 创建 热搜关键字接口方法
  • 将 Search.js 导入到公共API总入口文件,方便 @/api 调用
    • 引入接口文件,发起请求,并将返回的结果 铺设到界面
    • 搜索框监听 @input 输入事件,实现基本的输入时动态显示内容

代码模拟

按需引入 vant组件库的 van-search组件

main.js
import { Search } from 'vant';

Vue.use(Search);

Search/index.vue 使用 van-search 标签,实现搜索框样式

Srarch/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">海底</span>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      value: '',
    }
  }
} 
</script>

阅读接口文档规则,创建 api/Search.js 实现热搜关键词接口业务

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

api/index.js
...
import { hotSearch , searchResultList } from './Search.js';
...
// 热搜关键词方法导出
export const hotSearchAPI = hotSearch;
// 热搜关键词搜索结果
export const searchResultListAPI = searchResultList;

使用接口,将返回数据配置并铺设至 Search/index.vue 页面的对应位置

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>

撰写关键词搜索接口的实现,点击 关键词 或 搜索栏填值 发起关键词详细搜索
最终将结果显示在界面,并关键词与搜索结果的区域互斥显示

Vue HTML
<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.实现单击后关键词显示在搜索框

%title插图%num

2.单击关键词时发起请求并铺设内容

%title插图%num

3.监听输入框输入事件,发起请求

%title插图%num
热搜加载更多

当滑动搜索结果的内容触底后,向服务器提交并加载下一页的内容

%title插图%num

在 热搜关键词 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

main.js
// 按需导入
import { List } from 'vant';
...
Vue.use(List);

van-list组件 嵌套在 van-cell组件 外侧,方便实现监听滚动触底

Vue HTML
<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参数,判断需要取的内容,实现加载更多业务

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

效果展示

%title插图%num
优化代码 – BUG修复

当向服务器发送关键词搜索请求返回无内容时,后续操作应终止

%title插图%num

用户输入关键词无内容返回,应清空界面数据,并终止后续业务;
滚动触底发起加载更多的业务请求返回无内容后,应呈现 “没有更多内容” 的效果

流程

  • 数据返回后,判断返回的对象内容是否存在有效数据,否则清空界面内容、终止后续代码业务
  • 所有包含发起加载更多的业务请求返回无结果后,将 finished 加载完成状态设置为 true
  • [初始化] 每次事件函数发起请求前(除滚动触底事件函数) 都将加载完成状态的 finished 设置为 false
    • 设置初始化是加载完成状态 是为了重置下次搜索关键词内容时候,允许加载更多的操作

代码模拟

添加判断条件,服务器返回的指定内容为 undefined

JavaScript
if (res.data.result?.songs === undefined) {
   // 执行的操作
   ...
   // 结束后续业务
   return
}

修改输入框内容发生改变事件函数,与滚动触底时触发的事件函数等

JavaScript
  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.输入关键词无结果,清空界面内容

%title插图%num

2.内容已全部显示,显示加载完成

%title插图%num
优化代码 – 防抖

减少在搜索框每次改变内容时向服务器发送无意义请求的操作

%title插图%num

当在搜索框输入内容时,大多数时间都是高频率向服务器发送无意义的请求,需要引入防抖策略

说明

使用 setTimeout 定时器:触发后,计时n秒后执行。如果中途再次触发,重新计时

流程

  • 在 data() 中定义 timer,用于 存入/清除 定时器
  • 在发起关键词搜索请求事件处理函数的 逻辑代码外 嵌套一层定时器,延时900毫秒
    1. (由于逻辑代码中存在异步,需要在匿名函数中用 async 修饰)
  • 在每次调用定时器包裹的逻辑代码前,执行清除定时器 (模拟再次点击时)

代码模拟

设置定时器,解决频繁请求问题

JavaScript
   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() {
     ...
    }
  }

效果展示

%title插图%num
优化代码 – 页码重置

当触发新的关键词搜索时,应重置页码值

%title插图%num

经过累加的页码值,在新的请求后也会同时提交,这会导致新的内容不完整或者无内容

说明

滚动触底的操作是配合页码进行的,但是前面的新的请求操作并没有对页码值初始化。导致页码只加不重置,
这会导致新的内容不完整或者无内容

流程

  • 在新的关键词搜索提交之前,初始化页码值 (例如:input事件、关键词点击事件)

代码模拟

修改 点击关键词业务代码、输入框发生改变业务代码

JavaScript
 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);
    },
优化代码 – 封装组件

重复且可封装的界面进行封装方便多次使用

%title插图%num

将 van-list组件二次封装,方便多个地方直接导入,并使用 components 引入使用

代码模拟

找到需要封装的内容,修改后放置在 components 文件夹中

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" />
            </template>
        </van-cell>
    </div>
</template>

<script>
export default {
    props: {
        name: String, // 歌曲
        author: String, // 歌手
        id: Number, // 歌曲id
    }
}
</script>

其他页面直接引用该封装好的 SongItem 组件,使用 v-for 铺设数据

Vue
<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>
跳转播放

点击播放图标,跳转到播放页面并支持播放音乐

%title插图%num

通过预先提供的代码和样式,只需要提供给路由的 歌曲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 接口文件

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

JavaScript
...
import { getSongById, getLyricById } from './Play.js';
...
// 获取歌曲播放地址
export const getSongByIdAPI = getSongById;
// 获取歌曲歌词数据
export const getLyricByIdAPI = getLyricById;

为 components/SongItem.vue 的图标添加 单击事件,并执行路由操作

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 文件中其实已经写完了业务,注意引入对应接口即可

Vue
<template>
  ...
</template>

<script>
// 获取歌曲详情和 歌曲的歌词接口
import { getSongByIdAPI, getLyricByIdAPI } from '@/api'
  ...
</script>

效果展示

%title插图%num
vant适配

Vant组件库采用的是 px 的计算单位,通过 postcss、postcss-pxtorem 实现多端适配

移动端需要使用 em 进行适配操作,通过 postcss、postcss-pxtorem 配合 webpack 自动把 px 转成 rem

导入

PowerShell
yarn add postcss postcss-pxtorem@5.1.1

配置文件

在项目的根目录创建 postcss.config.js,对 postcss 插件进行配置

JavaScript
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 进行转换

JavaScript
// postcss.config.js
module.exports = {
  plugins: {
    'postcss-px-to-viewport': {
      viewportWidth: 375,
    },
  },
};