网易云音乐——微信小程序实现

项目作者:盛文增

本项目开源于Github,遵循GPL-3.0 License协议。

    

网易云音乐小程序项目报告

一、API的搭建

1. API的选择

一个项目应当从数据开始。为了获取网易云的数据,首先要获取网易云的API。所幸,万能的GitHub有一个API项目:Binaryify/NeteaseCloudMusicApi: 网易云音乐 Node.js API service (github.com)。首先将此项目部署在自己的服务器上:

以下命令为大致步骤

  • 安装nodejs
  • 安装pm2
  • 克隆项目
  • 使用node启动此项目并使用pm2管理
1
2
3
4
5
yum install nodejs
git clone https://github.com/Binaryify/NeteaseCloudMusicApi.git
yum install pm2
pm2 start app.js
pm2 save

并将域名www.raspberry.cool解析到此服务器(在阿里云控制台解析)。

2. API的HTTP支持与鉴权

经过以上步骤,此时可以访问到此API,但是仍然存在两个重要问题:

  • 不支持HTTPS
  • 所有人都可以访问,不安全

未解决以上问题,决定使用Nginx作反向代理并配置域名证书,实现HTTP访问和鉴权。

具体解决办法:使用Nginx进行判断,如果header或者get参数中存在appkey并且正确,那么就可以通过并访问到API接口,否则直接拦截返回404页面。

使用Lua脚本实现以上过程会简单很多,但是由于自己把Lua等常用模块编译到Nginx中较为麻烦耗时,因此决定采用OpenResty——内置了Lua模块。

OpenResty中的nginx.conf关键配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#user  nobody;
worker_processes 1;

error_log logs/error.log;
error_log logs/error.log notice;
error_log logs/error.log info;

pid logs/nginx.pid;


events {
worker_connections 1024;
}


http {
include mime.types;
default_type application/octet-stream;

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log logs/access.log main;

sendfile on;
tcp_nopush on;

keepalive_timeout 65;

gzip on;

server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
rewrite ^(.*)$ https://$host$1 permanent;
include /etc/nginx/default.d/*.conf;
}

# HTTPS server
rewrite_by_lua_no_postpone on;
server {
listen 443 ssl http2 default_server;
listen [::]:443 ssl http2 default_server;
server_name www.raspberry.cool;
root /usr/share/nginx/html;

ssl_certificate "/usr/local/openresty/nginx/cert/www.raspberry.cool.pem";
ssl_certificate_key "/usr/local/openresty/nginx/cert/www.raspberry.cool.key";
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 10m;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; #使用该协议进行配置。

include /etc/nginx/default.d/*.conf;

location / {
rewrite_by_lua_block{
-- 请求头 a
local headers = ngx.req.get_headers()
-- 准备从请求头中获取 appkey
appkey = nil
-- 遍历请求头
for k,v in pairs(headers) do
if k == "appkey" then
appkey = v
end
end
-- 如果请求头没有 appkey
if appkey ~= "4e51fb5b4a2d4e3cb7cccf87e6c4908a" then
-- 并且 get 参数也没有 appkey
if ngx.var.arg_appkey ~= "4e51fb5b4a2d4e3cb7cccf87e6c4908a" then
ngx.req.set_uri("/404");
end
end
}
proxy_pass http://localhost:8080;
}
}
}

配置中的appkey是写死在代码中的,更好的方案是通过lua从文件中读取。

3. 小程序中的API使用与调试

header中写appkey的原因是为了方便小程序请求,可以在request中统一封装header。在get中写appkey的原因是为了方便浏览器直接打开地址调试。

封装的小程序request实例(其中promisic函数是将小程序的原生APIpromise化):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import {
promisic
} from "../miniprogram_npm/lin-ui/utils/util"

/**
* @Description: HTTP请求工具类
* @author: 盛文增
* @date: 2020/10/8 13:09
*/
class Http {
/**
* 发起HTTP请求
* @param url 请求URL
* @param data 请求数据,默认无数狙
* @param method 请求方法,默认为GET方法
* @returns {Promise<*>}
*/
static async request({
url,
data = {},
method = 'GET'
}) {
return await promisic(wx.request)({
url: `https://www.raspberry.cool${url}`,
data,
method,
header: {
appkey: "4e51fb5b4a2d4e3cb7cccf87e6c4908a"
}
})
}
}

export {
Http
}

二、小程序页面设计

由于是仿照网易云播放器的项目,开始之前我仔细观察了网易云音乐APP、网易云音乐微信小程序、QQ音乐APP、QQ音乐微信小程序以及Apple Music APP,同时结合微信小程序的特征——小而美,使用PS工具、蓝湖等进行页面的全新设计。

三、小程序实现

1. 视图层

由于此项目较为复杂,使用第三方组件能够加快开发效率。考虑市场上诸多第三方小程序组件,如微信官方的WeUI WXSSiViewZanUI WeAppMinUIColorUILin UI,考虑良久后决定选择Lin UI,其基于微信小程序原生语法实现的组件库,没有太多的破坏小程序的原生感。遵循简洁,易用的设计规范。且其开发者团队非常活跃,问题能够得到有效的解决。

上图为Lin-UI交流群。

页面实现:

  • 未登录的首页
  • 未登录的个人中心
  • 登陆界面及登陆提示
  • 登录后的首页
  • 音乐播放界面
  • 歌词展示界面
  • 评论界面
  • 歌单界面

2. 逻辑层

本项目力求做到代码优雅、逻辑明确、结构清晰,因此在架构设计上花费很多时间,同时仔细研读了《高级JavaScript程序设计 第四版》一书。代码注释量足够,可读性较高。

项目目录结构:

目录及对应代码介绍:

image:存放一些必须存放在本地的图片,即tabbar的小图标。其他图片全部从服务端获取。

model:存放实体类,如歌单类、音乐类。并提供对应的方法。例如,对于音乐类,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import {
Http
} from "../utils/http"

/**
* @Description: 根据不同参数请求音乐
* @author: 盛文增
* @date: 2020/10/8 13:08
*/
class Music {
/**
* 检查音乐是否可用(版权)
* @param songId
* @returns {Promise<*>}
*/
static async checkMusic(songId) {
return await Http.request({
url: "/check/music",
data: {
id: songId
}
})
}

/**
* 获取音乐URL
* @param songId
* @returns {Promise<*>}
*/
static async getSongBySongId(songId) {
return await Http.request({
url: '/song/url',
data: {
id: songId
}
})
}

/**
* 获取音乐详情
* @param songId
* @returns {Promise<void>}
*/
static async getSongDetailBySongId(songId) {
return await Http.request({
url: "/song/detail",
data: {
ids: songId
}
})
}

/**
* 获取歌词接口
* @param songId
* @returns {Promise<*>}
*/
static async getSongLyrics(songId) {
return await Http.request({
url: "/lyric",
data: {
id: songId
}
})
}

/**
* 搜索音乐
* @param value
* @returns {Promise<*>}
*/
static async searchMusic(value) {
return await Http.request({
url: '/search',
data: {
keywords: value
}
})
}
}


export {
Music
}

其他代码展示:

  • 加载内容服务使用find,使得代码更加简洁明了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 加载首页内容服务
* @returns {Promise<void>}
* @private
*/
async _indexContentService(coldLoad) {
/**
* 加载推荐歌单、推荐单曲、官方歌单
*/
if (coldLoad || this.data.PLAYLIST_RCMD.length === 0) {
// 获取播放列表
// const playlists = await Playlists.getPlayLists()
const homePageBlocks = await Homepage.getHomePageBlockPage()
this.setData({
//推荐歌单
PLAYLIST_RCMD: homePageBlocks.data.data.blocks.find(items => items.blockCode === 'HOMEPAGE_BLOCK_PLAYLIST_RCMD').creatives,
//推荐歌曲
STYLE_RCMD: homePageBlocks.data.data.blocks.find(items => items.blockCode === 'HOMEPAGE_BLOCK_STYLE_RCMD').creatives,
//推荐歌单
OFFICIAL_PLAYLIST: homePageBlocks.data.data.blocks.find(items => items.blockCode === 'HOMEPAGE_BLOCK_OFFICIAL_PLAYLIST').creatives
})
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
phoneNumberRules: [{
validator(rule, value, callback, source) {
if (value) {
if (/^1[3-9]\d{9}$/.test(value)) {
callback()
} else {
callback(false)
}
} else {
callback(false)
}
},
message: '你真的输入正确的手机号了吗',
// trigger: 'change'
trigger: 'blur',
required: true
}]

四、迭代历史

项目在GitHub上有完整的迭代历史。

五、心得体会

由于时间仓促,仍然有不少不完善的地方,一些界面仍然不够美观、一些功能还来不及实现。

这几个月来,我通读了一遍《JavaScript高级程序设计 第四版》,对前端的语法、设计思想等有了很多的了解,并活学活用,极大地提高了我的编码能力。对整个项目的设计流程有了大致的了解——从后端开发、UI设计到前端实现,

在项目目录设计方面,看了很多知名项目的目录设计;在代码可读性、优雅性方面做了很多努力;写了很多注释,可读性较高;保持良好的编码习惯,不乱用变量,能使用局部不可变变量就不使用全局可变变量等。

评论

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×