今年的腾讯互娱市场体系年会,由tgideas团队制作了一款以线下服务为主的微信小程序,参与到员工大会以及晚宴的多项环节中,应用的技术包括:公司智能网关+iBeacon判定身份;小程序拉起导航;小程序云函数,云数据库,云储存,多场景使用实时数据库;小程序支付能力;内嵌H5小游戏;小程序红包能力。
背景
本小程序是服务于TIEM市场体系年会的辅助工具和互动工具。年会参与员工1200+人,还有几十个外部嘉宾,年会分下午的员工大会,红地毯以及晚宴节目表演等环节。而小程序在其中的具体功能点,以及对应的核心技术如下:
1. 智能网关判定是否员工,iBeacon判断是否现场嘉宾;
2. 实时数据库更新会议+红地毯+晚宴节目的进程;
3. 现场辩论赛环节,实时数据库展示投票并实时呈现在舞台侧屏(类奇葩说);
4. 实时数据库更新红地毯进程(部门每一个团队排队入场的过程);
5. 精彩现场通过云储存以瀑布流方式,提供员工上传照片和预览照片功能;
6. 晚宴节目里,可以对员工表演的节目进行免费支持和付费打赏,并通过实时数据库在小程序以及舞台侧屏呈现;
7. 舞台大屏每次的web抽奖结果通过HTTP API同步到小程序云数据库;
8. 晚宴期间全场H5互动小游戏,每个人的游戏分数以HTTP API汇总在他选择的桌总分中,以桌为单位角逐第一,并在舞台大屏通过实时数据库能力呈现前三名的桌号和总分;
9. 每一个体验过游戏的人,在回到小程序的游戏大厅后会分配到一个由小程序红包能力的现金红包。
在贴一下对应的逻辑图(绿色为页面实体)
登录逻辑 年会逻辑
结合以上流程的实际效果图
技术职能分配:
- 总PM:东莞哥
- 顾问:超哥 花叔
- 项目PM:啊铭,负责整体方案和流程设计,交互稿设计,大屏展示的页面,小程序“精彩现场”部分对应的UI重构、前端交互实现、小程序云开发、数据库设计;
- 运维:啊贤,后台服务等后端开发;
- 接口:啊苏+玉环,负责腾讯智能网关接入功能,小程序支付功能,小程序红包功能,现场大屏的web端与小程序云端数据打通等开发;
- 设计:艾斯,哈哇夷;
- H5小游戏:虎哥;
- 抽奖:雨哥,负责抽奖模块的舞台大屏展示的web端开发;
- 我负责小程序部分,以上图片绿色的页面(除精彩现场分支)对应的UI重构、前端交互实现、小程序云开发、数据库设计,以及小程序整体的交互抛光和体验包浆。 从分工来就可以看出这个项目的复杂性,涉及到的人员也比较多,我已经很多次跟同事朋友口述整个项目的流程和逻辑,都花费了很久,所以这里只能挑选我所涉及到的部分做总结和沉淀,而其他部分的请等待当事人的文章出炉。
iBeacon
我们团队玩iBeacon已经玩了好几年了,更精细的文章之前也有了。而且我们此次的小程序对iBeacon的要求不高,只要监测到iBeacon就可以了。 我从看微信小程序的相关API到实体设备调试,甚至具体测试都做了,也不过2-3个小时的时间,所以也没有写太多的逻辑。这里就跳过了。
云开发
前文交代的有点多,还是回到主题吧。这次是我第一次接触云开发,1个月内要从无到有并完成任务,任务重,时间紧,我也是莫名信心的开始了。事实证明小程序云开发,像我这么一个没有什么后台开发经验的前端,也能上手。
而我也是从小程序创建的时候自带的demo开始上手,只要在创建的时候勾选云开发选项,就有相对应的云开发demo了。里面可以对云开发的能力有一个大致的了解。
小程序云开发的好处是,开箱即用,当你在微信开发工具创建任务的时候,其实就已经开通了云开发功能。而云开发的API都挂靠在wx.下,非常友好。原生的微信能力集成,云函数自带鉴权,可以获取openid等信息。
增强编译
之前有小伙伴看到我写代码的时候看到了Async/await,说什么时候小程序支持这个了?所以我这里稍稍提一下:
如果你需要用到以上能力,记得打开增强编译。这里是官方说明链接,看完你要回来。
云函数
简单说一下云函数:顾名思义,就是运行在云端的函数。其实也是JS语法,只是当这段JS运行在云端后,云端赋予它的能力比在小程序端里的能力更强一些,比如它可以直接得到openid,比如它比小程序端拥有更多的数据操作方式和权限,比如它可以让web端跟小程序端沟通等等。而我用的最多的云函数,基本都是用在操作云数据库上。而且云函数的本地调试也非常方便,云函数本地调试和小程序模拟器深度集成,从开发者工具小程序发起对该云函数的请求均会到此而不是云端。
云数据库
毕竟第一次接触云数据库,对里面的很多API都不清楚,问了一下懂MongoDB的小伙伴,他们也觉得小程序数据库虽然是基于MongoDB的,但是却并不太一样。那只好自己摸索着来了,在项目的过程中,数据库一块的API是查了一遍又一遍,说说自己的感受,避免新人踩坑。
比如以上API,对数据库多条记录进行删除,红框内说明,此方法可以在小程序端版本2.9.4以上,云端云函数使用,所以如果有时候你觉得你使用的API报错了,你可能得对应一下这里的官方信息看看了。同时如果你使用了这条方法,那你也要在小程序开发工具设置“调试基础库”版本,以及小程序网页管理端设置一下基础库最低版本。
从简单的cloud.database().command就能看出,基础库不一样,它们包含的方法也不一样。下图蓝色是新基础库多出的能力。
数据库的每一个collection,相当于我们平时获取的json,我们可以通过云函数或者小程序端脚本进行增删改查(注意基础库版本),但是云函数和小程序端脚本的权限还是有区别的。比如最基础的查,如果通过云函数查询数据,可以返回上限为100条的记录,而小程序脚本返回的上限为20条。这些原则性的规则,在我们考虑是把脚本放在小程序里,还是云函数里的时候,都是要注意的。这甚至会影响到我们对数据库的设计。
比如我们小程序的游戏大厅:
这里1-30桌(去除尾号4桌号,其实是27桌),每桌12人。如果我们按照每个人的加入作为一条记录add到colletion中,则可能出现12X27=324条记录。假设当第201人进入的时候,要展示1-30桌的入座情况,需要反复查询(小程序脚本每次只能吐20条记录,所以需要查询200/20=10次;云函数每次能吐100条记录,则需要查询200/100=2次)每一个人的记录才能正确显示座位的对应关系。而这样的数据库设计在小程序云数据库里是不合理的。而我认为比较合理的方式,在后文会介绍。 而类似‘签到’这种,只需要获取自己是否签到过的记录,就可以直接在小程序脚本上直接get,直接add。
集合设计
我们的员工大会小程序设计的数据库colletion:
作用
|
表名
|
读写
|
小程序端管理后台
|
adminConfig
|
白名单人员可读写
|
白名单
|
adminList
|
管理员的openid
|
星光盛典获奖列表
|
awardsList
|
静态库,开发工具编辑
|
辩论赛信息
|
battleData
|
静态库,开发工具编辑
|
投票记录
|
battleVote
|
用户可读写自己的记录
|
红地毯顺序
|
carpetList
|
静态库,开发工具编辑
|
晚宴安排清单
|
dinnerList
|
静态库,开发工具编辑
|
iBeacon设备ID
|
ibeaconArr
|
iBeacon硬件信息
|
员工大会进程
|
meetingList
|
静态库,开发工具编辑
|
节目信息
|
shows
|
静态库,开发工具编辑
|
签到信息
|
signList
|
用户可读写自己的记录
|
游戏大厅
|
tableDataNew
|
每桌一条记录,员工可修改读写
|
小程序UV
|
userData
|
每个openid只能写一次
|
这里有很多静态库,是用做小程序发布后,方便修改文案的作用。我们知道小程序一旦提审发布后,如果写死在前端的文案图片等修改,都得再走一遍提审发布流程。所以我们把所有有可能修改的文案都放到云数据库,图片则放到外部服务器。 我们还设计了一个非常完整的小程序端后台页面,方便控制大家手里的小程序呼应每一个现场环节。其实这个管理端也是我梳理整个会场流程的一种方式,一种明确需求的方式,一种梳理需求方最终想要效果的方式,而这种方式在对整个小程序的实时数据库设计,对小程序的性能优化都起到非常大的作用。
这个管理端也确实没有辜负我们的期望,现场只需要一个管理端就做到了所有的节奏掌控。虽然我们也在现场准备了电脑,打开了后端做备案。
实时数据库
我其实最想分享的就是腾讯云联合小程序下半年退出的这个能力--实时数据推送。
上文列举功能点的时候,这个关键词出现了多次,也说明这个功能在我们项目里的使用频次。
因为是服务线下的小程序,我们在很多场景都是配合主持人的节奏,进行高效且快速的实时数据反馈。如以上提到的投票,打赏,游戏大厅等。
在没有实时数据推送能力之前,以上的常规的方式就是轮询请求,设计一个反复监测后台数据是否有变化的函数,这不但浪费带宽,也浪费计算资源,结合小程序云的套餐说明 ,也能发现这是不小的费用成本。
如果降低轮询频次,又会得不到及时反馈,降低用户体验。
如果使用长连的方式,开发和管理的成本无疑增加,分布性和一致性也是问题。
而微信小程序提供的实时推送能力,很简单,只有一个API,就是watch 记住这个关键词,它就是实时数据推送的代名词 官方自研能力,开箱即用,无需管理长连,无需编写服务端代码,而且不占用数据库连接数(感谢周子杰,邓坤力的解答)简而言之就是官方赠送的。
从官方的云开发demo中包含的一个聊天场景就能看出,实时数据推送能力对聊天室,聊天模块等的需要即时通讯功能天生友好。而我们的年会小程序“打赏后即时反馈”的功能也很契合,还有我们小程序里的游戏大厅,都使用到了该项技术。由此可推,在小游戏上应该也有大展拳脚的地方,比如棋牌类的,已经可以展望它给小游戏开发的交互带来更多的可能性。 接下来具体说说我们是怎么应用实时数据推送能力的。 watch应用——年会全程节奏掌控 我在小程序relaunch的时候,起了一个watch,监听了adminConfig,并把监听到的数据变化,写入到当前页面的data里,这样就可以改变界面状态。
先上一个动图,可以感受得到左边的小程序管理端只要有所操作,右边的所有用户的小程序端都会即时产生对应的变化:
//业务逻辑太复杂,这里写伪代码
//app.js
App({
loadConfig:function(){
//可以把它当成是一个setInterval,设置一个ID给它,方便关闭它
this.globalData.adminWatch = db.collection("adminConfig").watch({
onChange: function(res) {
let {
_id,
...adminConfig
} = res.docs[0];
console.log('配置改变:', adminConfig)
const _page = getCurrentPages();
if (_page) {
_page[_page.length - 1].setData({ //这里很关键,把监听到的变化驱动当前界面的变化
adminConfig
})
}
},
onShow:function(){
this.loadConfig() //每次唤起小程序都开启监听
},
onHide:function(){
this.globalData.adminWatch.close() //每次隐藏小程序都关闭监听
}
})
//某个具体页面的比如game.xml,就可以依赖以上的adminConfig做状态判断了。
<view wx:if="{{adminConfig.dinnerGame}}">游戏正式开始</view>
<view wx:else>游戏试玩</view>
前文说过,我们的小程序管理端页面,其实都是在对一个名为adminConfig的colletion做修改。而我们在打开小程序的第一时间就开启监听此adminConfig,当它有什么变化,则会驱动我们的界面相应变化。 而我们也需要设计小程序的每一个需要被控制的页面,结构与逻辑都想办法依赖这个adminConfig,不然没办法驱动此页面的界面变化。 比如下图,我们在管理端点击“小游戏入口开启”后,大家的小程序界面瞬间多了一个banner入口; 点击“游戏开始”后,大家的小程序界面则会瞬间从“开始试玩”变成“点击进入”。
而其他的类似节目菜单标红,都是一样的实现原理。
我们就是通过实时数据推送,做到在每一个员工/嘉宾的小程序界面上,实时更改他们的界面,改变员工大会晚宴节目进程。 辩论赛投票/锁票操作也一样是利用这个原理。
watch应用——打赏实时推送
我们在每一个员工精心编排的现场表演里,加入了一个入口:
当主持人宣布节目开始的时候,我们在之前的adminConfig里配置当前节目状态,点击上图的入口进入后,我们可以通过打赏界面免费打赏和支付打赏。
这里对实时数据库的应用就有点象官方demo聊天室了。 这也是咱们腾讯这么多年会第一次通过线上支付的方式为给我们表演节目的同事们表达谢意了吧?看咱们的老板们多么激动。这里的实时数据推送,更是考验watch的能力。 然而。。。watch并不是瓶颈,watch的及时性与密度有多变态呢?我们100人在1秒内同时update数据库,watch很可能给你推送100次。数据落盘DB=>数据从服务端推送出去,这里是5-10ms。所以。。。setData才可能成为瓶颈。
在watch面前,连setData都有可能成为性能瓶颈
打赏页面的逻辑是,用户在打赏的时候,我们就在colletion为rewards的集合里add一条记录。然后我们的实时数据推送watch将监听整个rewards的数据改变,watch的onChange事件吐出了啥数据,我们就把这些数据洗一遍,然后直接setData到我们的界面上。
看起来没问题,自己测试的时候也没问题,每次送上小爱心,界面的打赏记录也会瞬间反馈我的打赏行为。我也觉得这一切都如此合理。
其实这里存在了一个很隐蔽的问题,如果不是我们利用年会演习的机会测试了一次,如果不是东莞哥心细如发体验到了此问题,估计到了现场将会是一个灾难。
存在的危机:当有很多人同时在打赏的时候,watch的onChange几乎是无微(每条记录)不至(推送到达)的,在每一次onChange都会反馈到小程序端,也就是每次都会触发setData去驱动界面渲染一次。watch并不是我们想象的,会缓存一波数据改变再推送过来,它的反馈是如此直接暴力,用户add一条,它就推送一条,数据落盘DB到数据从服务端推送出去,这里仅仅是5-10ms,也就是说除非在这5-10ms内有多个用户同时add数据,才会出现返回的记录length是2的情况。试想如果每秒都有10个记录反馈,那setData就得操作10次。而setData是一个异步行为,也就意味着它的执行是需要时间的。我们如果操作唤起“立即打赏”面板(其实也是一次setData操作),则会面临,我们需要在1秒10次的setData间隙中,插入这次的唤起界面的操作,但是试想我们怎么能有设备快?setData一旦空闲,立刻就被onChange事件的反馈自动执行并占用了。而我们的唤起界面的操作就被阻挡在外了,连排队的资格都没有(setData没有排队的设定)。
如果在现场大家想打赏却唤不起打赏面板,那将是很尴尬的事情。如果涉及到收入什么的(比如此处涉及到了微信支付),那就是运营事故了~~
问题已经发现并抛出来了,解决起来就简单了。我们的解决方式是: javascript
onChange推送过来的数据堆在一个arr变量里:
onChange:function(res){
_this.arr.push(res.doc)
}
然后setInterval一个函数,每秒执行一次:
{
把arr的值setData
然后再清空arr
}
我以这种自然语言描述代码,是不是更直观? 这样就很难在点击唤起界面的setData执行的时候,watch正在占用setData了。
这里还要补充一个注意事项,watch有一个能力限制,只能监视一个collection的5000条记录。比如上面的打赏数据库,设计的是每一次打赏都add一条记录的话,如果超过5000条后,watch事件就会报错。
所以我们一定要注意数据库的设计,避免watch的collection超过5000条记录的情况! watch走另一个通道,不占用套餐里标识的同时连接数,但是默认最高支持1W的监听数。 比如我之前说的adminConfig里的设计,只有一条记录,即便是全局watch,也毫无压力。
watch应用——游戏大厅
游戏大厅可能是我在整个小程序里花费最多的一个环节。这里先上个界面:
刚开始进到这个界面,自己是没有入座的,自己现场就餐的桌号,对应此处的桌号,选择桌号入座(主持人会宣读);入座后如果发现自己桌号不对,可以直接点击正确的桌号进行再次入座,也可以先点离开,再选择正确桌号入座。这里就涉及到2次云函数的调用了,在弱网环境可能造成1秒的延迟,而如果延迟过程有人跟你选择同样的桌号,而此桌已经11人了(每桌限制12人),则会造成数据错误。所以这里的查询条件以及update方式都有讲究。我是经过了3次半的改版,才将体验优化到勉强能让大家接收的地步。
第一次:表的设计本身不合理,只是为了能跑通整个交互,达到加入,离开,直接开始的基础目标,但是不光是性能不行,点击入座到界面展示完成需要1.5+秒,前端的表现也出现不一致性,主要体现在头像已经在桌位上了,但是延迟0.5秒后才显示“直接开始”和“离开”按钮。
第二次:优化了表结构,每桌作为一个固定的表记录,每一个玩家只是这个记录里的一个member字段数组里的一个对象,但是这次优化出现,当频繁点击“加入”别的桌,会有多个自己头像的BUG。也就是一个用户可以占多桌,这明显是不合理的。而且换桌的逻辑涉及到了4次云函数的调用计算,云函数计算时间800ms左右。
第三次:对云数据库的了解更深入后,对云函数的优化从换桌的4次调用变成2次。之前是先查询目标桌是否满座,然后再进行插入数据;优化后则是直接插入,如果插入不了,则返回status='0'的状态码通知前端。避免了换桌出现多个自己头像的bug,避免11人一桌,2人同时抢桌造成数据异常的情况。云函数计算时间也缩短到400ms以内。在跳桌的时候,先做目标桌加入本人信息的操作,再做当前桌delete本人信息操作,让delete的时间不需要等待。
第三次半:优化了交互,点击加入的时候增加一个showLoading,避免静态等待的同时,也避免了弱网下反复跳桌造成的渲染bug。 虽然勉强可以使用了,但是当我看到“刺激战场”自定义房间的跳桌的体验后。。。这完全感觉不到延迟啊!光子爸爸就是厉害。。。难道是因为吃鸡客户端用的是UDP协议的关系?
后来微信小程序组的林超同学补充,以上的截图为云函数调试界面的控制台截图,其中因为是开发工具的缘故,耗时远大于实体机器上的真正耗时。我们可以参考数据,达到优化目的。
总结一下游戏大厅的关键优化点:
- 数据表的结构设计,最好以可以估算上限的单位作为记录本身,比如桌子数量;
- 加入桌子的数据操作是update,member的增加用数据库API自增_.inc,多个用户同时写,对数据库来说都是将字段自增,不会有后来者覆写前者的情况;
- 如果是跳桌,先运行加入桌逻辑,再走离开当前桌逻辑;
- 要有针对防止多次点击的设计;
- 分区(1-30桌,31-60桌)渲染,分区watch,细节做到位,就不需要一直watch所有(1-120桌)的桌子了;
- 更少的查询次数,更详细的查询条件;
- 在适当的环节加入带mask的showLoading,可以避免用户行为的互相干扰。
对这种多人参与,彼此会互相关系,即时性要求也比较高,对数据一致性也要保障的小程序场景,后续建议给自己评估多一倍的时间去应对,这样才能保障最终的落地,带来良好的体验。一味的从一个方向去优化,也许并不是一个好办法。比如云函数的操作瓶颈是,至少150+ms的回调时间,那么你再怎么优化体验,也不会超出这个范畴;而如果通过加入适当的loading动画,屏蔽其他操作,则无形中让用户少了尴尬的等待,也提高了交互的稳定性。
总结
此次TIEM的年会小程序,从无到有,从设计到研发到测试,大概就是1个月的时间,当我们团队怀着忐忑的心情,直到年会结束,顺利完成了这个稍稍超出我们预估范畴的任务后,大伙才放下了一颗悬着的心。毕竟我们复盘的时候,才发现我们还有很多没做到位,比如线下活动的弱网环境测试,我们就没做。所以一些同事在游戏大厅,开启不了H5游戏,我们只能表示非常抱歉!是我们工作没做好。我们一直感慨,如果再来一次,我们一定会花那电信特殊通道的钱,给会场拉一个特供WIFI,而不再需要大家去抢基站或者酒店的信号。
而老板们秘书们也给予我们好的评价和肯定,同事们也提到一些中肯的建议,这都是我们整个团队的收获。
而我自己的收获是很大的,之前感觉云开发是一个很遥远的工作,毕竟我们一直都有特定的岗位在做这部分工作,而我只需要用他提供的下游接口就好。 经过此项目,我不仅通过各种方式学习了云开发,云函数,也深入探索了小程序实时数据推送的能力。 而我做为一个前端er,也能结合自己对交互体验的理解和洞察,为小程序整体的交互和体验抛光,弥补交互初期的设计闭环,让整个产品显得更圆润,更符合自然人的操作习惯。 同时我也对线下活动的支持有了更深入的体会,品尝到了后台人员的艰辛。
最后,祝大家2020吉祥如意!Band Together!
关注TGIDEAS团队