全新版本仿网易云音乐来啦

17,033 阅读7分钟

前言

在前端技术领域中,我们可以切身感受得到技术的更新、变革的速度是非常快的,所以工程师们都会需要时常关注和学习一些新技术、新标准。

因为在工作中负责项目的技术栈相比于业界来说,算比较落后了,所以自己动手来开发一个音乐类 web app,可以尝试一些新技术栈,或者往一些特定方向深挖学习。

项目开发时间从年末至今,利用工作之余的时间断断续续地开发,主体功能已经大致完成了,接下来也会陆续添加一些新功能上去,也会持续优化代码,在此也做一下记录和总结。

项目信息

在线体验使用 chrome 移动端调试体验

项目地址

技术栈

  • vue:vue 2.6, vue-router, vuex, vue-server-renderer
  • webpack:webpack 4, webpack-dev-middleware, webpack-hot-middleware
  • node:express 4
  • test:karma 4, mocha, sinon-chai, vue/test-utils, eslint

整体架构

后端 api 是使用 NeteaseCloudMusicApi,提供了非常多接口,并且支持 CORS 跨域。

项目分为两个部分,分别是前端,比如 javascript、css、img、components 等;还有服务端,负责请求响应和服务端渲染,所以项目整体架构如图:

架构

技术实现

项目刚开始使用 vue-cli 初始化,开箱即用的使用体验为我省去了不少繁琐的流程,可以直接上手进行开发。

登录态

用户登录是首先需要解决的问题,因为许多接口都依赖用户登录态。最终是将 api 服务和项目分成两个子域名:

163api.domain.cn    // api
163music.domain.cn  // 项目

但是后来发现,请求登录接口成功后,用户 cookie 无法写入到浏览器内,发现原来是 cookie 内的 domain 设置的是 api 子域名,所以导致 163music.domain.cn 下是无法读取到 cookie 的,但是经过调试发现,接口在 set cookie 的时候是并没有设置 domain,解决方案是在 nginx 内加上 proxy_cookie_path 的配置,为 cookie 添加 domain 为 .domain.cn,那么在其他子域名下就能正常读取到 cookie(刚开始设置的是替换 domain,然而不会生效):

// nginx.conf
server {
        listen 80;
        server_name 163api.domain.cn;
        location / {
            proxy_pass http://127.0.0.1:3000/;
            proxy_cookie_path ~^(.+)$ "$1; domain=domain.cn";
        }
}

webpack

在项目开始初期,一切都是那么的和谐,可以欢腾畅快的开发。开发到中期功能都完成的差不多时候决定添加 ssr 了。vue-cli 3 是可以通过配置文件 vue.config.js 来实现自定义的 webpack 配置,在加入了 ssr 相关配置之后,就可以成功构建打包了,但我希望代码能够实时重载和模块热替换,不然开发效率会比较低下。然后,在尝试了一些改造方案(一番挣扎)之后,还是觉得不能够很灵活地实现,我决定重新搭建环境 Orz

主要的 webpack 配置是参考 vue-cli,node 代码主要参考官方 demo,当代码编写好后就尝试运行了,结果当然是...满屏红色报错。

因为官方 demo 使用的 webpack 3,所以有些配置需要更新,还有一些依赖随版本升级也需要更新调用方法等等。但值得高兴的是,错误提示都基本是准确的,比如:

// 需要提供 mode 选项
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
// 配置迁移,需要使用新配置选项
Error: webpack.optimize.UglifyJsPlugin has been removed, please use config.optimization.minimize instead.

还有可能会缺少各种 loader,需要安装各种依赖。确保构建流程正常后,就可以打开浏览器内看效果了,但又会发现这样的报错:

window is not defined.

原因是因为一个轮播插件内包含 window,而在 node 环境内是没有这个全局变量的,所以导致了这个报错。亦或者访问其他浏览器内置对象时,也会出现这样类似的报错,所以需要确保代码和插件都可以在 node 环境下正常运行。因为轮播插件本身是支持 ssr 模式的,所以修改完代码后即可正常运行。

最后,项目中共有四份 webpack 配置和两个构建脚本。在开发环境下,搭配 webpack-dev-middleware 和 webpack-hot-middleware 来实现了代码的热加载。在生产环境构建时,因为希望能清晰看到错误和警告,也想对构建耗时进行统计,所以将构建脚本拎出来。

build
├── build-production.js        // 生产环境构建脚本
├── setup-dev-server.js        // 开发环境构建脚本
├── webpack.base.config.js      // 基本配置
├── webpack.client.config.js    // 客户端配置
├── webpack.server.config.js    // 服务端配置
└── webpack.test.config.js      // 测试配置

播放器

音乐播放是最主要的核心功能,底层就是使用 audio 标签,并且监听了标签元素的 canplay、timeupdate、ended 事件来分别实现时长计算、更新当前播放进度、下一首播放。 因为播放器是可以支持“后台”播放,所以将播放器放到根组件中并且设置隐藏,所以 dom 结构如下:

<div id="app">
    <!-- audio -->
    <player></player>
    <transition :name="transitionName">
        <router-view class="component"></router-view>
    </transition>
    <footBox></footBox>
</div>

组件数据同步是使用 vuex,比如播放的状态、歌曲总时长、当前的播放进度等,当歌曲播放完毕时候需要播放下一首,这里使用的是 eventBus 来做事件触发,它会比较适合这种类似的场景。

当用户打开播放页面时,我希望音乐是能够自动播放的,无论用户是从其他入口进来亦或者是直接刷新的时候。自动播放是通过 play() 方法去触发的,前者没有问题,但是后者在调用时就会提示错误,错误意思是需要用户进行手势操作之后才能够播放,然后尝试了模拟点击、静音播放的方案之后发现在 chrome 内依然无效,后来感觉 chrome 这样做是正确的,应该把网站的控制权交给用户,让用户清楚页面到底发生了什么,而不是让用户在一堆标签页里寻找是哪个页面发出了奇怪的声音。

更新从播放列表进入播放页后才会自动播放,感谢小伙伴提供解决方案

单元测试

单元测试也是早期规划的功能之一,开始是参考一些开源项目来搭建,最终选型是 karma + mocha + sinon-chai (官方 demo)。搭建的过程就是摸着石头过河了,其中也经历了一些报错,比如:安装依赖失败、配置文件出错、缺少依赖插件等等,然后接近搭建完成后才发现还有官方文档。不得不说是, cli 的确帮开发者节省了非常多配置、搭建的工作,搭建完成之后就可以根据官方文档来编写用例了,根据官方文档内例子已经可以覆盖到绝大部分场景,比如模拟浏览器渲染、用户点击等等。但同时也发现一个问题,如果项目代码经常发生变更的话,那么之前的测试用例也可能需要重新编写了,想知道大家在项目中是怎么处理或者怎么看待呢?

以上是在开发过程中遇到一小部分问题,还有过程当中大部分问题描述和解决方案就不在这里一一展开去讲了,大家如果有问题的地方,欢迎大家私信或者邮件与我交流。

总结

  • 在项目的开发过程中也参考和使用了很多优秀的开源项目,帮助我快速消化一些功能实现,还有提供了后端 api,不然也没有开发这个项目的灵感;
  • Vue 生态下有丰富、详细的官方文档和活跃的社区,基本上遇到的问题都能够解决,超赞;
  • 项目在立项之初可能只是大脑一闪而过的简单想法,再回顾这几个月开发经历,其实过得是比较充实和富有激情的,就是有点费头发 😂;
  • 最后,自知项目中还有很多不足的地方,如果您发现有什么问题或者有更好的想法,欢迎 issue 或者 pr。如果您觉得项目有参考和学习的价值,可以在 github 上点个 star,谢谢~

参考资料

NeteaseCloudMusicApi

vue-awesome-swiper

use nginx to add Domain to a Set-Cookie

Cookies on localhost with explicit domain

mini-css-extract-plugin with SSR

Autoplay Policy Changes