
女生节告一段落了,广工大许愿墙 5.0 也差不多要下线了,终于有时间来谈谈许愿墙了,这次我主要负责电台和后台开发。
# 技术栈选型 ↵
- Vue.js With Webpack
- MongoDB With Mongoose
- Redis Cache
- Nginx Gateway
- Qiniu Cloud Service
# Git 版本管理 ↵
许愿墙分前端部分和后台部分两份代码,但是我没有将他们做成两个仓库,而是使用一个仓库进行管理,目录结构如下:
00- wall2017 01 - .git // Git Files 02 - client // 前端代码 03 - package.json 04 - .... 05 - server // 后台代码 06 - package.json 07 - ..... 08 - doc // 接口文档 09 - 接口文档 10 - 设计稿
也就是说,一个 Git 仓库放两个 package.json
# 接口文档 ↵
前后端分离SPA,需要编写接口文档供开发查阅,因此利用 Gitbook 起了一个服务来做接口文档网站。
具体可查看 wall2017/doc/接口文档/book.json
# 微信的问题 ↵
(微信开发) 一个重要的问题就是线下和线上的不同,微信开发里面几乎很少照顾到线下(非公网IP)的集成开发,年初的时候我用 node 开发过一个简单的营销页的后台,因为项目不算复杂,两张表的数据库还有一些微信登录相关的操作,因此我所有的后台代码,都是在线上的 Linux 服务器上用 SSH + Vim 的方式书写的,还好前端代码是在本地完成,并在 localhost 上开一个端口进行开发,前端会调用远在服务器的跨域 API 执行业务代码。
实话说,这很不好,真的很麻烦,写起来也不方便做这样那样的调试,而且 SSH 时不时会掉线,又要重新登陆。
以上摘抄于我先前写的 《微信开发的最佳配置》,修改了局域网的 HOSTS 和本机的反向代理解决了这个问题。
# 部署的问题 ↵
许愿墙前端是用 vue-cli 起的,他已经写好了编译脚本用于产生生产环境的代码:
00$ npm run build
npm 处理上述命令的时候会在 package.json 的 script 字段寻找
build
的定义,在 vue-cli 中,build其实是 /build 下的一个 js 文件,也就是说执行上述代码其实就是执行这个文件 (webpack编译)编译之后可以得到 index.html 以及对应的 js 文件,下一步就是将这些文件弄到线上去,这个过程叫做部署。
许愿墙有两个资源域名,一个是对应 api 服务器的那个服务器 gw.chenpt.cc 还有一个是指向七牛存储空间的 io.chenpt.cc , 因此部署代码的过程应该是:
- 将 index.html 上传到 gw.chenpt.cc 上
- 将其余的资源文件存储到七牛上 (而且还要在 index.html 的资源前加上 io.chenpt.cc 的域名,这个可以通过修改 wall2017/config/index.js 里的 build 字段进行修改)
而这整个过程,应该是一键化部署的,也就是说我希望这样就可以一键部署:
00$ npm run build 01$ npm run deploy
进一步考虑,需要传文件到一个普通服务器和七牛服务器,拟定了方案是:
- index.html 需要传输到我的服务器上,采用 sftp 上传
- 其他文件需要传输到七牛,应该查阅七牛提供的文档进行上传
基于以上我完成了 auto-deploy.js ,整个部署上传过程很爽,完全一键化:
00$ npm run build && npm run deploy
最近时间比较多,整理了 sftp 上传的代码,并完成了一个简单的工具用于上传文件夹到服务器上 (欢迎 star):
# Modal Controller ↵
在 Github 上我找了很久都没有找到可以将我们自己的组件作为模态框弹出来的控制器。
Ionic2 上有这种东西 ———— ModalController 或者 AlertContoller 这样的。
这种模态框的使用方法可以像下面这样:
00import ModalController from 'xxx'; 01 02let hello = ModalController.create({ 03 component: { 04 template: `<div @click="$emit('hello', 'eczn')"> Hello, World </div>`, 05 von: { 06 hello(msg){ 07 // 打印输出 'eczn' 当上面的组件被点击的时候 08 console.log(msg); 09 } 10 }, 11 vbind: { 12 style: { 13 // 换成白色的背景颜色 14 backgroundColor: '#FFF' 15 } 16 } 17 } 18}); 19 20// 弹出 `<div> Hello, World </div>` 出来 21hello.launch();
因为找不到这种可以自定义内容的控制器,所以就自己写了一个
GwPopup
作为许愿墙的模态框、alert、吐司的控制器。
点击'返回'会关闭最顶层的Modal
此外因为设计师的需求,当点击返回键的时候应该关闭顶层的模态框,所以还需要监听
vue-router
提供的路由切换触发的回调函数。最近空闲多,就把 GwPopup 重新按照当时的思路重写了一次,并改名 vue-ppp,欢迎 Star (厚颜无耻):
# http.client.js ↵
该文件在 axios 的基础上,添加如下功能:
- 打印 http 日志 logger (参数、url、返回值等等)
- 触发
加载中
动画 - 修改了一下 API 的表现
- 封装 jsonp (电台要用)
然后在许愿墙环境下调用一次 Get / Post 请求可以是这样:
00import http from '@/utils/http.client'; 01 02http.get('/api/test', { 03 msg: 'hello' 04}).then(res => { 05 console.log(res); 06}); 07 08http.post('/api/test', { 09 msg: 'world' 10}).then(res => { 11 console.log(res); 12});
# Music.vue & /api/music/* ↵

许愿墙电台
电台是今年许愿墙的特色,它的功能是:
- 用户可以点歌,所点的歌进入队列
- 一个时间、电台只能播放一首歌曲,这个歌曲是队列的队头,到歌曲结束的时候弹出这个歌曲,播放下一首
- 所有的其他用户进来的时候都会听到当前播放的歌曲
- 用户可以发送弹幕评论
- 今天所有用户所点的歌曲最多只会播放到子时 0 点,此时再点歌就会报
今天点歌人数已满、明日再来
# 怎么搞定乐库
要点歌最麻烦的还是乐库了,没有乐库点个毛线歌,可是乐库这种东西谁会公开呀,经过多方寻找,终于找到了酷狗有两个接口是公众可以访问的,一个用于搜索歌曲,另外一个用于获取歌曲详细信息。
我对它具体的实现请见 《jsonp、跨域和同源策略》
来说说要点:
- 接口是跨域的,因此要使用 jsonp
- 许愿墙用的是 https, 这意味着不能加载 http 域上的 js 文件, 需要中继代理
# 怎么设计数据结构
使用队列,毫无疑问。
问题在于队列元素的结构要如何设计, 经过考虑我设计成如下的样子:
00{ 01 // 歌曲封面 (专辑封面优先于歌手封面) 02 cover: { 03 type: String 04 }, 05 // 音乐 url 06 mp3: { 07 type: String 08 }, 09 // 音乐的一些 meta data (来自酷狗) 10 kugou: { 11 type: Object 12 }, 13 // 什么时候开始播放 14 start_at: { 15 type: Number 16 }, 17 // 时长 18 duration: { 19 type: Number 20 }, 21 // 点歌者想要说的话 22 content: { 23 type: String 24 }, 25 // 点歌者个人信息 26 who: { 27 type: Object 28 } 29}
其中,最为重要的是 start_at 和 duration。
start_at: 从今天的 0 点到现在经过的秒数
duration: 一首歌的持续时间
为了计算从今天的 0 点到现在经过的秒数、应该引入一个函数来处理:
00function todayZero(ts){ 01 return parseInt(ts % 86400000 / 1000); 02}
此外,控制歌曲的播放还得引入几个全局性的变量:
00/** 01 * 计数、每点一首自加一次 02 */ 03var count = 0; 04 05/** 06 * 歌曲队列 07 */ 08var musicQueue = []; 09 10/** 11 * 零时刻 12 * 当新的一首歌入队的时候执行: 13 * zero = zero + duration; 14 * 15 * 利用这个来计算 start_at 16 */ 17var zero = todayZero(Date.now()); 18 19/** 20 * 今天所点的全部歌加起来的最大时长 21 */ 22var MAX_TIME = 86400;
# 怎么记录、判断歌曲正在播放、超时、切换歌曲
切换歌曲其实就是当一首歌播放完的时候,切换到下一首的过程。
实现这个过程是不可能写一个 setInterval 去轮询的,这很耗性能而且不实时,显得很蠢。
我的做法是,在用户尝试获取当前播放的音乐的时候:
- 检查当前有没有歌曲正在播放,如果没有则返回 null 否则继续下一步
- 检查这个歌曲有没有播放完毕,如果播完了,则出队一首新歌作为当前播放的歌曲,如果没有则还是返回这个么有播放完毕的歌曲
- 特别的、如果当前的歌曲播放完毕了,而队列里没有下一首歌曲的时候,此时返回 null
再来谈谈怎么判断一首歌是否已经播完了:
在一天内,歌曲的播放从今日的 0 秒开始,到今日的 86399 秒,一共 86400 秒 (由 MAX_TIME 决定)
所有的歌曲播放都会落在 [0, 86400) 这个区间内,比如如果我在今天的 0 秒处点了第一首歌曲,该歌曲时长是 300 秒,那么这个歌曲的播放区间应该是: [0, 300)
若在一小时之后有个用户来请求获取当前播放的歌曲的时候,这时候就可以知道这个歌曲已经播完了,应该执行切歌。
而且,只要遵循了上面这个设计思路,“你是今天第 N 位点歌的人,你的歌曲将会在几点几分播放” 也很容易做了:
N 由计数器变量 count 控制
几点几分其实就是距离今天 0 点多少秒,这由歌曲 start_at 决定
几点几分其实就是距离今天 0 点多少秒,这由歌曲 start_at 决定
# 怎么做一个播放器
播放器做了很多了,可以看看这篇:
因为这次是微信网页上的播放器,稍微有些不一样,在 iOS 上的歌曲绝不会执行一下 music.play 就自动播放,必须借助事件才能播放 (在点击事件的回调里面执行 play)
# 怎么做弹幕系统
这部分需要做 Websocket,这里我用的是 socket.io 去实现的。

发送、接收弹幕
# Nginx & Mongoose & Redis ↵
许愿墙挂在 https 上,还有七牛的存储也是 https 的,因此需要两个证书,具体的申请都是走腾讯云的免费 SSL 证书流程拿到的,很容易申请到。
此外 nginx 还要负责 index.html 的存储,以及后台接口的反向代理。
数据库用的是 Mongo,具体的使用是通过 Mongoose 进行的,不过 Mongoose 自带的 Promise 并不是原生的 Promise,而是 mpromise,它并不是 Promise A+ 规范的,行为上跟标准有差异(查了资料看到说是为了性能),因此吃了点屎。
如果要使用原生的 Promise 作为 Mongoose 的 Promise 只需要执行如下代码即可:
00let mongoose = require('mongoose'); 01mongoose.Promise = Promise
还有就是学了挺多一些查询技巧、模糊查找这些、有机会开一篇来谈。
对 Mongoose 的利用可以看我的这篇博客:
缓存的话使用的是 redis,用来做… 做各种缓存…
比如首页分页,用户个人信息,cookie密文/明文组成的键值对,微信的AccessToken等等
主要是利用 JSON.stringify 存入缓存里面,需要用的时候还需要 JSON.parse 一次才可以使用。
# Webscoket ↵
许愿墙的 Websocket 在页面加载的时候会尝试登录,登录成功之后会返回三条重要数据:
- 用户个人资料
- 许愿墙客服小哥的资料
- 未读消息
前端代码接下来会处理这些,数据最终流到 localStorage 里并影响对应的页面。
# 战报 ↵
许愿墙,七天内:
- 5000+ 用户
- 1500+ 条愿望
- 100 G 流量
最后我确信了一点,腾讯云1兆的服务器完全能撑得起整个应用,而且还非常有余力。
# TL;DR ↵
啦啦啦,做完这个挺开心的,算是阶段性胜利啦 ~
好了,回宿舍。