- 用小程序·云开发打造功能全面的博客小程序丨实战
用小程序·云开发将博客小程序常用功能“一网打尽” 本文介绍mini博客小程序的详情页的功能按钮如何实现,具体包括评论、点赞、收藏和海报功能,这里记录下整个实现过程和实际编码中的一些坑。 评论、点赞、收藏功能 实现思路 实现文章的一些操作功能,最主要的还是评论,这是作者和读者之间沟通的桥梁,评论功能的衍生无非是细化作者和读者之间的互动,或者增加文章的传播,所以在动手开发时需要思考下你期望实现哪些功能,并对应功能进行细化。 我一般的经验是,先在脑子里过一遍需要的功能和大致流程,然后在笔记稍微画下「最最基础的原型,相当于产品的角色」。 然后就开始直接开始搭建页面和简单的交互「使用假数据,优先完成页面」,在构造页面的时候其实也能够补充最初想法上一些流程上的缺陷,这样在设计后端和数据库结构的时候可以补上,整体下来也基本比较完善了。 回头看我的小程序的需求,首先肯定是操作,在文章底部需要有个操作栏,用于发送点评和其他一些操作,在参考了一些同类型的小程序之后,逐步实现自己的一套风格,样式截图如下: [图片] 在有了功能之后,点评的数据需要有地方展示「通常是文章底部」,然后就有了文章底部的评论列表,样式如下: [图片] 既然有[代码]点赞[代码]和[代码]收藏[代码]的功能按钮,是否用户需要看下我点赞和收藏的文章列表呢,所以在「我的」中就有相应的列表,样式如下: [图片] 到这里,最最基础的功能基本差不多,接下来就要看后端是否能支持这些页面了「主要就是数据的保存和展示了」 对于评论来说,肯定需要一个集合用于保存用户的评论,而对于用户的喜欢和收藏也需要一个集合来进行保存。 所以根据页面我们就可以设计[代码]mini_comments[代码]和[代码]mini_posts_related[代码]两个集合。前者用于保存评论数据,后者用户保存用户操作与文章之间的关联。 剩下的工作就是变现了,无非就是页面交互和数据的增删改查了。 细节点解析 关于评论数量 目前在文章的集合中有个[代码]totalComments[代码]这个属性,当这篇文章每新增一个评论时,需要加1。 最初在写这个的时候,每次都是先查再更新,两段式,原代码如下: [代码] let count=post.totalComments+1; let result =await db.collection('mini_posts').doc(event.commentContent.postId).update({ data: { totalComments: count } }); [代码] 后来看文档发现,可以使用[代码]db.command.inc[代码]这个指令,无需再查一遍,直接可对原字段加1,还能保证原子性。代码如下: [代码] const _ = db.command let result = db.collection('mini_posts').doc(event.commentContent.postId).update({ data: { totalComments: _.inc(1) } }); [代码] 关于新增子评论 需要实现在某个评论下进行回复。 在交互上,点击评论者的昵称或头像时,触发相应的点击事件,在事件中去记录相应的评论ID及必要数据,同时去设置焦点到评论框内: [代码] /** * 点击评论内容回复 */ focusComment: function (e) { let that = this; let name = e.currentTarget.dataset.name; let commentId = e.currentTarget.dataset.id; let openId = e.currentTarget.dataset.openid; that.setData({ commentId: commentId, placeholder: "回复" + name + ":", focus: true, toName: name, toOpenId: openId }); }, [代码] 利用云开发新增子评论时可以使用[代码]db.command.push[代码]来进行操作「更新指令,对一个值为数组的字段,往数组尾部添加一个或多个值」,往子评论集合中新增: [代码] /** * 新增子评论 * @param {} event */ async function addPostChildComment(event) { let task = db.collection('mini_posts').doc(event.postId).update({ data: { totalComments: _.inc(1) } }); await db.collection('mini_comments').doc(event.id).update({ data: { childComment: _.push(event.comments) } }) await task; } [代码] 关于判断是否已收藏 在文章第一次加载时,我们需要判断下该用户是否有对该文章有相关操作,如果有相应的收藏和点赞操作,在初始化时需要更新相应的功能图标,核心代码如下: [代码] /** * 获取收藏和喜欢的状态 */ getPostRelated: async function (blogId) { let where = { postId: blogId, openId: app.globalData.openid } let postRelated = await api.getPostRelated(where, 1); let that = this; for (var item of postRelated.data) { if (config.postRelatedType.COLLECTION === item.type) { that.setData({ collection: { status: true, text: "已收藏", icon: "favorfill" } }) continue; } if (config.postRelatedType.ZAN === item.type) { that.setData({ zan: { status: true, text: "已赞", icon: "appreciatefill" } }) continue; } } }, [代码] 至于其他一些交互细节和代码细节,可以自行阅读源码去体会,如果有任何疑问或者有更好的实现方式,也可以与我沟通。 海报功能 交代些背景 其实在最早之前的小程序中已经实现了一次,具体可以参考利用云开发优化博客小程序(三)——生成海报功能,主要还是使用原生的[代码]cavans[代码]进行组装,原本想代码copy过来改改就行了,但总觉得原来的代码写的不是特别好。 于是想看看是否有现成的轮子可以利用,果然发现了[代码]wxa-plugin-canvas[代码]这款组件,通过非常简单的配置就可以生成精美的海报。 小程序使用npm 在总结生成海报功能之前还是有必要记录下小程序npm的使用,避免一些不必要的坑。 考虑到小程序本身的大小限制,使用npm的方式是最佳的。 原因是根据官方文档介绍,小程序 npm 包里只有构建文件生成目录会被算入小程序包的占用空间,上传小程序代码时也只会上传该目录的代码。这样大大减少了上传的代码体积。 下面简单介绍下小程序端如何使用npm的「其实根据官方文档按照步骤就可以了」。 以我目前小程序的路径为例,在[代码]/miniprogram[代码]新增文件夹[代码]node_modules[代码],在命令行指向到[代码]/miniprogram[代码]目录下: [图片] 通过命令进行安装: [代码] npm install wxa-plugin-canvas --production [代码] 安装成功后,即可在小程序开发工具中进行构建,构建前需要勾选[代码]使用 npm 模块[代码] [图片] 然后点击开发者工具中的菜单栏:工具 --> 构建 npm即可: [图片] 构建完成后会生成miniprogram_npm目录,到这里,项目端基本就调通了。 [图片] wxa-plugin-canvas 在构建完之后,就可以正常使用wxa-plugin-canvas这个自定义组件,使用方式还是比较简单的。 首先在你需要的页面引入该组件: [代码] { "usingComponents": {"poster": "wxa-plugin-canvas/poster"} } [代码] 然后就可以在[代码]wsml[代码]中使用了: [代码] <poster id="poster" hide-loading="{{false}}" preload="{{false}}" config="{{posterConfig}}" bind:success="onPosterSuccess" bind:fail="onPosterFail"></poster> [代码] 由于我们在生成海报前,需要异步获取一些用于海报的数据,所以我们采用异步生成的海报方式。 需要引入该组件的[代码]poster/poster.js[代码]文件,然后在代码中调用即可: [代码] import Poster from '../../utils/poster'; Page({ /** * 异步生成海报 */ onCreatePoster() { // setData配置数据 this.setData({ posterConfig: {...} }, () => { Poster.create(); }); } }) [代码] 核心代码解析 海报需要的数据 先来看看分享海报的整体结构: [图片] 首先需要确认海报的构成需要哪些数据,在调用组件前先获取好相应的数据。 在我设计的海报中主要包含三块内容,用户的信息(头像和昵称),文章信息(首图,标题,简介)和最重要的文章的小程序码。 用户信息和文章信息其实比较简单,在小程序的详情页两者数据都有,但这里有两个问题点需要注意下。 第一个是域名问题,在画布中使用到的图片都需要配置域名,头像的域名和公众号文章首图的域名 [代码] https://mmbiz.qpic.cn https://wx.qlogo.cn [代码] [图片] 第二个是公众号首图的问题,公众号素材列表返回的图片url其实是[代码]http[代码]的,但小程序规定绑定的域名必须是[代码]https[代码]的,当时比较无奈,后来尝试改用https访问首图的url也可以,不幸中的万幸,所以在使用首图地址时进行替换下: [代码] imageUrl = imageUrl.replace('http://', 'https://') [代码] 最后就是文章的小程序码了,需要利用小程序的[代码]getUnlimited[代码]的api,具体可以参考官方文档,目前已经提供了云调用的方式「无需获取access_token」,调用起来还是比较简单的。 原本打算在文章同步的时候「adminService」直接生成对应文章的小程序码,代码写完后本地调试可以,但上传至云端后测试发现一直报错,逛了轮胎才知道原来不支持,同时触发器也不支持云调用,所以这个计划泡汤了,我在代码中打了TODO。 [图片] 既然这样,那就在生成海报的时候进行生成,同时生成后直接上传至云存储,将对应的FileID保存至文章集合中,这样只用生成一次就可以一直使用了,具体代码如下: [代码] /** * 新增文章二维码 * @param {} event */ async function addPostQrCode(event) { let scene = 'timestamp=' + event.timestamp; let result = await cloud.openapi.wxacode.getUnlimited({ scene: scene, page: 'pages/detail/detail' }) if (result.errCode === 0) { let upload = await cloud.uploadFile({ cloudPath: event.postId + '.png', fileContent: result.buffer, }) await db.collection("mini_posts").doc(event.postId).update({ data: { qrCode: upload.fileID } }); let fileList = [upload.fileID] let resultUrl = await cloud.getTempFileURL({ fileList, }) return resultUrl.fileList } return [] } [代码] 但这里有个尴尬的地方是,生成小程序码的api中的[代码]scene[代码]参数最大长度是32,而文章id的长度已经是32了,无法根据文章id进行拼接跳转页面的路径了,所以这里暂时用了[代码]mini_posts[代码]集合中timestamp字段「理论上也是唯一的」。 所以在详情页中也需要兼容timestamp这个字段。 海报图片展示 海报图片展示就比较简单了,使用个弹窗,将生成好的海报图片进行展示即可: [代码] /** * 生成海报成功-回调 * @param {} e */ onPosterSuccess(e) { const { detail } = e; this.setData({ posterImageUrl: detail, isShowPosterModal: true }) console.info(detail) }, [代码] 保存海报图片 保存图片使用wx.saveImageToPhotosAlbum调用用户相册,这里主要需要兼容用户拒绝相册授权的一些列操作,具体代码如下: [代码] /** * 保存海报图片 */ savePosterImage: function () { let that = this wx.saveImageToPhotosAlbum({ filePath: that.data.posterImageUrl, success(result) { console.log(result) wx.showModal({ title: '提示', content: '二维码海报已存入手机相册,赶快分享到朋友圈吧', showCancel: false, success: function (res) { that.setData({ isShowPosterModal: false, isShow: false }) } }) }, fail: function (err) { console.log(err); if (err.errMsg === "saveImageToPhotosAlbum:fail auth deny") { console.log("再次发起授权"); wx.showModal({ title: '用户未授权', content: '如需保存海报图片到相册,需获取授权.是否在授权管理中选中“保存到相册”?', showCancel: true, success: function (res) { if (res.confirm) { console.log('用户点击确定') wx.openSetting({ success: function success(res) { console.log('打开设置', res.authSetting); wx.openSetting({ success(settingdata) { console.log(settingdata) if (settingdata.authSetting['scope.writePhotosAlbum']) { console.log('获取保存到相册权限成功'); } else { console.log('获取保存到相册权限失败'); } } }) } }); } } }) } } }); }, [代码] 体验总结 有好的开源组件可以充分利用,避免重复造轮子,有机会也可以学习下别人的实现方式。 多看看文档,其实小程序的文档真的挺详细的。 这里主要想分享实现一个功能实现的过程,有想法的时候如何一步步去成功实现。 小程序本身不难,相应的文档也很详细,但是组装的过程和逻辑的实现需要自身去思考和体会。多看看文档,其实小程序的文档真的挺详细的。 如果你的想法和流程都非常清晰,但还是没办法实现你的预期功能,那我建议你先放放,先把[代码]html[代码],[代码]css[代码],[代码]javascript[代码]熟悉下,再看几遍小程序的文档,也许你当时面临的问题就不再是问题了。 源码链接 https://github.com/TencentCloudBase/Good-practice-tutorial-recommended 如果你有关于使用云开发CloudBase相关的技术故事/技术实战经验想要跟大家分享,欢迎留言联系我们哦~比心! [图片]
2019-08-26 - mac os 开发者工具命令行调用提示 command not found ?
[图片]
2019-08-12 - 如何用小程序实现类原生APP下一条无限刷体验
1.背景 如今信息流业务是各大互联网公司争先抢占的一个大面包,为了提高用户的后续消费,产品想出了各种各样的方法,例如在微视中,用户可以无限上拉出下一条视频;在知乎中,也可以无限上拉出下一条回答。这样的操作方式用户体验更好,后续消费也更多。最近几年的时间,微信小程序已经从一颗小小的萌芽成长为参天大树,形成了较大规模的生态,小程序也拥有了一个很大的流量入口。 2.demo体验 那如何才能在小程序中实现类原生APP效果的下一条无限刷体验? 这篇文章详细记录了下一条无限刷效果的实现原理,以及细节和体验优化,并将相关代码抽象成一个微信小程序代码片段,有需要的同学可查看demo源码。 线上效果请用微信扫码体验: [图片] 小程序demo体验请点击:https://developers.weixin.qq.com/s/vIfPUomP7f9a 3.实现原理 出于性能和兼容性考虑,我们尽量采用小程序官方提供的原生组件来实现下一条无限刷效果。我们发现,可以将无限上拉下一篇的文章看作一个竖向滚动的轮播图,又由于每一篇文章的内容长度高于一屏幕高度,所以需要实现文章内部可滚动,以及文章之间可以上拉和下拉切换的功能。 在多次尝试后,我们最终采用了在[代码]<swiper>[代码]组件内部嵌套一个[代码]<scroll-view>[代码]组件的方式实现,利用[代码]<swiper>[代码]组件来实现文章之间上拉和下拉切换的功能,利用[代码]<scroll-view>[代码]来实现一篇文章内部可上下滚动的功能。 所以页面的dom结构如下所示: [代码]<swiper class='scroll-swiper' circular="{{false}}" vertical="{{true}}" bindchange="bindChange" skip-hidden-item-layout="{{true}}" duration="{{500}}" easing-function="easeInCubic" > <block wx:for="{{articleData}}"> <swiper-item> <scroll-view scroll-top="0" scroll-with-animation="{{false}}" scroll-y > content </scroll-view> </swiper-item> </block> </swiper> [代码] 4.性能优化 我们知道view部分是运行在webview上的,所以前端领域的大多数优化方式都有用。例如减少代码包体积,使用分包,渲染性能优化等。下面主要讲一下渲染性能优化。 4.1 dom优化 由于页面需要无限上拉刷新,所以要在[代码]<swiper>[代码]组件中不断的增加[代码]<swiper-item>[代码],这样必然会导致页面的dom节点成倍数的增加,最后非常卡顿。 为了优化页面的dom节点,我们利用[代码]<swiper>[代码]的[代码]current[代码]和[代码]<swiper-item>[代码]的[代码]index[代码]来做优化,控制是否渲染dom节点。首先,仅当[代码]index <= current + 1[代码]时渲染[代码]<swiper-item>[代码],也就是页面中最多预先加载出下一条,而不是将接口返回的所有后续数据都渲染出来;其次,对于用户已经消费过的之前的[代码]<swiper-item>[代码],不能直接销毁dom节点,否则会导致[代码]<swiper>[代码]的[代码]current[代码]值出现错乱,但是我们可以控制是否渲染[代码]<swiper-item>[代码]内部的子节点,我们设置了仅当[代码]current <= index + 1 && index -1 <= current[代码]时才会渲染[代码]<swiper-item>[代码]中的内容,也就是仅渲染当先文章,及上一篇和下一篇的文章内容,其他文章的dom节点都被销毁了。 这样,无论用户上拉刷新了多少次,页面中最多只会渲染3篇文章的内容,避免了因为上拉次数太多导致的页面卡顿。 4.2 分页时setData的优化 setData工作原理 [图片] 小程序的视图层目前使用[代码]WebView[代码]作为渲染载体,而逻辑层是由独立的 [代码]JavascriptCore[代码] 作为运行环境。在架构上,[代码]WebView[代码] 和 [代码]JavascriptCore[代码] 都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 [代码]evaluateJavascript[代码] 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 [代码]JS[代码] 脚本,再通过执行 [代码]JS[代码] 脚本的形式传递到两边独立环境。 而 [代码]evaluateJavascript[代码] 的执行会受很多方面的影响,数据到达视图层并不是实时的。 每次 [代码]setData[代码] 的调用都是一次进程间通信过程,通信开销与 setData 的数据量正相关。 [代码]setData[代码] 会引发视图层页面内容的更新,这一耗时操作一定时间中会阻塞用户交互。 [代码]setData[代码] 是小程序开发中使用最频繁的接口,也是最容易引发性能问题的接口。 避免不当使用setData [代码]data[代码] 应仅包括与页面渲染相关的数据,其他数据可绑定在this上。使用 [代码]data[代码] 在方法间共享数据,会增加 setData 传输的数据量,。 使用 [代码]setData[代码] 传输大量数据,通讯耗时与数据正相关,页面更新延迟可能造成页面更新开销增加。仅传输页面中发生变化的数据,使用 [代码]setData[代码] 的特殊 [代码]key[代码] 实现局部更新。 避免不必要的 [代码]setData[代码],避免短时间内频繁调用 [代码]setData[代码],对连续的setData调用进行合并。不然会导致操作卡顿,交互延迟,阻塞通信,页面渲染延迟。 避免在后台页面进行 [代码]setData[代码],这样会抢占前台页面的渲染资源。可将页面切入后台后的[代码]setData[代码]调用延迟到页面重新展示时执行。 优化示例 无限上拉刷新的数据会采用分页接口的形式,分多次请求回来。在使用分页接口拉取到下一刷的数据后,我们需要调用[代码]setData[代码]将数据写进[代码]data[代码]的[代码]articleData[代码]中,这个[代码]articleData[代码]是一个数组,里面存放着所有的文章数据,数据量十分庞大,如果直接[代码]setData[代码]会增加通讯耗时和页面更新开销,导致操作卡顿,交互延迟。 为了避免这个问题,我们将[代码]articleData[代码]改进为一个二维数组,每一次[代码]setData[代码]通过分页的 [代码]cachedCount[代码]标识来实现局部更新,具体代码如下: [代码]this.setData({ [`articleData[${cachedCount}]`]: [...data], cachedCount: cachedCount + 1, }) [代码] [代码]articleData[代码]的结构如下: [图片] 4.3 体验优化 解决了操作卡顿,交互延迟等问题,我们还需要对动画和交互的体验进行优化,以达到类原生APP效果的体验。 在文章间上拉切换时,我们使用了[代码]<swiper>[代码]组件自带的动画效果,并通过设置[代码]duration[代码]和[代码]easing-function[代码]来优化滚动细节和动画。 当用户阅读文章到底部时,会提示下一篇文章的标题等信息,而在页面上拉时,由于下一篇文章的内容已经加载出来了,这样在滑动过程中会出现两个重复的标题。为了避免这种情况出现,我们通过一个占满屏幕宽高的空白[代码]<view>[代码]来将下一篇文章的内容撑出屏幕,并在滚动结束时,通过[代码]hidden="{{index !== current && index !== current + 1}}"[代码]来隐藏这个空白[代码]<view>[代码],并对这个空白[代码]<view>[代码]的高度变化增加动画,来实现下一篇文章从屏幕底部滚动到屏幕顶部的效果: [代码].fake-scroll { height: 100%; width: 100%; transition: height 0.3s cubic-bezier(0.167,0.167,0.4,1); } [代码] [图片] 而当用户想要上拉查看之前阅读过的文章时,我们需要给用户一个“下滑查看上一条”提示,所以也可以采用同上的方式,通过一个占满屏幕宽高的提示语[代码]<view>[代码]来将上一篇文章的内容撑出屏幕,并在滚动结束时,通过[代码]wx:if="{{index + 1 === current}}"[代码]来隐藏这个提示语[代码]<view>[代码],并对这个提示语[代码]<view>[代码]的透明度变化增加动画,来实现下拉时提示“下滑查看上一条”的效果: [代码].fake-previous { height: 100%; width: 100%; opacity: 0; transition: opacity 1s ease-in; } .fake-previous.show-fake-previous { opacity: 1; } [代码] 至此,这个类原生APP效果的下一条无限刷体验的需求的所有要点和细节都已实现。 记录在此,欢迎交流和讨论。 小程序demo体验请点击:https://developers.weixin.qq.com/s/vIfPUomP7f9a
2019-06-25 - 我们是怎么进行前端工程化的
随着业务的不断发展,企鹅电竞的前端规模在不断的疯狂膨胀。越来越多的项目被建立起来,越来越多的体系被架构出来,越来越多的代码变的越来越难维护。我们在不断创造新代码的时候,不断在思考,为啥我们的项目看起来这么的野?我们不禁反复思考,我们干了那么久的前端工程师,我们他喵的到底在干个啥?很自然,有人就开始说,我们要推动前端工程化,那么我们在讨论前端工程化的时候,我们到底在讨论啥? 文章将阐述我们在前端工程化的一些思想和做的一些工具,文章并不主要介绍和推广我们的工具,我们更多的介绍的是我们业务发展过程中,遇到各种问题的时候,我们的思考和想法,以及我们采用的对策和措施。当然,如果大家有更好的想法,希望大家能贴在讨论区,我们一起讨论一波。 什么是前端工程化 我们在思考这个问题之前,我们先思考一个更大的问题,什么叫软件工程化?我依稀记得,在我还有头发的时候,在梧桐树旁的十五栋楼上,上课(睡觉)的日子里,那满脸褶皱的软件工程老师是这么说的: 将系统化的、严格约束的、可量化的方法应用于软件的开发、运行和维护,即将工程化应用于软件。 没记错的话是应该是清华大学出版社出版的《软件工程》这本书里的定义,回去如果我翻到了是哪本书,我再贴出来。不管,这里先套用一下,前端的工程化是不是应该是指: 将系统化的、严格约束的、可量化的方法应用于前端页面的开发、运行和维护,即将工程化应用于前端开发。 所以,当我们要讨论前端工程化的时候,是不是讨论的这么几个基本问题: 如何进行系统化的前端开发、运行和维护? 如何对前端的开发、运行和维护进行严格约束? 如何对前端的开发、运行和维护进行量化? 为了方便的进行行文,在此约定,下文的开发将指代开发、运行和维护。所以,在后面我们将就这三个问题,来谈谈我们对于前端工程化的思考和实践。 大部分情况下,我们在讨论前端工程化的时候,我们其实只讨论CI/CD和脚手架,但是软件开发并不是只有CI/CD,在不同的开发阶段,甚至在不同的项目阶段,面对不同的问题,我们都要采用不用的方案方法。里面有共性,也有个性。共性的问题,我们可以参考社区的解决方案,但是个性的问题,则需要我们自己去探索和思考。 为什么要进行前端工程化 ok,按照三段论的基本套路,我们应该要来讨论一下,为啥要进行前端的工程化? 如果你非要我吹逼的话,我会这样吹:世界的本质是熵增的,而生命的本质是熵减的。为了体现我们作为生命的伟大价值,我们将坚定不移的将负熵进行到各个角落。 说一个比较通俗的答案:当前的开发环境已经复杂到影响我们开心的进行下一步的开发,我们需要对当前的开发环境进行一次整理,而这个整理的过程,就是前端工程化的过程。 那我们现在的开发环境到底有多复杂? 截止到我写这篇文章的这一刻,我们一共要负责9大系统。各系统由于立项时间,定位,运行环境,产品要求等各种乱七八糟的原因,使用了不同的技术架构,不同的技术框架,不同的开发方式,不同的发布方式。更恐怖的是,由于技术的不断发展,框架的推陈出新,我们的项目还在不断的复杂中。。。 分别为:电竞官网(可以看直播的那种),电竞ipad版本(可以看直播的那种),电竞移动端(android 和 ios两端各70+场景页面,还有一堆的分享页面),电竞移动助手端(android和ios双端),电竞pc助手端(主播管理端),电竞运营管理端(内部数据管理,各种个性化需求和数据分析工具),电竞开放平台(提供给外部合作伙伴进行数据管理),公会管理系统(用于辅助公会进行主播管理),无以计数的活动页面。 所以,每次当有新同学加入我们的时候,我们都必须要提供多位资深员工,手把手的说明一下,我们项目到底要怎么跑起来,有的项目因为实在太复杂,已经快要跑不起来了:D 如何进行前端工程化 面对日益复杂的业务场景,日益庞复的代码逻辑我们开始思考我们到底要如何解决我们的前端工程化问题。根据前面我们的思考,我们将在以下三个方面来谈谈我们对于前端工程化的思考: 如何进行系统化的前端开发、运行和维护? 对于这个问题,我们首先思考的是什么是系统化?我们要怎么做才能称的上是系统化? 抛开那些高大上的术语,我们的解是,将日常开发的各个步骤整化为固定的流程,当我们在日常开发中,面对各种情况都会有固定的一二三四五的时候,就说明我们完成了系统化。 那么,我们每天都要做哪些工作呢? 收集一下大家的日常工作,都有这么一堆: [图片] 这些庞杂的工作有没有规律? 我们开始对我们的工作流程进行了一次收归。其实如果按照需求开发的基本流程,我们可以简单的把各种毛细的工作收归为以下四个阶段: [图片] 在不同的阶段,我们需要做不同的事情,而且,我们发现这些事情是重复的,或者说,是每次做需求的时候在相同的阶段都需要重复的。 在相同阶段里,做重复的事情,我们是不是能够自动化起来? 当然可以,我们开发一个流程管理工具是不是就可以了!在开发之前,我们看了一下公司内和业内在这个方向上的实践。这里列几个公司内宣传的比较好的方案:weflow、feflow、leah-cli 当然社区也有很多很不错的方案比如vue-cli,还有ng-cli等等,完成度相当高,非常的靠谱和好用。 但是,我们不难发现,这些cli也好,flow也好,都是基于工具的思路去实现的,和我们的思路并不一样。我们希望是能基于流程去实现的,而且能贯穿整个开发流程,形成一个闭环。让我们的同学能够系统化的去一二三就好了,不要思考太多有的没的。所以,我们基于上述的开源实现和思路,自己实现了一套新的方案:pgg-flow。 pgg-flow是以开发周期为核心,辅以各种工具库。举个栗子: 在需求的准备阶段,不管三七二十一,我们先来个pgg-flow start,然后开发分支就自动从主干拉出,并将本地环境切入到该分支。 在需求的开发阶段,来个pgg-flow dev,代码就跑起来了,dev-server,本地构建就跑起来了。 在需求的测试阶段,来个pgg-flow test,代码就提交远端,触发服务端ci/cd,测试环境立即生效就绪。 在需求的发布阶段,来个pgg-flow release,直接就触发ci/cd ,发起merge request(用于code review),生成ars单,触发发布系统流程,触发codecc的自动代码审查,并回收占用的各种资源。 我们希望开发同学,能够不用关心不同模块间的框架和依附系统差异。只要知道以上四个命令就能够完成所有的开发流程。 我们在CI/CD的思考和实践详情参考我们电竞小哥思名的这篇文章《weex/vue 应用的服务器端构建之路》,利用内部基于docker的orange-ci和发布系统,实现git工作流到外网发布的一条龙打通。 当然,在这个基础上,我们可以继续去支持,new page, new api等等更具体的业务辅助工具。通过如此,我们磨平了不同框架体系间的差异,大家可以很开心的进行需求开发了。但是,很快大家又有新问题来了,不同的框架中,不同的开发体系在相同的事情上,使用了不同的技术选型。比如,pc的同学自己基于fetch进行了封装,但是在h5上使用了axios来进行请求的。 这个主要是很多历史遗留,开发pc的和开发h5的在很长一段时间内都不是同一拨人,虽然在开发相同或者说相近的业务,但是,技术选型并不一致,这其实是之前遗留的一个很大的坑。 不管问题之前如何产生的,但是,就事论事,如何让大家的技术选型和技术实践相贴近? 如何让大家的技术选型和技术实践相贴近? 对于这个问题,我们的思考是这样的: 大家选型和实践有较大差异的原因是没有现成的方案,或者不知道有现成的方案。所以我们是从三个角度来解决这个问题的: 提供横跨框架场景的公共库,包含完整的基础方法 提供自动化更新的文档,帮助大家更好的了解基础方法有哪些 提供完善的脚手架,减小使用代价 公共库的管理是基于tnpm(公司内部的npm镜像)。 文档自动化,可以参考我们电竞一姐的这篇文章《企鹅电竞前端公共库文档自动化建设》。文章详细的介绍了,我们是如何使用蓝盾来进行文档自动化管理的。 脚手架的提供,为的是进一步减少重复工作量。 当我们完成以上的工作的时候,我们发现,我们已经基本实现了,系统化的进行前端的页面需求开发。当同学们开始一个需求的时候,可以很容易的借助既成的体系,进行快速的开发迭代。当然,现在这个工作模式还比较稚嫩,在很多细节上的地方还需要继续打磨。 为啥要采用相同或者相近的的技术实践?有个很大的原因是为了能够将优化的工作更容易的扩散出去。 比如,通过webpck4+babel7 的升级,一下就减少了移动端几乎所有页面的40%体积,具体可以参考思名的《weex/vue 应用的webpack4 升级优化之路》,《真·一行代码优化30%的JS bundle体积》。我们觉得优化应该倾向于底层,脱离于业务,只有如此,才能实现更高层级的复用。 如何对前端的开发、运行和维护进行严格约束? 在实现了系统化的进行开发和迭代之后,我们不得不开始面对开发质量的问题。如何对开发质量进行有效的约束和评估,这并不是一个简单的问题。我们最早的方案使用了业内比较通用的方案: 使用本地的eslint进行代码管控 通过eslint + husky + lint-staged来搭建一套基于本地的代码及风格管控,这个一个很棒的方式。业内对于这个方案也有很多很好的实践,大家自行百度一下就好。但是,这个方案对于我们而言,并不能解决所有的问题。 我们希望能对代码质量有一个整体的认识,我们希望能够对于代码质量的监控有一个比较好的客观衡量。 我们希望这个监控体系能够不阻塞大家正常的业务交付。 我们希望这个体系能够从各个维度对我们的代码进行合理的分析。 很明显,本地化的eslint根本不能满足我们对于代码质量监控的要求,就算,你在这个基础上增加prettier来做你的代码格式化的管理工具。于是我们开始寻找,是否有能够有一个平台来帮助我们完成这个工作。 如何优化的监控的前端代码质量 这个问题,我们最后使用了codecc+蓝盾的解决方案。将质量监控体系建设在远端,能够有效的对代码进行监控,同时,也能很容易的让更多的同学0成本的使用起来。有兴趣的话,大家可以参考我们电竞一姐的《如何优雅的监控前端代码质量》。 codecc是公司内部的在线代码自动审查平台,可以实现在线的代码eslint,coverity,重复性,复杂度等各种纬度的代码审查,能够将审查结果进行很好的分析和数据图形化。蓝盾是公司内部的流程集成系统,可以流水线化的实现代码的自动化构建和二三级子服务的连接。 通过蓝盾和codecc的体系,我们发现并解决了很多的问题,但是,很快,我们又发现了,单纯的依靠系统自动化的检查和检测,并不能解决所有的问题。有些设计思路上的问题,我们没办法简单的通过自动化的代码审查来解决。那么是否可以在必要的时候,引入人工的代码审查来进一步的提高代码质量。 在合适的时候进行code review 不能否认,cr是一个很重要,但是很烦人的工作。如何让大家对代码在合适的时候,进行cr,这个问题让我们思考了很久。我们尝试过很多的方案: 在发布流程中通过人工要求增加cr环境 以代码小课堂的形式,进行cr 在开发前后以技术评审的形式来进行cr 但是,这些方式有好有坏,但是,都不能够像水一样的融入到我们的开发生活中来。我们觉得不自然。我们最后的思考是: 将code review和git 工作流和ci相互融合,通过ci工具来自动触发。利用cli来封装各种常用操作,将原本复杂的命令操作,化繁为简。 简单的介绍一下我们的方案: [图片] 为了能够使这一套工作流变的更智能,我们利用node-git构建了一个轻便的git工具。具体的实现方式可以参考我们电竞小哥锦亮的《基于NodeGit的Git工作流封装》。 我们之前讨论了很久的代码质量的管控,但是除了代码质量,还有一个很大的部分是我们的行为。我们在开发过程中,有很多的行为会改变远端的代码状态和共用资源(测试服务器)的状态。我们怎么才能对这个状态进行一个很好的管理? 如何监控代码的过程? 在电竞小哥的《weex/vue 应用的服务器端构建之路》一文中," 通过工蜂的webhook监控提交" 这段内容详细的介绍了我们的实践。除了这部分介绍的实践,我们还搭建了一个node sever用来记录和查询,我们的测试服务器的使用情况。 简单的说,我们实现了各大系统的过程内容收归,各个不同平台的流程数据能够统一的以企业微信机器人的形式主动推送给你。足不出户,可知天下事。 通过完善的自动化代码监控 + 人工cr的双检查,我们简单的构建起了我们的代码质量监控体系。 对于前端而言,轻薄短小快是基本要求,但是做到轻薄短小快并不是一件简单的事情,更复杂的事情,我们有这么多的工作要做,居然还有一群人在后面用排期表抽我们的屁股,这个就让我们很难受了。 如何对前端的开发、运行和维护进行量化 对工作进行量化是一件很复杂,很难把控的事情。我们在这方面并没有很好的实践。对于如何正确的评估工作量, 我们有一个不怎么成熟的想法: 所有工作量应该包含以下几个部分: 技术方案预研所花费的时间 技术方案落地所耗费的时间 自测的时间 联调的时间 产品修改需求的时间 调试解决问题的时间 在具体的时间分配上,根据各个不同的人自我确定和分配。在具体的实践过程中,我们建议保留一定的buffer,你总会在你的代码上线前,发现这样或那样的问题的。 除了上面的建议,我们还有一些比较通用的建议: 比如实现从设计到结果交付的组件化管理,组件化一定要从设计语言开始,这样才能从根本上减少各个流程的耗时,和实现标准化。 技术预研要早于产品开发。预研一代,落地一代,维护一代。永远不要在业务实际开发中使用未经预研的框架和技术体系,会引入极大的项目风险。 使用graphql这种前端为核心的接口配置体系,能够极大的减少因为接口联调带来的不稳定耗时。 以上,简单的介绍了一下,我们现在的前端工程化的体系进程,和我们的一些思考。很多观点还比较的幼稚,很多实现还特别的落后,希望各位大佬,能够给我们提供更多好的思路和方式方法,大家一起讨论和思考如何更优雅的进行完善的前端工程化。 最后打一波广告: 企鹅电竞——腾讯游戏官方直播平台,欢迎UI开发和前端开发同学加入 https://hr.tencent.com/position_detail.php?id=48298
2019-03-11 - 日活低、留存低,我的小程序就毫无价值了吗?
作者:王隐 无论是PC还是移动互联网,人们总是更愿意相信数据的可靠性,我们时常听见“月活十万”、“总用户千万”这样的形容词也就不足为怪。 人们之所以如此迷恋数据,是因为它很好地过滤掉了评价过程中的诸多噪音与低效信息,并在评价体系中掌握着至高无上的公信力。 数据不说谎,但如果在你面前摆满各种表格,折线图与各种名词,这个时候,数据的降噪能力可能就大打折扣。 全篇重点等于没有重点。 微信小程序本身提供了各种各样的数据指标,累计访问人数、打开次数、访问次数、访问人数、新访问人数、总添加人数、新添加人数、分享次数、分享人数、人均停留时长、次均停留时长等都可以反映出小程序的某些特点。 作为一名小程序创业者,如何从大量数据中筛选出有价值的信息就显得尤为重要。 但是各种数据指标也带来了不同的焦虑。有人为用户增长而焦虑,有人为日活低而焦虑,也有人为留存率始终上不去而焦虑。 我的小程序日活低、留存低,就真的毫无价值了吗?太多开发者有类似的困惑,但太少人反思:这些由App衍生来的评价体系,对小程序真的公平吗?真的客观吗? 量子程序请到了业界多位小程序负责人,和他们聊了聊数据这件“小”事。 [图片] [图片] 小程序能约等于App吗? 在手机平台与App两层附庸之下,日活、月活、用户数这些App上的舶来品指标直接用来量化小程序,是否合理? 如果合理则沿用,不合理则思考哪些是合理的,这是我们数据降噪的第一步。 对于这个问题,开发者的态度见仁见智。 「加推」COO Jack:“合理,本质上小程序可以当成是App。” 数据咨询师张佳:“对比的意义不是很大,这俩(小程序、App)的流量不是一个东西,要是硬套也能套上用……” 「报告查一查」产品经理Max:“衡量App的数据指标有很多,转移到小程序上面,这些指标当然越高越好,一个日活、留存、用户总数都很高的小程序一定非常有价值。 但是小程序这种简单的载体,在微信生态里面往往处于调起的状态,总是会被微信的主要功能打断,所以很难在各个数据指标上向App看齐,找到适合小程序的使用场景,才能找到适合该小程序的数据反馈,像之前的‘高频、刚性’的衡量方式有可能在小程序上不合适。 比如说一个摇号的小程序,在用户完成行为之后,基本不会再用,但实际上满足了用户很大的需求。这个时候你要求他的留存率高,其实价值不大,你要求全国用户都用它,又有什么用呢。” [图片] 「黑猫看电影」CTO赵成雨:“(这些指标中)日活还是有参考价值,因为小程序虽然功能单一,获客成本低,但是用户一旦到了你的小程序上,至少他还是对你提供的服务感兴趣。” 小程序在沿用App的衡量指标时,仍要考虑自身的产品特性。只有这样,我们才能进行数据降噪的第二步:应当重点关注哪些指标? [图片] 你最看重什么指标? 每一个指标都是对产品某一维度的评估,不同的产品、不同的阶段,重视的指标都有不同。小程序开发者最看重什么指标呢? 「报告查一查」产品经理Max:“对于我们的小程序来讲,我们的用户并不会每天都研究报告,所以我们更关注他的月留存。 每份报告的质量不同,所以我们并不关注他们看了多少份,而是关注他们呆了多长时间(停留时长)。 用户一旦需要研报就想到我们,我们提供付费检索的功能,这样就实现了我们的商业获利。 所以找到用户的习惯,在用户可接触的时间范围内,满足他们的需求,顺便挣点钱,小程序是可以实现的。” 而在Max看来,工具与咨询类小程序的指标还可以被量化: 工具类小程序,月活、日活更重要些,25-30%月留存算不错;内容资讯类小程序,停留时间更重要,300-600秒算不错。 [图片] 数据咨询师张佳:“我看中变现能力。” 「知识圈」CEO大伟:“商业价值体现在成交上,没有成交体现的是产品价值,那就是日活和月活,最后还是要看交易。” 万国法考产品负责人杜佳丞:“第一变现指标就是流量,得流量者得天下。对于我们自己的小程序,我不指望他赚钱,并且我们又处在这么一个特殊的教育行业中,我最看重的指标是留存,第二指标流量,虽然流量很少。” 「群接龙」CEO吴彬:“活跃留存,可以体现粘性。” 酷客多CEO郝宪玮:“除去这几个指标,建议加上获客成本,用户使用路径长短,用户体验度,用户裂变速度,客单价,研发成本。” 留存是大家普遍提到的一个指标,这也体现了从业者对其关注度之高。 而在赵成雨看来,留存的参考价值貌似不大。 「黑猫看电影」CTO赵成雨:“除了交易额之外,就是日活。其他的像留存,说白了,看了也没用。” 那小程序要不要看留存? [图片] 留存:来的快,走得也快 小程序主打用完即走,这可能是和App最大的不同,毕竟下载一个App的成本确实要比小程序高很多。那小程序到底要不要和App一样看留存。如果不应该,原因是什么? 「黑猫看电影」CTO赵成雨:“留存这块,如果和App比,我感觉不能这么比。首先它(小程序)的用户粘性不是特别大,虽然获客成本低,但来的容易,走的也容易。而且小程序的入口要比App深。 而且App经过了搜索商店里搜索、下载、注册的过程,让用户打开App正常使用,其实已经过滤掉了很多人,所以说App的留存肯定要比小程序高很多。 但小程序又不一样,他可能不小心碰了下就进去了,他也不知道怎么进来,(离开后)他可能永远都不会再来。” 「知识圈」CEO大伟:“留存,可以理解为使用频次。如果多次使用,代表产品有价值,如果没有成交,还是没有价值。” 数据咨询师张佳:“现有的规律显示,要搞小程序留存是非常困难的,唯一的办法就是在从流量进来到流失的这个空档完成变现。这对于已经有盈利模式的公司来说是最大的优势,你要做的就是把你们的变现模式压缩、打碎,使它们能融合到小程序流量短暂的停滞时间内。” 看来,就算看重留存,这个指标也会经常让我们失望,因为“留存低”确实与小程序本身的产品特性息息相关。 [图片] 数据降噪终将回到变现上 我们经过以上两步数据降噪,目的就是为了快速挑选出有价值的数据指标,并借助它们衡量小程序的商业价值。 很多时候,这种商业价值直接体现在变现能力的强与弱,快与慢,明确与模糊之间。 「加推」COO Jack:“小程序是否高频使用,场景是否明确清晰,决定了小程序变现能力强弱。” 万国法考产品负责人杜佳丞:“总体来说,对于小程序,我们不能只将目光停留日活,留存,总用户上。 小程序上线以来,用户上千万的产品一只手数不过来吧?但是再回头看一下,又有多少小程序现在依然如日中天呢? 老话说,赚钱容易,守财难。那么用这句话来解释,在相同的资源、背景下,App守住财富的难度要低于小程序,更直接的理解:小程序成功=暴发户,App=创业成功者。 如果从暴发户的角度去考虑日活,留存、总用户,我相信我只拿一个指标来衡量,我赚了多少现金(流量广告变现?)。当我们我去考虑小程序的日活,留存,总用户的数据如何好看时,我不能说这是错的,这更像一个暴发户想要脱离这个身份,成为一位企业家,这是一种自我的蜕变,理念是对的,但是过程很痛苦。 对于创业者,我希望他们用小程序去赚第一桶金。对于大企业来说,我希望他们通过小程序去接触到更多的新鲜血液,如果赖以为生,至少在现在的情况下我不支持。” [图片] 数据咨询师张佳:“《高效能人士的七个习惯》里引用过维克多·弗兰克的一句话: 刺激和反应之间,有一个空间。在那个空间里我们有力量选择自己的反应。而我们的反应展现了我们的成长和自由。 放在小程序我们可以这么解读: 小程序流量的获取和流失之间存在一个空间。在那个空间里,我们要有能力选择自己的变现方式。这个变现方式决定了我们能否吃到小程序的红利。” [图片] 谈的最多的还是日活、留存、用户数 如果你将小程序定位于一个商业产品而不是随意玩票,那清晰的使用场景、明确的变现手段以及对流量从来到走阶段的利用,或许要比数据本身更需要你关心。 而像日活,用户数这样的指标,除了能体现出多少人用你的产品之外,还可以起到“交流”作用,我们经常听到“月活十万”、“总用户千万”的话,可见这类指标是多么的深入人心。你确实需要这些指标来告诉别人,自己小程序到底做的怎么样。目前阶段,别人也确实无法通过其他更直观的途径快速了解你的小程序。 留存则是另外一类指标,它代表着用户对你产品的粘性与忠诚度,但小程序毕竟还是生长在一个App而不是手机平台中,主打“用完即走”的特性也决定了它更接近微信功能上的延伸而不是App,所以过于依赖留存来评估一个小程序的优劣或许有些勉强。 总而言之,一方面,我们仍然需要借助日活、留存、用户数等已经成熟的评估标准,不然对小程序的评估交流就会鸡同鸭讲;另一方面,我们又要意识到,不同类型的小程序应该侧重的指标各有不同,因而不能以留存不行、日活不行就轻易否定一个小程序的价值。 你更看重什么指标?或者说,你认为小程序的用户数据哪一个对变现更有价值?
2019-02-21 - 如何使用dataWorker大幅度提高小程序性能
[图片] https://v.qq.com/x/page/j079878zozx.html 在开发小程序的过程中,往往会遇到三个比较明显的问题,一是页面加载后的真机显示效果会在一瞬间(大约半秒)出现错乱,二是页面加载速度慢,尤其是页面需要请求接口的情况,三是全局管理数据,并且把数据同步到Page或者Component内显示麻烦。所以我们研发了一个叫做dataWorker的工具来解决真机显示效果出现错乱、提高页面加载和显示速度、并且做好全局数据管理。 我们在开发多个小程序的过程中,尤其是在解决用户数据上报丢失时发现,微信小程序的生命周期在真机中其实为伪周期。事实上,完全可以在生命周期外走逻辑,请求接口,唯一比较麻烦的把数据同步到页面内。这也就是为什么我们要用dataWorker(可以理解为浏览器内的service worker)来同步更新视图。我相信,市面上的大部分业务方往往是在页面的[代码]onLoad[代码]或者[代码]onShow[代码]的时候才开始加载并且渲染页面的,最终导致了体验上的卡顿,在数据上则常常出现业务PV点消失的情况。但是如果我们在页面还没有被打开之前就加载好了页面,那么打开的速度当然会快很多。 当然,有很多业务方已有的代码实际上是不需要有全局数据介入的,所以我们又添加了一个开关,叫做palindrome(可以理解为数据双向绑定把)。 然后在需要使用worker的Page或Component内添加palindrome: true,如下: Page({ [代码]palindrome: true, data: {...}, onLoad(){...} [代码] }) 那么比方说我现在想要设置该页面的数据,我依然可以使用原先微信提供的this.setData({…}),这和dataWorker不冲突,也不会改变dataWorker内的数据,但是我同时又可以用dataWorker.data = {…}或dataWorker.list = […]这种办法去修改页面的数据(会直接影响视图)。 dataWorker 代码片段,需要使用appId,并且申请《微盟RPRM测试版》插件(wx10692d472c83e02c)0.0.4或以上版本的使用权限。并且需要关闭安全域名检查(需要请求接口)。 代码片段:https://developers.weixin.qq.com/s/5f5PDMmv7O4J
2019-02-27 - 小程序架构设计(二)
接着上篇文章《小程序架构设计(一)》 前边我们说到采用Web+离线包的方式可以解决很多问题,但是遗留了一个安全问题有待解决。 经过了一番讨论,我们决定把开发者的JS逻辑代码放到单独的线程去运行,因为不在Webview线程里,所以这个环境没有Webview任何接口,自然的开发者就没法直接操作Dom,也就没法动态去更改界面,“管控”的问题得以解决。 还存在一个问题:开发者没法操作Dom,如果用户交互需要界面变化的话,开发者就没办法动态变化界面了。所以我们要找到一个办法:不直接操作Dom也能做到界面更新。 其实Facebook早有方案解决这个问题,就是上篇文章提到的React。React引入了Virtual Dom的概念(后文简称VD),业务侧只需要改变数据即可引起界面变化,相关原理后边再写篇文章来分享。 至此小程序双线程的模型就定下来了:渲染层(Webview)+逻辑层(JSCore) [图片] 其中渲染层用了Webview进行渲染,开发者的JS逻辑运行在一个独立的JSCore线程。 渲染层提供了带有数据绑定语法的WXML,逻辑层提供了setData等等API,开发者需要进行界面变化时,只需要通过setData把变化的数据传进去,小程序框架就会进行Dom Diff等流程最后把正确的结果更新在Dom树上。 [图片] 可以看到在开发者的逻辑下层,还需要有一层小程序框架的支持(数据通信、API、VD算法等等),我们把它称为基础库。 我们在两个线程各自注入了一份基础库,渲染层的基础库含有VD的处理以及底层组件系统的机制,对上层提供一些内置组件,例如video、image等等。逻辑层的基础库主要会提供给上层一些API,例如大家经常用到的wx.login、wx.getSystemInfo等等。 解决了渲染问题,我们还要看一下用户在和界面交互时的问题。 [图片] 用户在屏幕点击某个按钮,开发者的逻辑层要处理一些事情,然后再通过setData引起界面变化,整个过程需要四次通信。对于一些强交互(例如拖动视频进度条)的场景,这样的处理流程会导致用户的操作很卡。 对于这种强交互的场景,我们引入了原生组件,这样用户和原生组件的交互可以节省两次通信。 [图片] 正如上图所示,原生组件和Webview不是在同一层级进行渲染,原生组件其实是叠在Webview之上,想必大家都遇到过这个问题,video、input、map等等原生组件总是盖在其他组件之上,这就是这个设计带来的问题。 我们也很重视这个问题,经过了一段时间的努力,我们攻克了这个难题,把原生组件渲染到Webview里,从而实现同层渲染。目前video组件已经完成同层渲染的全量发布,详细可以看我们之前的公告:同层渲染公测。 为了让开发者可以更好的开发小程序,我们在后来还引入了自定义组件和插件的概念,我们后续会有相关的文章再介绍这两块的设计,希望大家关注我们社区的文章板块。 [图片] 以上就是小程序架构设计的历史。
2019-02-27