- 【高校开发者】小程序,遇见你真好 ;
前言 很荣幸自己能够在微信小程序开发社区上面发布一篇关于程序开发的经验分享文章,文章标题有点奇怪,不是我写错,而是是我故意的哈哈哈哈😄,“;”是为了表示我和小程序还有将来。 在这三年的大学生活以来,除了课程报告,本人几乎很少写技术性/经验分享的文章,博客编写也基本没接触过o(︶︿︶)o 。 本人也将近毕业了,学习的道路走得越远,越觉得自己缺乏的是知识的沉淀,因此自己也正准备搭建一个(高大上)的博客,用来记录我作为程序员的一生。 噢好像话题跑偏了嘿嘿😁, 我是2019年微信开发者大赛小程序组的一个不知名队伍,作品是一个答题小程序,名称叫 “不服来挑战我”。本来臆想是没有“我”字的,但因为被注册了,就没办法了(希望那个开发者看到这篇文章可以大方让给我(一脸正经调皮脸))。 1. 设计背景 在一个偶然的机会,我在大一暑假开始接触微信小程序/小游戏生态环境。我找到一份初创公司的小游戏实习工作(当时小游戏爆火,曾经我对此很感兴趣),负责业余功能的实现。 那时候我很高兴,毕竟那是我人生第一次工作啊! 但经过两个多月的996工作“折磨”,我对程序员的工作有点害怕了。 不过,那也是一瞬间的事,不然我为什么会在这里分享经验呢。 现在回想起那段时间,我的脑海总会冒出一句话:“纵你虐我千百遍,我仍待你如初恋”。咳咳虽然是百度的,但此话的确表明了我的心。因为我在那段时间,学到了很多,从人生到理想,从后端到前端,从基础到复杂,从设计到架构……我真的很感谢那段时间公司老板倾力授予我的各种知识。 在此,我首先感谢他,感谢他在暑假指导我,如果没有他,我估计也走不进程序之美的地方。 [图片] (哎又跑偏了,各位见谅!)回到正题,“不服来挑战我”答题小程序是我在2019年寒假就开始着手开发的小程序,其实当时我并不知道有微信开发者大赛的,模糊记得2018年举办过一次。但我心中还是只有一个念头:通过我在大学中学到的知识,把我的idea实现出来!所以比赛只是顺手参加的。[图片] 由于我自己英语水平比较低,在校期间想提升一下,但用了好多app都不是我的菜,咱又没女朋友一起学习,所以就想自己搞一个程序学习英语。偶然接触到“一站到底”之类的节目,我就想要不写个答题程序? 我想我好像政治也不咋地,要不再加个政治题目呢?背熟点大三考研。 然后想到我曾经可是做过小游戏的呀!而且小游戏挺火的,嗯那就做个答题小游戏吧。然后就对我的创意进行编织、架构,后面想起,小游戏 发布要软著,这要咋弄呢,我当初做小游戏的时候都是客户提供的,而且还挺麻烦的。心里面又打退堂鼓了。后来发现除了小游戏,还有个小程序呀,我做个小程序不就好了,程序的主题玩法应该不受前端架构的影响才对。然后就确定了做个答题小程序。 接着看了小程序的文档,发现入门很简单,非常简单,十分之简单(划重点),这使我信心十足。 然后想着我一个人做那不得累死,就找了一个也对小程序感兴趣的好朋友一起做。我们从想法、架构聊到UI设计。我主要负责后端代码实现,以及前端逻辑层对接;他负责前端UI设计。至于后来题库变多,是因为我打算做出给想要学习的人群使用的,可以学习一下专业知识。然后在开学后就找个同学负责测试和题库收集。 寒假那段时间,我们每天7点干到晚上1点到2点多,连过年都是在打代码,所以有借口一脸正经地对家人说我在学习!那是我最嗨皮的一段时间了。 后台架构花太多时间了,现在还有一些功能还没完善,时间也有限,这学期也挺忙的。 3. 小程序介绍 小程序第一版功能架构挺详细的,分享给大家看看: [图片] 但后面发现有些功能没法实现了,然后有一些又麻烦,就暂时还没实现出来。 现在的版本,主要玩法有匹配功能和好友对战功能,其他的功能有任务系统、排行榜每日签到等,然后最近加了个上传题库。目前打算把小程序发展到学习交流范围,日后整理好数据,会给用户提供题库下载的。 最终成型作品如下: [图片][图片][图片][图片] [图片] 没有美术属性加成,界面有点丑… 4. 开发前准备 团队开发过程中,只有充足的准备才能达到合理分工,开发高效的效果。 首先,代码管理是一个很重要的问题,作为程序员,每天编码不会很少的(如果有那是大佬),如果发现了写错了,撤销或删除也可以,但是如果程序中增加了一个功能可能有100行代码,然后在程序的其他地方进行调用,那这个时候我们就不好找了;除此之外,每写一个功能都要备份一次,不备份万一弄丢就心疼了;最重要的一点就是对人操作同一个文件,最终需要整合一起。 所以代码管理工具就是为了解决类似问题而产生的。因此,团队开发中,我建议一定要学会使用代码管理工具,比如git或者svn,即使在学习过程中,也可以使用它来存储我们日常训练的代码的。然后可以装一些git的GUI工具,我使用的是SourceTree,方便快捷,不过萌新学习期间还是推荐用命令行,不然很难理解并且深入学习到git的原理的。 [图片] 其次,作为后端开发者,编写规范的API文档是必不可少的,大家可以上网查找一些在线文档编辑工具,我用的是ShowDoc,把团队成员加在一起就可以共享文档项目了,上面还有数据库设计文档的模板。 [图片] 接着的话,在设计数据库的时候,可以先用visio等绘图软件绘制ER图,然后再用数据库相关GUI设计数据库表。GUI我使用的是Navicat,在上面进行数据库操作的时候比较方便。 [图片] 最后就是前后端语言开发工具了,前端的话首选微信开发工具,尽管目前工具还是有一些bug,但我相信经过我们开发者群体的使用并详细反馈,以及微信小程序开发工程师的维护后,它会变得更好更优秀的! [图片] 5. 后台架构 重点来了,在这里我会详细给大家说一说我的后台架构以及一些重要的功能实现。 首先,小程序后端使用的是node+express+mysql框架,微信登录使用官方的wafer-node-sdk。 由于我的小程序本质上属于一个游戏,因此在后端设计上需要重视一下性能与稳定性。参考了网上一些解释,我在此重复一下,文笔不好,望见谅。 (借用网上的解释) 游戏的运行是一个多进程协同工作的过程,因此开发的复杂度会大大提高。同时,我们也需要关注小程序对手机内存和CPU的使用程度,以求在特定业务代码下,能尽量满足承载量和响应延迟的需求。除此之外,网卡也是另外一个约束因素,网络带宽直接限制了服务器的处理能力,所以游戏服务器架构也必定要考虑这个因素。因此,解决以上的问题,最基本的做法就是“时空转换”,用各种缓存的方式来开发程序,以求在CPU时间和内存空间上取得合适的平衡。对于游戏服务器架构设计来说,最重要的是利用游戏产品的需求约束,从而优化出对此特定功能最合适的“时-空”架构。并且最小化对网络带宽的占用。 [图片] 基于上述的分析模型,对于游戏服务端架构,最重要的三个部分就是,如何使用CPU、内存、网卡的设计: 内存架构:主要决定服务器如何使用内存,以保证尽量少的内存泄漏的可能,以及最大化利用服务器端内存来提高承载量,降低服务延迟。比如在我的小程序里面遇到一个空闲房间占有内存问题:玩家创建了一个房间,同时服务器建立一个监听方法,启动空闲计时器,监听房间是否空闲,如果房间的人数一直有变动,则监听方法则会重新更新时间,计时器结束, 解散房间,游戏开始,则清除房间的空闲计时器,从而提高服务器的内存使用效率,避免内存泄露。 调度架构:设计如何使用进程、线程这些对于CPU调度的方案。选择同步、异步操作等不同的编程模型,以提高服务器的稳定性和承载量。同时也要考虑对于开发带来的复杂度问题,例如开始游戏扣除房费、结算时更新玩家经验值、等级以及虚拟货币这些都需要异步处理。 通信模式:决定使用何种方式通讯。网络通讯包含有传输层的选择,如TCP/UDP,Socket通讯等;跟据表达层的选择,如定义协议;以及应用层的接口设计,如消息队列、事件分发、远程调用等;在我的服务器里面,账号服与大厅服使用TCP进行消息传输,游戏服使用Socket进行游戏逻辑通讯。 整个程序运行的服务器在设计上分为三层服务器:账号服务器、大厅服务器以及游戏运行服务器: [图片] 账号服务器:主要用于处理用户注册和登录游戏系统。用户是否重复登录游戏也将在账号服务器上处理。账号服务器由主备构成,任何时候只有主账号服务器负责和客户端及GATE服务器交互。 大厅服务器:响应账号服务器和游戏服务器的请求,将游戏列表和房间信息返回给它们。当用户登录成功后将和大厅服务器保持长连接以实时获取游戏系统的信息。当用户在大厅里创建房间时,大厅服务器会根据存储的游戏服务器信息,依靠负载均衡算法把用户分布到任何一台游戏服务器上,以此保证工作任务分摊到多个处理单元,即减轻了单个服务器的承载力,也提高了并发处理能力。 游戏服务器:服务器在启动时将自己注册进入大厅服务器,关闭时从大厅服务器里销毁自己,同时在线玩家进入房间时还会要求大厅服务器更新在线人数。当用户在大厅中点击进入某个游戏时,用户将登录相应的游戏服务器进行游戏。 6. 相关功能分析 6.1 快速匹配 快速匹配这一个模块比较复杂,主要的核心思想是使用了状态机思想来实现的,以及整个游戏包括房间内的一切操作都使用状态机的思想来实现的。首先用户在大厅里点击快速匹配,选择完房间类型后,把房间的配置信息发送给大厅服务器,大厅服务器再把信息转发给网关,网关服根据房间信息查找是否存在此类型的房间,如果存在,则会创建一间临时房间供玩家入座,玩家入座后与后台建立Socket连接,并且创建房间状态机,房间状态机控制一切玩家在房间内的操作。然后可手动点击快速匹配,然后服务器会触发匹配状态机,从而把玩家安排进匹配队列中,如果匹配队列的人数达不到房间要求,则触发机器人调度器,给玩家分配机器人陪玩。 [图片] [图片] 相关算法如下: [代码]let waitingPlayers = this.getOtherMatchingPlayers(player); if (waitingPlayers.length + 1 === this.descriptor.numberOfSeats) { if (player.socket) { //房间没换, 也一样让客户端发起到新房间的登录 player.socket.emit(`startMatchingReply`, { newState: `Matched`, roomNumber: player.waitingRoom.roomNumber, chuShiMultiple: player.waitingRoom.config.initialMultiple, }); } for (let i = 0; i < waitingPlayers.length; i++) { if (waitingPlayers[i].socket) { //房间换了, 客户端需要发起到新房间的登录 waitingPlayers[i].socket.emit(`matchingComplete`, { newState: `Matched`, roomNumber: player.waitingRoom.roomNumber, chuShiMultiple: player.waitingRoom.config.initialMultiple, }); } } this.playerCompleteOneMatching(player, waitingPlayers); } [代码] 6.2 游戏运行时流程 [图片] 由于休闲对战类游戏的性质,导致游戏前期需要机器人的支持才能渡过艰难的前期运营,所以我在快速匹配中实现了机器人陪玩的功能,当然由于游戏的难度,以及个人能力有限,机器人的正确率与答题时间目前只能用随机算法来决定。游戏状态机如下: [图片] 状态机相关算法如下: [图片] 6.3 游戏对局信息重建 在玩家对战类型的游戏里面,游戏场景重建是一个十分重要的技术,当玩家正在游戏的时候,由于网络原因或者有意与服务器断开,此时网络恢复畅通或者玩家重新回到游戏中,这是就需要把玩家离场期间发生的所有消息一一重建。 根据上述状态机我们可以知道,游戏期间有五个状态,对于这五个状态而言,服务器都会下发消息通知客户端执行了什么,此时服务器会把下发给某个用户的消息记录进用户分身里面(非微信唯一用户,用户分身是游戏中创建的一个用户对象,游戏开始则创建,游戏结束则销毁),然后当用户断线重连时,客户端会检查消息记录里面都有那些发生过的消息,然后再一一重新发送回给服务器。 但是这时候会有一个很严重的问题,当你场景重建的同时,实际上玩家仍然在继续答题,这样就会导致客户端在处理各个消息的时候发生冲突,于是我对每个消息设立一个timer以及消息发送时当前时间date,这个timer是服务器处理消息时使用的时长,也就是理论上timer后客户端会收到消息;那么,客户端在场景重建的时候,就会用date+timer得出客户端收到消息时的理论时间,理论时间与场景重建时的当前时间做比较,如果前者大于后者的话,服务器会在时间差值内处理过期消息,直至同步至最新消息,反之则不处理。 这样就能避免消息冲突的情况了。那么消息重建的过程就如下所示: [图片] 客户端相关算法如下: [代码]reconstructFromPlayerMessages(roomData) { this.isReconstructing = false; if (roomData.playerMessages.length) { // this.beginRound(); let RoundMessagePlayer = require(`./../../model/RoundMessagePlayer.js`).instance; RoundMessagePlayer.currentActor = this; this.isReconstructing = true; console.log(`重构消息内容包括:`); roomData.playerMessages.forEach(msg => { console.log(JSON.stringify(msg)); }); console.log(`重构消息全`); let playerMessages = roomData.playerMessages; if (playerMessages.length > 0) { for (let i = 0; i < playerMessages.length; i++) { let message = playerMessages[i]; let eventName = message.name; let content = message.content; RoundMessagePlayer.addItem({ name: eventName, packet: content }); } RoundMessagePlayer.reconstruct(); } this.isReconstructing = false; } }, [代码] 6.4. 匹配机器人模式 众所周知,多人联网对战游戏大部分都是经过单机游戏演化过来的,而作为单机游戏,最重要的一点就是机器人陪玩了,其实,无论站在开发角度或者玩家角度来说,机器人陪玩是必不可少的,对于开发者来说可以节省很多时间,一个人边开发边进行游戏测试即可,而联网对战游戏上的机器人更是解决了游戏初期玩家无人匹配的尴尬场面,因此我也实现了匹配模式机器人调度的功能。启动机器人调度服务器,会加载出机器人数据,然后登录并连接上游戏服务器,当玩家触发了匹配程序时,调度器就会安排机器人进入队列,从而实现陪玩功能。 [图片] 后端相关算法如下: [代码]gateWaySocket.on(`connected`, function () { console.log(`恭喜, 调度器连接成功`); gateWaySocket.emit(`iScheduler`, {credential: `dfoakuewqnvlhdojfd`}); }); gateWaySocket.on(`iScheduler`, function () { console.log(`服务器确认我可以调度了`); }); gateWaySocket.on(`playerEnterGroup`, async function (desc) { if (idleRobots.length >= desc.numberOfSeats - 1) { for(let i = 1; i <= desc.numberOfSeats - 1; i++) { let first = randInt(0, idleRobots.length); let robot = idleRobots[first]; idleRobots.splice(first, 1); await robot.login(); if (!robot.loginData) { robotBecomeAvailable(robot); return; } let enterResult = await robot.enterGroup(desc.groupType, desc.gameType, desc.subjectType, desc.numberOfSeats); if (!enterResult.waitingRoomLoginData) { console.log(`机器人进入场地失败了`); robotBecomeAvailable(robot); return; } robot.doMatching(enterResult.waitingRoomLoginData); } } else { console.log(`糟糕透顶, 没有空闲的机器人`); } }); [代码] 6.5 运行内存清除 为了避免在游戏服务器上的内存泄露与溢出的情况,由于玩家是并发创建用户分身以及创建房间进行游戏的,如果不做内存清除的措施,服务器内存是会造成泄露或溢出的情况的。 [图片] 后端相关算法如下: [代码]/** * 恢复房间清除计时器 */ function _resetRoomIdleTimeout(room) { clearTimeout(room[idleTimeout]); _setRoomIdleTimeout(room); } /** * 设置房间清除计时器 */ function _setRoomIdleTimeout(room) { room[idleTimeout] = setTimeout(() => { roomMgr.removeRoom(room); delete room[idleTimeout]; delete room.onPlayerDidEnter; delete room.onPlayerDidLeave; }, allowedIdleTime); } //用户成功创建房间时使用, 马上启动空闲计时器, 计时器结束, 解散房间 exports.startWatchingRoom = function (room) { room.onPlayerDidEnter = function () { clearTimeout(room[idleTimeout]); }; room.onPlayerDidLeave = function () { if (room.numberOfPersons() === 0) { _resetRoomIdleTimeout(room); } }; _setRoomIdleTimeout(room); }; [代码] 7. 未来展望 小程序我也使用过很多,也发现有一些通病,比如小程序反馈不友好,无法长久挽留用户等问题。为了增加产品的粘性,我应该在后期会陆续丰富我的小程序的功能,提供更多用户下载,当然需要小程序的虚拟货币,同时也可以通过虚拟货币换取礼物(比如一些学习的工具书、周边产品等)的一些形式来让大家有所收获等等。希望我的作品能一直走下去。 [图片][图片] 8. 看法 在这个项目开发过程中,我第一次体会到自己开发一个程序并且在平台上面提供给所有人使用的感受。在此过程中我学到了很多,从前端的UI设计与交互到后端的网络编程、性能优化,还有运维方面的知识,例如域名备案、配置SSL证书、把项目部署在服务器上等等,除此之外,还有团队合作的精神,队员的交流沟通等,我觉得这些基础知识是我们开发者必须要掌握的一些技术。最后,衷心感谢微信提供这样的一个平台给我们开发者去尝试和为之努力。 小程序/小游戏正在蓬勃发展,我相信,在众多开发者的努力下,总有一天,这个生态环境会变得更好! 小程序/小游戏,遇见你真好;我们的故事未完!
2019-06-28 - #小程序大赛# 如何以产品视角打造一款小程序?
[图片] 5月31日,小程序大赛作品提交,终于告一段落。 这段时间,相信很多同学和我们一样,在项目过程中积累、收获了不少经验,或是设计,或是开发,或是其他,都能从不同角度体会到小程序大赛的意义。今天,我想分享一下,在项目过程中,是如何以一种产品的视角,去打造小程序“ddl冲鸭”。扫描下方小程序码即可体验哦~ [图片] 【写在前面】虽然本文是个人撰写,但小程序项目是团队共同完成,无论如何,在这里都应该先致谢我们“咕咕咕”队伍的指导老师和参赛队友,以及各位作为体验用户、帮忙测试程序并给予意见反馈的朋友。 本文篇幅较长,主要包括以下内容: 一、产品定位——目标用户与主要功能 二、产品灵感——为什么选择小程序“ddl冲鸭”? 三、关注用户——从用户角度进行需求分析与功能设计 四、竞品现状——我们如何做得比他人更好? 五、精益求精——抓住用户心理进行产品设计 六、把握细节——优化产品体验,提高用户粘性 七、创新运营——产品已从0到1,还要从1到100 八、小结 感谢阅读。 一、产品定位——目标用户与主要功能这次参赛作品是微信小程序“ddl冲鸭”,主要目标用户是高校学生,主要是为了解决大学生ddl记录、管理需求。“ddl”即“deadline”,意为“最后期限/截止时间”,相信很多同学对这个词并不陌生,甚至内心有着强烈的共鸣。大学生日常ddl:提交作业的ddl,提交社团工作方案的ddl,提交比赛作品的ddl……日常ddl任务繁多,高效记录与管理成一大需求。 然而,目前市场上并没有针对ddl推出的产品,因此,针对性地解决用户需求、并实现产品的特色化功能,可成为产品的一大亮点。 “ddl冲鸭”的主要功能有: (1)记录ddl(可设置ddl分类、使用文本或语音记录、上传相关图片、选择相关位置等); (2)通过将ddl设为星标、设置分类,以及查看不同状态,高效地管理ddl; (3)通过分享ddl,让好友了解自己的ddl,更可将好友的类似ddl(如同班同学具有共同的班级作业)直接添加为自己的ddl,简化手动创建步骤; (4)发起群组ddl,小组好友共同为ddl努力,交流感想,促进完成; (5)消息提醒功能,如ddl过期提醒等。 [图片] [图片] 由于介绍作品功能并非本文重点,更多详情可查看以下演示视频: [视频] 如果您看完了演示视频,相信接下来的大部分内容都更加易懂。 二、产品灵感——为什么选择小程序“ddl冲鸭”?同样作为大学生,我们深感ddl在大学日常生活中的影响,推出“ddl冲鸭”,在为自己解决需求的同时,也希望它能够给更多人带来方便。因此,我们从实际出发,着重于解决ddl记录、管理需求,并在此基础上力求创新、创意。 小程序的命名: 我们很熟悉的微信、淘宝、饿了么这些产品的命名,基本都是通过名称就可以比较直观地体现主要功能,同时又避免枯燥无味而缺乏新意。比如,微信为什么不叫“信息”,淘宝为什么不叫“购物”,饿了么为什么不叫“叫外卖”,所以产品的命名还是比较重要的。 “ddl”不仅与主题对应,同时引起目标用户内心强烈共鸣,也让用户对产品的主要功能有初步设想;“冲鸭”即“冲呀”,结合了当下的网络流行词语,符合大学生的喜好。同时,小程序命名也体现了希望用户在面对ddl时向前冲、加油努力。 小程序的出现,不是为了取代手机app,它是为了在适当的情景下,更好地为用户提供服务。如小程序官方所介绍的,它无需下载,实现了应用“触手可及”的梦想,也体现了“用完即走”的理念。相对app,小程序开发门槛较低,能够满足用户的简单需求,同时,结合微信本身为小程序奠定的基础,如“搜一搜”等,也有利于产品运营、降低引流成本。 相信使用iPhone的朋友都了解,iOS 12有个新功能,可以统计每天使用手机上各款app的时长。不难发现,对于多数人来说,微信使用时长稳居第一,且占比极大。既然如此,我们也应该思考基于微信环境下,用户的使用习惯。“ddl冲鸭”在UI设计方面,以白色为主色,绿色为辅色,与微信7.0版本设计风格基本对应,能让用户产生高度融入之感。同时,风格简洁直观,让用户易懂易操作。用户长期使用微信的习惯也为小程序的使用带来了方便的基础,比如使用微信小程序可以方便用户充分利用碎片时间,进行ddl管理。 三、关注用户——从用户角度进行需求分析与功能设计相信之前有关注过2019微信公开课的朋友,应该都对张小龙的这句话比较熟悉:“我们应该关注用户,而不是竞争对手。”作为一个产品策划的角色,应该有着“用户至上”的基本理念,同理心、善于换位思考成为了必备基本技能之一。所以我们从用户的角度出发,注重用户的实际需求,进行需求分析、管理、功能设计。 从宏观到微观,整体的功能设计都应该以解决用户实际需求为基本原则。 我们以问卷收集、用户访谈、竞品调研等多种形式,挖掘用户需求,同时也是为了针对市场上已有竞品的不足,针对性进行弥补。 [图片] 具体的调研分析,这里不进行详细介绍,但我们大致得到了如下结论: (1)用户现状:ddl记录与管理需求存在,产品开发必要性强——这充分论证了我们产品定位的合理性; (2)竞品现状:目前市场无针对ddl产品,用户个性化需求未被满足——在针对ddl主题进行产品设计的同时,我们也应该针对用户的个性化需求,打造具有特色的产品; (3)用户关联:一般在完成任务中有一定的好友互动,群组任务也较为常见——我们据此设计了好友分享ddl、群组ddl等功能,满足用户的常见需求; (4)用户习惯:存在ddl紧迫感、未完成愧疚感、已完成成就感等——便于我们抓住用户习惯与心理特点,完善产品设计; (5)用户期望:希望需求得到满足,产品体验良好——实现功能后,应该注意细节,追求为用户提供最好的产品体验。 进行需求分析、分类后,应对其进行分类管理。不可能第一个版本就解决所有的需求,也没有这个必要,因为这样会导致开发成本大、风险也高、且发展空间小。我们会着重于解决基本型需求与期望型需求,在此基础上完善魅力型需求,从各种细节优化出发,力求在满足功能需求的基础上提升用户对产品的体验好感度。 四、竞品现状——我们如何做得比他人更好?有的人会说,“ddl冲鸭”不就是一个记事本工具那样的产品吗?不是这样的,我们针对ddl主题,并对比竞品,实现了许多特色化的功能。以下谈谈“ddl冲鸭”的几个功能上的亮点: (1)ddl记录 区别于传统的文本记录,“ddl冲鸭”加入了语音、图片、位置等多种不同形式的记录,多样化完善ddl记录,满足用户的个性化需求。 比如,有时用户觉得输入大段文字记录比较麻烦,就可以用语音记录;上课时拍到老师的PPT,不方便转换为文字,就可以用图片记录;需要到某地办理港澳通行证,ddl又与位置有关等。 [图片] (2)ddl管理 在ddl列表页面可以快速切换不同状态的ddl(全部、进行中、已完成、已删除、已过期),并可以通过设置ddl星标、分类等,快速筛选符合条件的ddl,还可以选择按最新创建时间、ddl时间为ddl列表排序,使原本繁琐的ddl列表变得一目了然; [图片] (3)管理进行中的ddl 相信大家都很了解,许多类似产品会在待办事项右侧显示剩余时间,如3天、5天等,而“ddl冲鸭”则在此基础上,根据剩余时间的不同,给予不同颜色的文本和不同长度的进度条进行提示,时间越少时文字颜色越深(从绿色到黄色、橙色、红色)、同时进度条长度越小,从视觉上给予用户ddl紧迫之感,督促用户更快完成ddl。 [图片] (4)便捷的创建ddl 当与好友具有共同或类似ddl时(如同班同学具有相同的课程作业ddl),可直接将好友分享给自己的ddl添加为自己的ddl,简化了用户手动创建ddl的步骤,为用户带来了方便(创建时也可对原有内容进行修改)。 [图片] (5)群组ddl功能 为进一步增强用户互动与提高效率,设计了群组ddl功能,当一个小组具有共同的ddl(如班级课程作业小组)时,可发起群组ddl,大家在同一个ddl中共同加油努力,交流ddl感想,互相促进完成。但我们设计这个功能的定位是小组,而不是社区,因此群组上限为20人。 [图片] 以上是关于作品,在区别于其他竞品方面的几个特色功能。总体而言,“ddl冲鸭”具有以下几个特点: (1)用途明确,用户共鸣强 (2)使用便捷,用户粘性大 (3)形式新颖,用户兴趣大 五、精益求精——抓住用户心理进行产品设计根据调研结果,结合我们自身换位思考、深入体验,我们从用户心理的角度,精益求精,完善产品设计。 (1)未完成ddl的心理罪恶感——设计过期提醒功能 当有未完成的ddl时,进入小程序首页将会弹出提示,为鸭子落泪的图片,并播放“咕咕咕”的鸽子声,提醒用户已经放了自己的ddl的鸽子; 这样设计是为了与用户未完成ddl时的心理愧疚感对应,提醒用户以后要按时完成ddl任务,不能再放了自己的鸽子。 [图片] (2)无ddl时的悠闲感——无ddl时提示相关图片 没有ddl时,显示一只正在睡觉的鸭子,表明当前没有ddl任务的悠闲感,与用户当前的心理对应。 [图片] (3)有ddl时的紧迫感——通过不同颜色、进度条显示剩余时间 这个前面已经简单介绍过,即根据剩余时间的不同,给予不同颜色的文本和不同长度的进度条进行提示,时间越少时文字颜色越深(从绿色到黄色、橙色、红色)、同时进度条长度越小,从视觉上给予用户ddl紧迫之感,督促用户更快完成ddl。 [图片] (4)用户懒、害怕选择——合理设置默认值、提供方便用户的功能 ①创建ddl时,默认时间为当前时间三天后的22时,默认ddl类别为“学习”。这是根据我们调研结果,得出通常在ddl剩余三天时,用户会开始重视ddl,且当下大学生面临的多数ddl为学习方面的(如课程作业)。设置默认值,用户无需选择,方便用户。 [图片] ②记录ddl,可以粘贴文本、语音记录、将好友分享的ddl直接添加为自己的ddl,这些都是为了方便用户而推出的功能。 [图片] [图片] (5)用户渴望得到鼓励——弹图提示,群组交流 ①个人创建或完成ddl时,要与用户充满斗志的激情感、完成时的喜悦感与成就感对应,因此我们设计了相关的图片进行提示,而不是普通的文本模态框。 [图片] [图片] ②设计了群组功能,完成ddl时可以交流感想,鼓励他人;同时,群组排行榜也有利于促进用户完成ddl。 [图片] [图片] 六、把握细节——优化产品体验,提高用户黏性产品设计过程中,充分考虑多个细节。虽然它们不一定会对产品的主要功能产生影响,但积少成多,优化了用户使用体验,有利于提高用户黏性,如: (1)启动页展示轮播图,让未使用过的新用户初步了解小程序的功能,只有第一次进入时会显示。 [图片] (2)不是第一次使用的用户,已经了解过小程序的基本功能,那么用户进来最想要看到的内容是什么——当然ddl列表,且是“进行中”状态(即还未完成的)的ddl列表。 就像微信,我们打开微信后最先想要看到的是什么?当然是微信的未读消息,而不是通讯录之类的页面。 (3)操作体验优化,更加易用:标签页除了点击顶部导航切换,也可以左右滑动进行切换,有利于方便用户单手操作。 [图片] (4)操作时,必要时给予提示,避免用户误操作,如误删除 [图片] [图片] (5)充分考虑多种可能的不同情况,如: ①用户第一次进入小程序时,进入启动页,在启动页进行授权,授权后才可以使用小程序;但如果用户是通过他人分享ddl页面第一次进入小程序,则还需要进行授权才能使用。 [图片] [图片] ②前面提到,群组ddl功能的设计,是针对小组,而不是为了打造社区,因此群组上限为20人,必要时也应给予提示。这样的设计是遵循了产品的定位,与产品定位对应——即做效率类工具,而不是做社交平台。 [图片] (6)尊重用户隐私与体验,如: ①用户只是想与他人分享自己正在忙的ddl,但没有打算让他人添加为自己的ddl——分享时可进行选择。 [图片] ②为避免消息过多给用户带来骚扰,消息列表最多只显示最近20条消息。 [图片] (7)在线客服、意见反馈等不可缺少,这是作为用户与我们沟通的桥梁之一,也有利于不断改进产品。 [图片] [图片] 七、创新运营——产品已从0到1,还要从1到100由于本文主要是在讲产品策划,因此开发方面不多讲。但产品运营与产品策划密不可分,产品策划强调从0到1,产品运营强调从1到100,在产品被设计开发出来之后,还要结合运营推广,让它真正投入使用,才能体现它的价值。 这里简单提几点想法: (1)既然产品是微信小程序,那么最基本的就是结合微信进行推广,除了普通的微信群、朋友圈、公众号,还可以结合微信新功能“时刻视频”、“看一看”等。在微信环境下的推广,有利于用户快速定位到产品,减少引流成本; (2)针对目标用户:由于目标用户是高校学生,可以借助各高校相关微信公众号、微博、贴吧等进行线上宣传; (3)培养目标KOL(关键意见领袖),KOL在产品上线后发挥小范围同化辐射作用,提高其他用户对产品的信任度,也是作为用户自传播的方式之一; (4)组建线上ddl交流群或活动,在收集用户反馈的同时提高产品存在感,同时也可作为MVP产品,更好地了解用户的需求与实际情况; (5)结合当下深受年轻群体(包括目标用户即大学生在内)喜爱的短视频,可制作相关宣传短视频(要追求创意),借助腾讯微视、快手、抖音等短视频平台进行宣传; (6)结合小程序项目已有“冲鸭”系列图片设计,进一步拓展,制作微信表情包“冲鸭”系列,可申请在微信表情包商店上架,在微信聊天中进行使用,提高产品曝光、辅助产品推广。 [图片] 如果能在作品提交前上线运营,有运营数据支撑(如用户量、用户使用情况等),就说明产品的确有人用,说明产品开发的合理性、必要性。 八、小结其实我和其他的队友一样,我们队里的同学没有软件专业的,也没有计算机专业的,甚至有中文之类的文科专业同学。我们都是凭借兴趣自学了相关的技能,这也是驱动我们认真投入的重要原因之一——是为了兴趣,为了挑战,不是为了参赛,不是为了拿奖。论开发技术,我们也许并不如很多计算机专业的,但在我眼里,产品思维比技术重要,这也是我今天写文章,是以产品的角度来写、而不是以开发的角度来写的原因(当然,写得也比较简单,还望见谅)。以下是我的几点简单的参赛感想: 1、首先应该确定的问题,是产品定位、目标用户等,这些作为整个项目的基础以及方向,至关重要;在构思时应该多方面考虑(功能设计、目标用户、开发成本、运营方案等),在满足基本需求的前提下,力求创新,才能比别人做得更好(一味地实现必备需求而不追求创新,则很难突出产品亮点); 2、开发固然占了很重要的一部分,但一个完整的项目,不是只有开发,设计、运营等都很重要,这些是共同推进一个产品成功的必不可少的因素; 3、大赛提到了“以赛促学”,个人认为对于初学者来说,不要急于使用高大上的技术以便快速做出功能很高大上的产品。技术基础要扎实,才有利于长远的发展。所以我们用的是原生开发技术,不使用任何第三方开发框架(这里说的是开发框架,但我们有一定程度地使用了UI组件库)。在WXML、WXSS、js等原生技术基础上,力求创新实践。 【写在最后】“ddl冲鸭”产品初步成形,虽已上线运营投入使用,但依然有很多不足之处,后面会尽力再继续完善。这一次做项目,同时也是为了实现个人理想。我本人是打算往互联网产品策划方向发展的,这次项目也是相当于一个伟大的尝试。 在学习、借鉴一些去年参赛作品的过程中,我发现有一些作品现在已经停止了更新维护,甚至还有作品暂停服务、无法访问、或者至今没有正式发布上线等,这也让我再次思考了大赛和作品的意义。 单纯抱着“想试试、想参与”的态度,真的容易做出一款很好的产品吗?我想起以前面试社团,其实那个部门没有很想去,尽管一开始很顺利,面到最后一轮的时候还是挂了。当时我的老师对我说:“其实你的内心根本就没有那种特别想去的决心,那么这种情况下你又怎么可能发挥出你最大的潜力呢?” 4月底,面了腾讯暑期实习产品策划岗,面了三轮后挂在了总监面,无缘HR面。但是也并不遗憾,毕竟当时面试好像是随机分配?所以我面的部门和产品,并不是我想要做的,我不怎么接触、兴趣也很一般,确实没有“非去不可”的心态。然而塞翁失马,焉知非福?印象深刻,当时面试官问我:“你为什么想要选择做腾讯的产品策划?”现在我想说:“我和腾讯一样,有着一颗想要创造出能够改变世界的产品的心。”鹅厂是我美丽羞涩的梦,祝鹅厂越来越好,也希望我和鹅厂有缘再次相遇。 文粗词浅,感谢阅读。如有意见,恳请指教,感激不尽。
2019-06-28 - 小程序富文本能力的深入研究与应用
前言 在开发小程序的过程中,很多时候会需要使用富文本内容,然而现有的方案都有着或多或少的缺陷,如何更好的显示富文本将是一个值得继续探索的问题。 [图片] 现有方案 WxParse [代码]WxParse[代码] 作为一个应用最为应用最广泛的富文本插件,在很多时候是大家的首选,但其也明显的存在许多问题。 格式不正确时标签会被原样显示 很多人可能都见到过这种情况,当标签里的内容出现格式上的错误(如冒号不匹配等),在[代码]WxParse[代码]中都会被认为是文本内容而原样输出,例如:[代码]<span style="font-family:"宋体"">Hello World!</span> [代码] 这是由于[代码]WxParse[代码]的解析脚本中,是通过正则匹配的方式进行解析,一旦格式不正确,就将导致无法匹配而被直接认为是文本[代码]//WxParse的匹配模式 var startTag = /^<([-A-Za-z0-9_]+)((?:\s+[a-zA-Z_:][-a-zA-Z0-9_:.]*(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/, endTag = /^<\/([-A-Za-z0-9_]+)[^>]*>/, attr = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g; [代码] 然而,[代码]html[代码] 对于格式的要求并不严格,一些诸如冒号不匹配之类的问题是可以被浏览器接受的,因此需要在解析脚本的层面上提高容错性。 超过限定层数时无法显示 这也是一个让许多人十分苦恼的问题,[代码]WxParse[代码] 通过 [代码]template[代码] 迭代的方式进行显示,当节点的层数大于设定的 [代码]template[代码] 数时就会无法显示,自行增加过多的层数又会大大增加空间大小,因此对于 [代码]wxml[代码] 的渲染方式也需要改进。 对于表格、列表等复杂内容支持性差 [代码]WxParse[代码] 对于 [代码]table[代码]、[代码]ol[代码]、[代码]ul[代码] 等支持性较差,类似于表格单元格合并,有序列表,多层列表等都无法渲染 rich-text [代码]rich-text[代码] 组件作为官方的富文本组件,也是很多人选择的方案,但也存在着一些不足之处 一些常用标签不支持 [代码]rich-text[代码] 支持的标签较少,一些常用的标签(比如 [代码]section[代码])等都不支持,导致其很难直接用于显示富文本内容 ps:最新的 2.7.1 基础库已经增加支持了许多语义化标签,但还是要考虑低版本兼容问题 不能实现图片和链接的点击 [代码]rich-text[代码] 组件中会屏蔽所有结点事件,这导致无法实现图片点击预览,链接点击效果等操作,较影响体验 不支持音视频 音频和视频作为富文本的重要内容,在 [代码]rich-text[代码] 中却不被支持,这也严重影响了使用体验 共同问题 不支持解析 [代码]style[代码] 标签 现有的方案中都不支持对 [代码]style[代码] 标签中的内容进行解析和匹配,这将导致一些标签样式的不正确 [图片] 方案构建 因此要解决上述问题,就得构建一个新的方案来实现 渲染方式 对于该节点下没有图片、视频、链接等的,直接使用 [代码]rich-text[代码] 显示(可以减少标签数,提高渲染效果),否则则继续进行深入迭代,例如: [图片] 对于迭代的方式,有以下两种方案: 方案一 像 [代码]WxParse[代码] 那样通过 [代码]template[代码] 进行迭代,对于小于 20 层的内容,通过 [代码]template[代码] 迭代的方式进行显示,超过 20 层时,用 [代码]rich-text[代码] 组件兜底,避免无法显示,这也是一开始采用的方案[代码]<!--超过20层直接使用rich-text--> <template name='rich-text-floor20'> <block wx:for='{{nodes}}' wx:key> <rich-text nodes="{{item}}" /> </block> </template> [代码] 方案二 添加一个辅助组件 [代码]trees[代码],通过组件递归的方式显示,该方式实现了没有层数的限制,且避免了多个重复性的 [代码]template[代码] 占用空间,也是最终采取的方案[代码]<!--继续递归--> <trees wx:else id="node" class="{{item.name}}" style="{{item.attrs.style}}" nodes="{{item.children}}" controls="{{controls}}" /> [代码] 解析脚本 从 [代码]htmlparser2[代码] 包进行改写,其通过状态机的方式取代了正则匹配,有效的解决了容错性问题,且大大提升了解析效率 [代码]//不同状态各通过一个函数进行判断和状态跳转 for (; this._index < this._buffer.length; this._index++) this[this._state](this._buffer[this._index]); [代码] 兼容 [代码]rich-text[代码] 为了解析结果能同时在 [代码]rich-text[代码] 组件上显示,需要对一些 [代码]rich-text[代码]不支持的组件进行转换[代码]//以u标签为例 case 'u': name = 'span'; attrs.style = 'text-decoration:underline;' + attrs.style; break; [代码] 适配渲染需要 在渲染过程中,需要对节点下含有图片、视频、链接等不能由 [代码]rich-text[代码]直接显示的节点继续迭代,否则直接使用 [代码]rich-text[代码] 组件显示;因此需要在解析过程中进行标记,遇到 [代码]img[代码]、[代码]video[代码]、[代码]a[代码] 等标签时,对其所有上级节点设置一个 [代码]continue[代码] 属性用于区分[代码]case 'a': attrs.style = 'color:#366092;display:inline;word-break:break-all;overflow:auto;' + attrs.style; element.continue = true; //冒泡:对上级节点设置continue属性 this._bubbling(); break; [代码] 处理style标签 解析方式 方案一 正则匹配[代码]var classes = style.match(/[^\{\}]+?\{([^\{\}]*?({[\s\S]*?})*)*?\}/g); [代码] 缺陷: 当 [代码]style[代码] 字符串较长时,可能出现栈溢出的问题 对于一些复杂的情况,可能出现匹配失败的问题 方案二 状态机的方式,类似于 [代码]html[代码] 字符串的处理方式,对于 [代码]css[代码] 的规则进行了调整和适配,也是目前采取的方案 匹配方式 方案一 将 [代码]style[代码] 标签解析为一个形如 [代码]{key:content}[代码] 的结构体,对于每个标签,通过访问结构体的相应属性即可知晓是否匹配成功[代码]if (this._style[name]) attrs.style += (';' + this._style[name]); if (this._style['.' + attrs.class]) attrs.style += (';' + this._style['.' + attrs.class]); if (this._style['#' + attrs.id]) attrs.style += (';' + this._style['#' + attrs.id]); [代码] 优点:匹配效率高,适合前端对于时间和空间的要求 缺点:对于多层选择器等复杂情况无法处理 因此在前端组件包中采取的是这种方式进行匹配 方案二 将 [代码]style[代码] 标签解析为一个数组,每个元素是形如 [代码]{key,list,content,index}[代码] 的结构体,主要用于多层选择器的匹配,内置了一个数组 [代码]list[代码] 存储各个层级的选择器,[代码]index[代码] 用于记录当前的层数,匹配成功时,[代码]index++[代码],匹配成功的标签出栈时,[代码]index--[代码];通过这样的方式可以匹配多层选择器等更加复杂的情况,但匹配过程比起方案一要复杂的多。 [图片] 遇到的问题 [代码]rich-text[代码] 组件整体的显示问题 在显示过程中,需要把 [代码]rich-text[代码] 作为整体的一部分,在一些情况下会出现问题,例如: [代码]Hello <rich-text nodes="<div style='display:inline-block'>World!</div>"/> [代码] 在这种情况下,虽然对 [代码]rich-text[代码] 中的顶层 [代码]div[代码] 设置了 [代码]display:inline-block[代码],但没有对 [代码]rich-text[代码] 本身进行设置的情况下,无法实现行内元素的效果,类似的还有 [代码]float[代码]、[代码]width[代码](设置为百分比时)等情况 解决方案 方案一 用一个 [代码]view[代码] 包裹在 [代码]rich-text[代码] 外面,替代最外层的标签[代码]<view style="{{item.attrs.style}}"> <rich-text nodes="{{item.children}}" /> </view> [代码] 缺陷:当该标签为 [代码]table[代码]、[代码]ol[代码] 等功能性标签时,会导致错误 方案二 对 [代码]rich-text[代码] 组件使用最外层标签的样式[代码]<rich-text nodes="{{item}}" style="{{item.attrs.style}}" /> [代码] 缺陷:当该标签的 [代码]style[代码] 中含有 [代码]margin[代码]、[代码]padding[代码] 等内容时会被缩进两次 方案三 通过 [代码]wxs[代码] 脚本将顶层标签的 [代码]display[代码]、[代码]float[代码]、[代码]width[代码] 等样式提取出来放在 [代码]rich-text[代码] 组件的 [代码]style[代码] 中,最终解决了这个问题[代码]var res = ""; var reg = getRegExp("float\s*:\s*[^;]*", "i"); if (reg.test(style)) res += reg.exec(style)[0]; reg = getRegExp("display\s*:\s*([^;]*)", "i"); if (reg.test(style)) { var info = reg.exec(style); res += (';' + info[0]); display = info[1]; } else res += (';display:' + display); reg = getRegExp("[^;\s]*width\s*:\s*[^;]*", "ig"); var width = reg.exec(style); while (width) { res += (';' + width[0]); width = reg.exec(style); } return res; [代码] 图片显示的问题 在 [代码]html[代码] 中,若 [代码]img[代码] 标签没有设置宽高,则会按照原大小显示;设置了宽或高,则按比例进行缩放;同时设置了宽高,则按设置的宽高进行显示;在小程序中,若通过 [代码]image[代码] 组件模拟,需要通过 [代码]bindload[代码] 来获取图片宽高,再进行 [代码]setData[代码],当图片数量较大时,会大大降低性能;另外,许多图片的宽度会超出屏幕宽度,需要加以限制 解决方案 用 [代码]rich-text[代码] 中的 [代码]img[代码] 替代 [代码]image[代码] 组件,实现更加贴近 [代码]html[代码] 的方式 ;对 [代码]img[代码] 组件设置默认的效果 [代码]max-width:100%;[代码] 视频显示的问题 当一个页面出现过多的视频时,同时进行加载可能导致页面卡死 解决方案 在解析过程中进行计数,若视频数量超过3个,则用一个 [代码]wxss[代码] 绘制的图片替代 [代码]video[代码] 组件,当受到点击时,再切换到 [代码]video[代码] 组件并设置 [代码]autoplay[代码] 以模拟正常效果,实现了一个类似懒加载的功能 [代码]<!--视频--> <view wx:if="{{item.attrs.id>'media3'&&!controls[item.attrs.id].play}}" class="video" data-id="{{item.attrs.id}}" bindtap="_loadVideo"> <view class="triangle_border_right"></view> </view> <video wx:else src='{{controls[item.attrs.id]?item.attrs.source[controls[item.attrs.id].index]:item.attrs.src}}' id="{{item.attrs.id}}" autoplay="{{item.attrs.autoplay||controls[item.attrs.id].play}}" /> [代码] 文本复制的问题 小程序中只有 [代码]text[代码] 组件可以通过设置 [代码]selectable[代码] 属性来实现长按复制,在富文本组件中实现这一功能就存在困难 解决方案 在顶层标签上加上 [代码]user-select:text;-webkit-user-select[代码] [图片] 实现更加丰富的功能 在此基础上,还可以实现更多有用的功能 自动设置页面标题 在浏览器中,会将 [代码]title[代码] 标签中的内容设置到页面标题上,在小程序中也同样可以实现这样的功能[代码]if (res.title) { wx.setNavigationBarTitle({ title: res.title }) } [代码] 多资源加载 由于平台差异,一些媒体文件在不同平台可能兼容性有差异,在浏览器中,可以通过 [代码]source[代码] 标签设置多个源,当一个源加载失败时,用下一个源进行加载和播放,在本插件中同样可以实现这样的功能[代码]errorEvent(e) { //尝试加载其他源 if (!this.data.controls[e.currentTarget.dataset.id] && e.currentTarget.dataset.source.length > 1) { this.data.controls[e.currentTarget.dataset.id] = { play: false, index: 1 } } else if (this.data.controls[e.currentTarget.dataset.id] && e.currentTarget.dataset.source.length > (this.data.controls[e.currentTarget.dataset.id].index + 1)) { this.data.controls[e.currentTarget.dataset.id].index++; } this.setData({ controls: this.data.controls }) this.triggerEvent('error', { target: e.currentTarget, message: e.detail.errMsg }, { bubbles: true, composed: true }); }, [代码] 添加加载提示 可以在组件的插槽中放入加载提示信息或动画,在加载完成后会将 [代码]slot[代码] 的内容渐隐,将富文本内容渐显,提高用户体验,避免在加载过程中一片空白。 最终效果 经过一个多月的改进,目前实现了这个功能丰富,无层数限制,渲染效果好,轻量化(30.0KB),效率高,前后端通用的富文本插件,体验小程序的用户数已经突破1k啦,欢迎使用和体验 [图片] github 地址 npm 地址 总结 以上就是我在开发这样一个富文本插件的过程大致介绍,希望对大家有所帮助;本人在校学生,水平所限,不足之处欢迎提意见啦! [图片]
2020-12-27