• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

vue3 网易云api 实现网页播放器

武飞扬头像
MerrickJ
帮助5

vue3 基于网易云api 实现网页播放器

项目介绍

Electro Player

一款基于网易云api构建的在线音乐播放器,具有音乐播放、排行榜、歌曲搜索、我的歌单、历史歌单、查看评论、通过uid简单登录等功能,适用于网页端和移动端(简单适配)

这也是我在github上找到的一个前端练手项目

基本上是复刻了这个项目,写此文以总结经验。

技术栈

  • vue3
  • vite脚手架
  • vue-router路由
  • pinia状态管理
  • ES6 js
  • less(css预处理器)
  • axios(网络请求)

页面结构

首先弄懂页面的结构,整体页面如下 学新通 再看完源码中的路由结构后整理如下:

路由结构

学新通

一级路由:

学新通 二级路由: 学新通 组件结构 学新通

实现过程

基础配置
  1. 配置路径别名

  2. 配置基础样式

学新通 reset.less: 将默认样式清除 var.less: 定义本项目采用的字体大小、颜色等变量 mixin.less: 定义混合样式,如flex布局居中等 index.less: 主样式文件 采用了less替代css,相对来说,功能更加丰富,方便了许多,具体表现在可以定义变量、样式嵌套、混合等。

如下面的例子:

// 显示省略号
.no-wrap() {
  text-overflow: ellipsis;
  overflow: hidden;
  white-space: nowrap;
}
.flex-center(@direction: row) {
  display: flex;
  flex-direction: @direction;
  justify-content: center;
  align-items: center;
}

定义了两个mixins,不带参数和带参数都可,这样就可以将常见的样式方便应用在其他定义上。

接着, 在vite中配置less的全局变量,即var.less和mixin.less中的变量(这样我们在其他文件中使用时不需要手动导入)

export default defineConfig({
  ...,
  css: {
    preprocessorOptions: {
      less: {
        additionalData: `@import "@/styles/var.less";
                         @import "@/styles/mixin.less";`,
      },
    },
  },
});
api接口实现

首先使用axios封装网易云api接口,设置baseURL和timeout,其中baseURL为api的node服务器地址,本地开发采用http://localhost:3000, 在项目部署到网上后可以替换为部署的网址。

接着,新建一个apis文件夹,用于定义获取各类信息的接口函数,我将本项目的接口函数分为了三类。
第一类为musiclist,包括获取歌单列表、歌曲详情、音乐播放地址、音乐是否可用、歌词、评论、搜索结果等。
第二类为toplist,包括获取排行榜歌单、推荐歌单等。
第三类为userinfo,包括获取用户歌单信息(包括用户头像)等。

例如获取歌曲详情接口函数

// 获取歌曲详情
export const getSongDetail = (ids) => {
  return request.get("/song/detail", {
    params: {
      ids,
    },
  });
};
stores实现

对于歌曲的播放、加入、删除等,毫无疑问我们需要在store中完成,我使用pinia设置了两个store。

第一个为playlist
保存播放列表的歌曲数据,并用于增删减改歌曲

export const usePlayListStore = defineStore("playList", () => {
  const audioEle = ref(null); // 引用audio元素
  const mode = ref(1); // 播放模式,默认顺序播放
  const playList = ref([]); // 正在播放列表
  const orderList = ref([]); // 顺序播放列表

  const isPlaying = ref(false); // 是否正在播放
  const currentIndex = ref(-1); // 当前音乐索引

  const currentMusic = computed(() => {
    return playList.value[currentIndex.value] || {};
  });

第二个为user
用于保存用户的uid、偏好音量、历史播放歌单等,并借助pinia持久化存储插件pinia-plugin-persistedstate,将数据持久保存在loaclStorage中。

// 正在播放列表
export const useUserStore = defineStore(
  "user",
  () => {
    const uid = ref("");
    const historyList = ref([]);
    const volume = ref(ELECTROPLAYER_CONFIG.VOLUME);
    const HISTORYLIST_MAX_LENGTH = 200;
全局指令

除了 Vue 内置的一系列指令 (比如 v-model 或 v-show) 之外,Vue 还允许你注册自定义的指令 (Custom Directives)。由于本项目中歌单的封面图片较多,所以我顶一个了一个全局指令,用于图片的懒加载,这也是一个比较常用的指令。

先在directives文件夹中定义一个插件,内容为懒加载指令,再到main.js中使用该插件,自动注册全局指令。

// 定义图片懒加载插件
import { useIntersectionObserver } from "@vueuse/core";

export const lazyPlugin = {
  install(app) {
    // 定义全局指令
    app.directive("img-lazy", {
      mounted(el, binding) {
        const { stop } = useIntersectionObserver(el, ([{ isIntersecting }]) => {
          if (isIntersecting) {
            // 图片进入视口区域
            el.src = binding.value;
            stop();
          }
        });
      },
    });
  },
};
import { lazyPlugin } from "./directives";
app.use(lazyPlugin);

当然,也可以直接再main.js中直接注册全局指令,例如我注册一个指令,使目标元素自动聚焦,

app.directive("focus", {
  mounted: (el) => {
    el.focus();
  },
});
组合式函数

vue3中可以使用组合式函数替代vue2中的mixins选项,实现代码的复用功能。 参考:组合式函数 | Vue.js (vuejs.org)

比如本项目中,经常需要等待加载和关闭加载(加载动画),那这个操作就可以提出为一个组合式函数。实现代码如下:

// loading 状态
import { ref } from "vue";
export const useLoading = () => {
  let timer = null;
  const isLoading = ref(true);
  const hideLoad = () => {
    timer = setTimeout(() => {
      isLoading.value = false;
    }, 200);
  };
  clearTimeout(timer);
  return { isLoading, hideLoad };
};

在其他地方调用该函数

import { useLoading } from "@/composables/loading"; // 使用组合式函数代替mixins
const { isLoading, hideLoad } = useLoading();
工具函数

我在文件夹utils中,定义了一些工具函数,避免在组件中代码过多,这些函数也分为两类。

第一类是song歌曲的处理函数,我们通过api获取的歌单数据较为复杂,需要处理抽取有用信息,我定义了一个Song类,属性为一首歌的id、名称、歌手等。工具函数需要将api获取的原始歌单result转换为Song数组。formatSongs、createSong等等。

第二类是比较杂的工具函数,
有时间格式化函数 formatSecond,将歌曲的秒数转化为分钟加秒数;

有将http链接转换为https链接的函数 toHttps;

有随机洗牌函数 randomSortArray 将歌曲列表顺序随机打乱,实现随机播放功能;

有歌词解析函数 praseLyric 将获取的歌词数组 转换为 时间 歌词 的对象数组;

有silencePromise,修复点击播放后快速点击暂停导致的错误:
“Uncaught (in promise) DOMException: The play() request was interrupted by a call to pause().”;

页面实现

页面实现的过程较为复杂,因为组件实在太多了,我是从大到小实现的。从主要页面着手,阅读源码,遇到其中一些子组件例如基础图标组件、基础toast组件,我一般会先跳过或者用简单功能代替,例如用alert代替toast组件。但是总结起来,如果按照这个顺序,会显得没有条理,所以我将页面的实现、次要组件的实现、基础组件的实现分开叙述。

header组件

实现效果,包括标题和登录后的用户头像及名称等信息。 学新通

用户的头像设置为一个router-link 渲染到页面上时是a元素,但是可以让其渲染为其他元素如dt,具体做法如下: 参考:从 Vue2 迁移 | Vue Router (vuejs.org)

    <!-- 用户信息--头像和登录 -->
    <dl class="user">
      <template v-if="isLoggedin">
        <RouterLink to="/music/userlist" custom v-slot="{ navigate }">
          <dt @click="navigate" class="user-info" role="link">
            <img :src="avatarUrl" class="avatar" alt="img" />
            <span class="user-name">{{ userInfo.nickname }}</span>
          </dt>
        </RouterLink>
        <dd class="user-btn" @click="opendialog('logout')">退出</dd>
      </template>
      <dd v-else class="user-btn" @click="opendialog('login')">登录</dd>
    </dl>

第二个是创建一个“炫酷”的标题,文字上的背景会不断移动。

学新通 首先将标题h文字颜色设置为透明,通过background设置背景图片,并将background-clip设置为text,背景只出现在文字后。
接着,设置一个背景移动动画,通过background-position改变背景的位置。

music组件(一级路由)

为一级路由的页面,也是播放器的主体部分。 包含了导航栏、播放列表、歌词组件,以及下方的播放栏,功能较多。 我的实现顺序是:
实现导航栏music-btn...
实现播放列表playlist...
实现播放栏和播放功能...
实现歌词组件...

该组件关键实现了歌曲的播放功能和播放栏的功能,具体实现在后面介绍。

views组件的实现(二级路由)
  1. playList

作为二级路由的页面,并且为首页的重定向页面,需要在初始化时读取store中的playList数据。 借助通用的music-list组件,listType指定为duration,显示歌曲的时长。

学新通

  1. search

热搜单词 搜索框 搜索结果列表

学新通

根据官方提供的api接口,根据输入的单词,获取搜索结果,再经过处理函数得到歌曲列表。

此处的api描述如下:

学新通

limit 指定返回的数量,默认为30;offset 为偏移数量,设置为 page * limit,page为页数。

当music-list 触发 滚动加载信号 pullUpLoad(下文musiclist会讲)时,page数目加1,获取下一页(30首)歌曲。

  1. historyList

类似于playList,不过用于保存用户听过的歌曲,这里不再展开。注意歌曲是可以播放的(vip会员专属无法播放),刷新网页后依然存在。

  1. comment

歌曲的评论页面,评论分为两类,一类是热门评论,另一类是最新评论。

热门评论

学新通

最新评论

学新通

评论标题采用sticky粘性导航

评论项的实现: 显示用户的头像、名称,发送时间和ip属地,评论内容,获得赞数,他人回复的评论(最多一条), 界面参考了qq音乐网页版底下的评论界面。

  1. topList

这里的界面与歌曲显示列表的界面大有不同,因为内容是歌单的排行榜,以及一些热门歌单推荐。

学新通

用户鼠标移入每个歌单图片后,点击后跳转到歌单详情detail页面。

hover后突变稍微放大,mask,出现一个播放图标(采用背景图片导入),

固定大小的图片在高分辨率的屏幕上会显示模糊,所以我准备了两个图片,一个x70,一个140,使用以下css让浏览器自己选择合适大小的图片。

background-image: -webkit-image-set(
url("assets/img/icon_play.png") 1x,
url("assets/img/icon_play100.png") 2x
);

显示效果如下 学新通

每个歌单都有其唯一的id,根据id调用接口获取对应的歌曲列表。

  1. detail

显然,这个页面用于显示选中歌单中的歌曲,我们需要根据歌单的id获取数据,那么该从何获得id呢?
可以从路由的params参数中获取,看下列路由:

学新通 后面的数字串即为歌单的id。

调用vue中的useRoute,获取当前路由参数中的id即可。

之后调用获取歌单详情接口获取歌曲列表显示。

  1. userList

这个页面的内容是显示用户创建的歌单卡片,包括自建和收藏的歌单。原本打算和topList一样呈现,但是后面看到一个3d的轮播图,觉得挺好的,就用上去了。经常看到各种网站上好看的效果,收藏起来用起来。

在vue2中,可以使用直接引入 vue carousel 3d 这个包 Vue Carousel 3D - 3D Carousel for Vue.js (wlada.github.io)

里面有现成的组件:

学新通

但是这个包没有更新到vue3,我找到了一个别人更新的,地址在:vue3-carousel-3d - npm (npmjs.com)

组件的使用方法: vue.js - 3D轮播插件vue-carousel-3d非官方最全文档 - 个人文章 - SegmentFault 思否

实现效果如下:

学新通

次要组件实现
  1. musicbtn 导航栏

    实现较为简单,就是一排按钮,导航到不同的二级路由,主要后面需要考虑页面响应式,借助@media媒体查询,调整按钮的宽度,手机上则显示两排按钮。

学新通

  1. musiclist 通用歌曲列表

    ❗重点来了:要实现一个歌曲播放列表组件,需要将该组件重复利用,因为路由中的正在播放页面、搜索页面、歌单详情页面等都需要用到该组件。因此,先要实现一个music-list组件,作为一个通用的组件,根据获取的歌曲列表显示在屏幕上

    这个组件可以接受一个处理好的歌单playList,并使用v-for将歌曲显示为一个列表,

    同时希望列表的种类有多种可选,有的显示歌曲时长,有点显示歌曲所在专辑,还有的列表下拉可以继续加载内容,如搜索歌曲列表。

    因此组件通过props接受两个参数list和listType,

学新通

最后,注意到部分列表的底部(例如正在播放列表)有一个清空列表的按钮,我选择用slot来实现,如果需要这个按钮,则将button元素传入到默认slot中。

学新通 注意好props和emit,那组件的功能就差不多清楚了,主要是css方面比较折磨😂

  1. volume

    音量调节条,建立在基础组件progress进度条之上,只不过前面再添加一个音量图标,有点击静音的功能。

学新通

  1. lyric

    刚开始看到歌词滚动组件的时候,觉得挺复杂的,但是后面着手做的时候,看懂之后就觉得还行,不是很复杂。道理和滑动列表差不多。 简单来讲,外面有一个固定高度的盒子,内部为一个歌词构成的列表,高度超出了外盒子

学新通

刚开始时或者用户缩放界面时,需要先将内部盒子下移到外部盒子的中央,使第一句歌词居中。此后,每播放一句歌词,即lyricIndex加一,内部盒子上移一句歌词的高度,使得新的歌词居中。

那么如何上移和下移呢?使用translateY就行了。

外部盒子在上下一部分区域添加蒙版层图像,使用mask-image属性,添加linear-gradient渐变。

基础组件实现
  1. icon
    以前做项目的时候都是直接使用阿里矢量库iconfont的图标,这样做有一个不好的地方就是要为图标设置样式时比较麻烦,例如设置大小的时候需要添加一个类,然后再写css。假如我实现了一个icon组件,通过props传入图标名称和大小不就方便多了吗。

    引用vue文档中的一句话:在绝大多数情况下,Vue 推荐使用模板语法来创建应用。然而在某些使用场景下,我们真的需要用到 JavaScript 完全的编程能力。这时渲染函数就派上用场了。 在组合式api中的使用方法如下:

    import { ref, h } from 'vue'
    export default {
    props: {
    /* ... */
    },
    setup(props) {
     const count = ref(1)
     // 返回渲染函数
     return () => h('div', props.msg   count.value)
     }
    }
    

    所以,我这里采用了vue3的渲染函数,简写为h()的函数。
    组件接受两个props:type图标类型和size图标大小
    使用h函数渲染一个i标签,class包含iconfont和icon-type,同时设置style中的font-size为size。

    icon组件在其他组件中的使用:

    <ElectroIcon type="github" :size="14" />
    
  2. loading
    加载组件,在获取数据时显示,优化用户的体验感,特别是在网络较慢时。不同于本地,服务器部署在云端后慢的一批。
    首先,去找一个转圈圈的样式,可以直接在codepen中找,如下所示:
    得到的是一个svg图片,添加一个wrapper标签,用于控制图片出现位置

    学新通

    实现效果如下

    学新通

  3. progress
    进度条组件,模仿qq音乐网页版的播放进度条,先看它是如何实现的。
    学新通

    可以看到,播放进度条有一个移动点,最浅色的不同条,偏浅色的缓冲条,亮色的播放条,结构非常清晰。使用检查查看其结构如下:

    学新通

    因此具体实现就是这样,不过小圆点可以直接用css实现,不需要用图标。

    脚本则比较复杂,涉及到了进度条的跳转和拖拽,还要解决进度条与歌曲播放进度对应的问题。这就涉及到歌曲的播放,audio元素有一个event为ontimeupdate,即歌曲的时间发生变化。这个事件非常关键。随歌曲播放时间变化,当前的播放进度条需要改变,歌词也要进行移动。具体在下文的播放功能中介绍。

    这里说明一下进度条的跳转与拖拽问题,下面分别进行实现。

a. 进度条的跳转

即当用户点击进度条的某个位置时,进度条就移动到这个位置。
给进度条最外层的元素添加一个click的回调函数barClick

 <div ref="electroProgress" class="electroProgress" @click="barClick">
      ···
 </div>

实现barClick函数

// 鼠标点击事件
const barClick = (e) => {
  const rect = electroProgressPlay.value.getBoundingClientRect();
  const offsetWidth = Math.min(
    electroProgress.value.clientWidth - dotWidth,
    Math.max(0, e.clientX - rect.left)
  );
  moveSlide(offsetWidth);
  commitPercent(true);
};

b. 实现进度条的拖拽

用户首先按住小圆点(mousedown),在屏幕上拖动(mousemove,手机上为touchmove),最后在某个位置松开(mouseup)。由于用户不只是在进度条上拖动,而是在整个屏幕上拖动,所以不能简单的在进度条上添加事件回调函数,而要在整个文档上使用document.addEventListener添加回调函数。

在小圆点上添加mousedown,实现回调函数bardown

<template>
  <div ref="electroProgress" class="electroProgress" @click="barClick">
    <div class="electroProgress-inner">
      <div ref="electroProgressLoad" class="electroProgress-load"></div>
      <div ref="electroProgressPlay" class="electroProgress-play">
        <div class="electroProgress-dot" @mousedown="barDown"></div>
      </div>
    </div>
  </div>
</template>

bardown记录此时的坐标

// 鼠标按下圆圈,记录下此时的坐标
const barDown = (e) => {
  move.value.isDragging = true;
  move.value.startX = e.clientX || e.touches[0].pageX;
  move.value.left = electroProgressPlay.value.clientWidth;
};

组件 onMounted时,绑定mousemove和mouseup等事件,实现barMove和barUp函数。
onUnmounted时解绑

// 添加绑定事件
const bindEvents = () => {
  document.addEventListener("mousemove", barMove);
  document.addEventListener("mouseup", barUp);

  document.addEventListener("touchmove", barMove);
  document.addEventListener("touchend", barUp);
};

barMove鼠标拖动时,改变进度条(注意拖动过程中歌曲还在正常播放)

// 鼠标处于拖动中,
const barMove = (e) => {
  if (!move.value.isDragging) {
    return;
  }
  e.preventDefault();
  let endX = e.clientX || e.touches[0].pageX;
  let dist = endX - move.value.startX;
  let offsetWidth = Math.min(
    electroProgress.value.clientWidth - dotWidth,
    Math.max(0, move.value.left   dist)
  );
  moveSlide(offsetWidth);
  commitPercent();
};

barUp鼠标拖动完成,改变歌曲的播放进度

// 鼠标拖动完成,
const barUp = () => {
  if (move.value.isDragging) {
    commitPercent(true);
    move.value.isDragging = false;
  }
};

有造轮子那味了😶‍🌫️

  1. toast

造造造! 之前用过elmentPlus组件库中的Elmessage组件,感觉看上去很舒服,比弹出的alert舒服一万倍。
那么我们如何造出这样的轮子呢? 我们需要实现类似Elmessage的效果,像这样调用ElMessage('this is a message.'),屏幕中上会生成一个简洁的提示: 学新通

这里同样需要用到vue3中的渲染函数h(),渲染函数既可以使用div等标签,也可以直接使用导入的vue组件。同时也要用到render函数。h()用于生成虚拟dom,最终展示到页面需要调用render,之前在创建自定义icon组件的时候不用render,是因为可以在setup()函数中返回h()函数。而这里,我们需要在js中手动渲染,所以需要用到render函数。

首先,新建一个toast组件,接受两个props:message文本内容和position出现位置。 之后再新建一个index.js用于渲染该组件, 在index.js中,
定义一个ToastCreator类,
定义一个构建函数,初始化toast的message和position;

export class ToastCreator {
  options;
  container;
  constructor(options) {
    this.options = options;
    this.container = document.createElement("div");
  }

定义一个显示toast方法,如下:
利用h()函数创建虚拟dom节点VNode,同时传入props,
使用render函数(大概是vue2中的函数,不太懂😶‍🌫️),传入VNode和外层容器container
将新增toast插入为body第一个子元素
自设一个duration,到时间后调用消除

  present() {
    const electroToast = h(Toast, this.options);
    render(electroToast, this.container);
    document.body.insertBefore(this.container, document.body.firstChild);
    // 到时间消失
    if (defaultOptions.duration) {
      setTimeout(() => {
        this.dismiss(); // 注意回调函数this的问题
      }, defaultOptions.duration);
    }
  }

定义一个消除toast方法,如下:

  dismiss() {
    document.body.removeChild(this.container);
  }

最后,定义一个创建toast的函数showToast,并导出:

export const showToast = ({ message, position }) => {
  const toast = new ToastCreator({
    message,
    position,
  });
  toast.present();
};

在其他地方使用showToast:

import { showToast } from "base/electroToast/index";
....
// 删除事件
const deleteItem = (index) => {
  deleteHistoryMusic(index);
  showToast({ message: "删除成功!" });
};

这样一个自定义toast组件就做好了,效果如下:

学新通

  1. dialog

学新通

弹窗也是十分常用的基本组件,主要包括基本的信息、确认和关闭按钮。
增加两个slot插槽,一个默认插槽,可以加入输入框等元素;另一个为具名插槽,用于加入额外的按钮。

父组件通过props传入需要显示的文本,通过slot传入需要显示的元素。

另外,可以在dialog组件中定义show和hide方法,并将两个方法通过defineExpose暴露给父组件,父组件可以轻松控制dialog的显示与隐藏。

  1. noresult

内容为空时显示,

学新通

播放功能和工具栏

实现music.vue 中的播放功能和工具栏中的功能

  1. 初始化audio元素

音乐播放的核心元素audio,audio.play() 表示播放, audio.pause()表示暂停

播放时我们需要对其进行一定的监听,定义一个initAudio函数。

比如onplay,当音乐开始播放时,将播放工具栏激活

onprogress,音乐缓冲,使用audioEle.buffered.end(0) 获取已缓冲时长,其与歌曲总时长duration的比值,即为当前的播放进度,可用于控制progress组件中的缓冲进度条显示。

ontimeupdate,随音乐播放,歌词和进度条也自动增长

oncanplay,将能播放的音乐加入到历史列表中

onend,音乐播放结束时,根据播放模式,选择重播还是播放下一首
...

在music.vue中 onMounted 中调用initAudio以初始化audio元素。

  1. 工具栏功能实现

学新通

  • 上一首prev 下一首next 播放play/暂停pause

  • 切换播放模式modeChange,共有四种模式

// 获取播放模式icon
const getModeType = computed(() => {
  return {
    [PLAY_MODE.LIST_LOOP]: "listloop", // 列表循环
    [PLAY_MODE.ORDER]: "order", // 顺序播放
    [PLAY_MODE.RANDOM]: "random", // 随机播放
    [PLAY_MODE.ONE_LOOP]: "oneloop", // 单曲循环
  }[mode.value];
});

这里说明一下随机播放, 这里的处理方法是利用洗牌函数将原列表随机打乱顺序,并替换掉原列表。

随机函数如下:循环n轮,每次从剩下的全部数中选一个数放在最后的位置,直到放完全部的数。

// 随机洗牌函数
export const randomSortArray = (arr) => {
  const result = arr.slice();
  let n = result.length;
  let random;
  while (0 != n) {
    // random = (Math.random() * n--) >>> 0;
    random = Math.floor(Math.random() * n--);
    [result[n], result[random]] = [result[random], result[n]];
  }
  return result;
};
  • 点击评论图标,打开当前音乐的comment页面

  • 切换纯净模式openPure,将歌词组件的width调整为100%,纯享听歌

学新通

  • 调节音量大小volumeChange,点击图标静音等。

最后为了方便用户操作,可以绑定键盘按键,为document添加一个onkeydown监听,通过switch(e.key)来执行相应函数。e.key与键盘的对应关系可以直接查表确定。
这里采用ctrl key 的组合键,即用户在按下ctrl键之后,再按某个键才有用,可以通过e.ctrlKey判断。

ctrl < / > : 上一首/下一首
ctrl ↑ / ↓ : 加大音量/减小音量
ctrl space : 播放 or 暂停

项目部署

将网易云node api服务器 和 播放器应用都部署在vercel上面。

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhfffjbh
系列文章
更多 icon
同类精品
更多 icon
继续加载