小程序开发实践赞、评论、分享功能(第一部分)
「这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战」
前言
对于一个内容类的应用,如果我们要为其添加社交
属性,那么点赞
、评论
和分享
无疑是非常必要的功能。这篇我们就来一起为小程序加上这些功能。
功能分析
首先我们分析一下要加的功能,其中点赞
和分享
相对容易,评论
涉及到的逻辑及数据管理相对复杂。让我们先来实现点赞和分享功能。
点赞
点赞就是用户对于某个内容的肯定或喜欢,交互非常简单,就是在内容下方的点赞图标上进行点击,随即图标变为已赞
状态,同时也支持对已赞的内容再次点击变回未赞
的状态。
组件拆分
由于点赞这类操作都是对某个内容的操作,但我们现在只有内容列表组件
,其内部维护的数据是内容列表
。像点赞这类实时性很高的操作,我们需要即时变更页面上的状态,所以显然每次更新列表的数据是不现实的。
因此我们首先要将原本的列表组件拆分为更细粒度的列表组件
和列表项组件
,其中原本列表内维护的大部分数据和方法都会变成列表项组件内部所用,而列表组件主要作为页面
和列表项组件
之间进行双向数据传递的“桥梁”。
列表组件
拆分组件的方法是要理清每个组件各自需要负责的事情,比如我们目标拆分后的列表组件
仅用于接收来自页面的数据并循环渲染列表项组件,同时将循环时拿到的每个数据对象传入列表项。
另外,列表组件还需要统一管理每个列表项的编辑操作,所以列表项内唯一需要管理的状态就是编辑相关的数据,包括是否展示底部选项弹窗
、当前编辑项
和底部选项内容
。
<view class="list-wrap">
<content-item
wx:for="{{items}}"
wx:key="index"
item="{{ item }}"
showEdit="{{ showEdit }}"
bind:settingClick="settingClick"
bind:clickLike="handleLike"
></content-item>
<van-action-sheet
show="{{ showEditing }}"
actions="{{ actions }}"
cancel-text="取消"
bind:close="onClose"
bind:select="onSelect"
bind:cancel="onClose"
/>
</view>
渲染优化
这里我们将底部选项组件
保留在列表组件内,是因为列表组件虽然有很多项,每项都支持进行编辑
操作,但同一时刻只能操作某一项
内容。即某一时刻底部只会有一个选项框弹出,所以没必要在每个列表项内都绑定一个选项组件,所有列表项共用一个即可。
列表项组件
<view class="item-wrap">
<view class="content-wrap">
<!-- 用户信息 -->
<view class="top-part">
<view wx:if="{{ item.isSelf }}" class="user-info">
<view class="user-avatar">
<open-data type="userAvatarUrl" default-avatar="{{defaultAvatar}}"></open-data>
</view>
<view class="user-name">
<open-data type="userNickName" default-text="未知用户"></open-data>
</view>
</view>
<view wx:else class="user-info">
<view class="user-avatar" bindtap="checkAvatar" data-bean="{{ item.avatarUrl }}">
<van-image width="80rpx" height="80rpx" src="{{ item.avatarUrl }}" fit="cover" />
</view>
<view class="user-name">
<text>{{ item.nickName }}</text>
</view>
</view>
<!-- 右上角编辑 -->
<view wx:if="{{ showEdit }}" class="edit-block" bindtap="settingClick" data-bean="{{ item }}">
<van-icon name="ellipsis" />
</view>
</view>
<!-- 正文 -->
<text wx:if="{{ item.text }}" class="item-text">{{item.text}}</text>
<!-- 图片 -->
<view
wx:if="{{ item.image && item.image.length }}"
class="{{ item.image.length === 4 ? 'image-container image-container-small' : 'image-container' }}"
>
<view
class="{{ item.image.length === 1 ? 'image-wrap single-image' : 'image-wrap' }}"
wx:for="{{ item.image }}"
wx:for-item="imageItem"
wx:for-index="imageIndex"
wx:key="imageIndex"
bindtap="onTapImage"
data-bean="{{ { current: imageItem, list: item.image } }}"
>
<van-image
width="{{ item.image.length === 1 ? '100%' : '220rpx' }}"
height="{{ item.image.length === 1 ? '300rpx' : '220rpx' }}"
radius="10rpx"
src="{{ imageItem }}" fit="cover"
/>
</view>
</view>
<!-- 点赞、评论、分享 -->
<view class="bottom-part">
<view class="like-block">
<van-image
src="{{ likeStatus ? '/assets/images/like-highlight-v2.png' : '/assets/images/like-v2.png' }}"
width="40rpx"
height="40rpx"
bind:click="handleLike"
data-bean="{{ item }}"
></van-image>
<text class="bottom-count">{{ likeCount }}</text>
</view>
</view>
</view>
</view>
列表项组件的视图文件基本就是将原本列表组件的内容迁移过来,然后去掉最外层。
关于逻辑部分,一个列表项要维护的内部状态就是它要展示的相关内容,比如头像、昵称、内容和图片。这些都来自外部传入的内容对象
,而点赞这类可能会即时发生改变的状态,组件内部可以用一个状态变量进行维护,初始值来自于外部传入的对象。
对于图片预览和头像预览这类操作,由于没有与其他组件的数据通信
,所以可以在组件内部进行实现。
比较复杂的部分就是关于点赞即时更新的处理,接下来我们来重点分析这部分的实现细节。
点赞状态更新
首先我们需要改造之前用于存储列表内容的对象结构,增加一项点赞者用于存储内容与点赞用户之间的关联。
然后对于一项内容的点赞状态更新可以分为页面侧的即时响应
和数据库中内容记录的状态变更
。因为通过云函数更新数据库中记录的状态会受网络因素
影响,如果页面要等待云函数的执行完成再更新点赞状态,在网络条件差的情况下页面的点赞反馈体验会比较差。
因此我们采取前端即时更新
点赞状态,同时调用云函数去对内容的点赞状态进行数据库层面的更新,然后在云函数执行完成,根据执行结果进行点赞状态的修正
,即对于数据更新失败的情况做点赞变更的撤销。
整体逻辑如下,供大家理解参考
云函数核心逻辑如下:
// 有id为修改
if (id) {
const newObj = {}
// 点赞逻辑
if (['like', 'unlike'].includes(action)) {
const user = {
openid: wxContext.OPENID,
nickName,
avatarUrl
}
if (action === 'like') {
// 点赞:向点赞者列表中添加点赞者对象
Object.assign(newObj, {
likers: _.push(user)
})
} else if (action === 'unlike') {
// 取消赞:查找点赞者中当前用户并移除
Object.assign(newObj, {
likers: _.pull({
openid: _.eq(user.openid)
})
})
} else {
throw new Error('未知的操作类型')
}
} else {
Object.assign(newObj, {
text,
image,
updateTime: Date.now()
})
}
const updateData = {
data: newObj
}
const { stats: { updated } } = await db.collection('homeContentList').doc(id).update(updateData)
if (updated < 1) {
errno = 400
errmsg = '更新失败,请稍后重试'
}
Object.assign(res, { updated })
}
这里我们根据本次交互是点赞
还是取消赞
,来分别对集合中的记录进行各自的更新操作。如果是点赞,要在记录下新增当前点赞者的用户对象,这里我们使用到了push方法,对记录中的数组类型字段进行插入操作。如果是取消赞,要从记录中的点赞者中找到当前用户并移除,我们用到用到了pull方法。
分享
接下来是分享功能,小程序页面内置分享方法,我们只需要根据 文档 进行使用即可。
由于分享的对象是页面
,而我们现在只有包含内容列表的首页,没有具体的内容详情页
。所以如果想针对某条内容进行分享,需要为内容增加内容详情页
。
详情页开发
页面组成
详情页其实就是只有一条内容的列表页,所以我们可以复用前面拆出来的列表项组件进行详情页的构造。
<view class="detail-wrap">
<view wx:if="{{item}}">
<content-item
item="{{ item }}"
></content-item>
</view>
<view class="empty-block" wx:else>
<van-loading size="24px" vertical>加载中...</van-loading>
</view>
</view>
详情页跳转
同时,我们为列表项的最大容器增加点击响应事件,用于跳转内容详情页。
这里需要注意,由于列表项的最大容器内部还包含很多子容器
,子容器上如果有事件绑定,由于DOM的事件机制,会在子容器的事件绑定触发后继续触发其父容器上绑定的事件。现象就是当我们点击图片预览时,会展示图片的预览,接着页面还会跳转至详情页,很明显这不符合我们的预期。
所以我们需要使用catchtap方法来阻止这些子元素的点击事件触发整个内容项的详情跳转事件,而仅当点击没有特殊动作的区域时去跳转内容详情页。
跳转逻辑
这里有一个矛盾的点是,我们在列表组件
内使用了内容项组件
,同时在内容详情页
也使用了内容项组件
。所以如果我们在内容项组件内实现点击跳转逻辑,则会导致内容详情页的内容区域点击还会继续跳转详情页。
这里的解决办法就是不在内容项组件直接实现跳转逻辑,而是将跳转逻辑向外派发
,由组件调用方
来实现。由于内容项组件在列表
内和在详情页
的调用方不同,所以我们仅对列表内的内容项点击做详情页跳转响应即可。
具体实现如下
这样我们就完成了内容详情页的开发
分享设置
当一个页面声明了onShareAppMessage
方法后,该页面即拥有了分享能力,即使这个方法为空,默认会将当前页面的标题作为分享标题,将当前页面的缩略图作为分享图片。
当然我们也可以在onShareAppMessage
方法返回一个对象来主动设置分享卡片的内容。
我们可以通过在开发者工具的模拟器右上角点击...
来查看分享设置效果
分享唤起
完成了内容详情页的开发和分享设置的熟悉后,我们来为列表页的每个内容项增加更具交互性质的分享按钮。
对于分享的主动唤起,我们需要使用小程序的内置组件button
,并设置open-type
进行使用,但是小程序原生的button
有其默认的样式和伪元素装饰,所以我们需要根据实际使用来覆盖掉这些默认样式。
另外,由于我们整个容器绑定有事件响应,所以要对这里的分享按钮进行容器包裹并使用catchtap
来阻止分享按钮点击后的事件冒泡
。
动态配置分享卡片
这里还有一个复杂的点,就是假如我们想在列表页根据每个内容项的具体内容来设置对应的分享内容,要怎么做呢?因为分享是针对于当前页面
进行设置的,但我们当前的场景是要在首页展示内容列表
的情况下,点击某条内容,针对这条具体内容来设置分享卡片。
比如我们点击第一条内容的分享按钮时,分享标题是第一条内容的文本,而点击第二条内容的分享时,分享标题是第二条内容的文本。
这就需要我们在点击某条内容的分享按钮后,将该条内容的相关信息存储在某个全局位置,然后当首页的分享被触发后,去读取全局的分享设置。
我们试着在之前按钮外部绑定的方法内将当前内容的分享配置存入全局对象。
这里使用到了getApp()
方法,该方法可以读取小程序全局对象,对于在app.js
中声明的对象可用于全局访问。
我们将当前内容的分享配置存入全局对象的shareObj
属性中,然后在首页的分享方法中读取该内容。
这里我们使用了分享方法的promise属性,是因为我们绑定在分享按钮上的自定义事件会在open-type=share
之后响应,所以如果不使用promise
对分享进行设置的话,首页内读取全局分享设置会发生在实际设置之前。
反之,如果我们通过promise
属性让首页的分享设置进行等待,这样我们就可以等到组件更新后的分享设置。
这样我们就完成了分享功能,让我们用前面介绍过的代码管理功能对截止目前的改动进行一次提交。
这样会对本地的文件修改进行一次提交,git会记录下这次的改动内容,然而远程的项目此时还停留在上一次更新的状态,所以我们将本地的提交记录进行推送
,将远程项目同步至本地最新的状态。
总结
到这里,我们完成了小程序的点赞
和分享
功能,其中在实现点赞过程中将原本的内容列表组件
拆分为了独立的列表组件
+列表内容项组件
。并且在点赞的逻辑实现上实现了页面上的即时响应
+数据库记录修改执行后的异步修正
。对于分享功能,我们使用了小程序分享API的异步设置
属性,实现了将组件内的个性化分享设置传递至页面。
对于相对独立的评论功能我们将放到下篇继续。
推荐阅读
-
纯干货分享 | 研发效能提升——敏捷需求篇-而敏捷需求是提升效能的方式中不可或缺的模块之一。 云智慧的敏捷教练——Iris Xu近期在公司做了一场分享,主题为「敏捷需求挖掘和组织方法,交付更高业务价值的产品」。Iris具有丰富的团队敏捷转型实施经验,完成了企业多个团队从传统模式到敏捷转型的落地和实施,积淀了很多的经验。 这次分享主要包含以下2个部分: 第一部分是用户影响地图 第二部分是事件驱动的业务分析Event driven business analysis(以下简称EDBA) 用户影响地图,是一种从业务目标到产品需求映射的需求挖掘和组织的方法。 在软件开发过程中可能会遇到一些问题,比如大家使用不同的业务语言、技术语言,造成角色间的沟通阻碍,还会导致一些问题,比如需求误解、需求传递错误等;这会直接导致产品的功能需求和要实现的业务目标不是映射关系。 但在交付期间,研发人员必须要将这些需求实现交付,他们实则并不清楚这些功能需求产生的原因是什么、要解决客户的哪些痛点。研发人员往往只是拿到了解决方案,需要把它实现,但没有和业务侧一起去思考解决方案是否正确,能否真正的帮助客户解决问题。而用户影响地图通常是能够连接业务目标和产品功能的一种手段。 我们在每次迭代里加入的假设,也就是功能需求。首先把它先实现,再逐步去验证我们每一个小目标是否已经实现,再看下一个目标要是什么。那影响地图就是在这个过程中帮我们不断地去梳理目标和功能之间的关系。 我们在软件开发中可能存在的一些问题 针对这些问题,我们如何避免?先简单介绍做敏捷转型的常规思路: 先做团队级的敏捷,首先把产品、开发、测试人员,还有一些更后端的人员比如交互运维的同学放在一起,组成一个特训团队做交付。这个团队要包含交付过程中所涉及的所有角色。 接着业务敏捷要打通整个业务环节和研发侧的一个交付。上图中可以看到在敏捷中需求是分层管理的,第一层是业务需求,在这个层级是以用户目标和业务目标作为输入进行规划,同时需要去考虑客户的诉求。业务人员通过获取到的业务需求,进一步的和团队一起将其分解为产品需求。所以业务需求其实是我们真正去发布和运营的单元,它可以被独立发布到我们的生产环境上。我们的产品需求其实就是产品的具体功能,它是我们集成和测试的对象,也就是我们最终去部署到系统上的一个基本单元。产品需求再到了我们的开发团队,映射到迭代计划会上要把它分解为相应的技术任务,包括我们平时所说的比如一些前端的开发、后端的开发、测试都是相应的技术任务。所以业务敏捷要达到的目标是需要去持续顺畅高质量的交付业务价值。 将这几个点串起来,形成金字塔结构。最上层我们会把业务目标放在整个金字塔的塔尖。这个业务目标是通过用户的目标以及北极星指标确立的。确认业务目标后再去梳理相应的业务流程,最后生产。另外产品需求包含了操作流程和业务规则,具需求交付时间、工程时间以及我们的一些质量标准的要求。 谈到用户影响的地图,在敏捷江湖上其实有一个传说,大家都有一个说法叫做敏捷需求的“任督二脉”。用户影响地图其实就是任脉,在黑客马拉松上用过的用户故事地图其实叫督脉。所以说用户影响地图是在用户故事地图之前,先帮我们去梳理出我们要做哪些东西。当我们真正识别出我们要实现的业务活动之后,用户故事地图才去梳理我们整个的业务工作流,以及每个工作流节点下所要包含的具体功能和用户故事。所以说用户影响地图需要解决的问题,我们包括以下这些: 首先是范围蔓延,我们在整张地图上,功能和对应的业务目标是要去有一个映射的。这就避免了一些在我们比如有很多干系人参与的会议上,那大家都有不同想法些立场,会提出很多需求(正确以及错误的需求)。这个时候我们会依据目标去看这些需求是否真的是会影响我们的目标。 这里提到的错误需求,比如是利益相关的人提出的、客户认为产品应该有的、某个产品经理需求分析师认为可以有的....但是这些功能在用户影响地图中匹配不到对应目标的话,就需要降低优先级或弃掉。另外,通常我们去制定解决方案的时候,会考虑较完美的实现,导致解决方案括很多的功能。这个时候关键目标至关重要,会帮助我们梳理筛选、确定优先级。 看一下用户影响到地图概貌 总共分为一个三层的结构: 第一层why,你的业务目标哪个是最重要的,为什么?涉及到的角色有哪些? 第二层how ,怎样产生影响?影响用户角色什么样的行为? (不需要去列出所有的影响,基于业务目标) 第三层what,最关键的是在梳理需求时不需一次把所有细节想全,这通常团队中经常遇到的问题。 我们用这个例子来看一下 这是一个客服中心的影响地图,业务目标是 3个月内不增加客服人数的前提下能支持1.5倍的用户数。此业务目标设定是符合 smart 原则的,specific非常的具体,miserable 是可以衡量的,action reoriented是面向活动的, real list 也是很实际的。 量化的目标会指引我们接下来的行动,梳理一个业务目标,尽量去量化,比如 :我们通过打造一条什么样的流水线,能够提高整个部署的效率,时间是原来的 1/2 。这样才是一个能量化的有意义的目标。 回到这幅图, how 层级识别出来的内容,客服角色:想要对它施加的影响,把客户引导到论坛上,帮助客户更容易的跟踪问题,更快速的去定位问题。初级用户:方论坛上找到问题。高级用户:在论坛上回答问题。通过我们这些用户角色,进行活动,完成在不增加客户客服人数的前提下支持更多的用户数量。 最后一个层级,才是我们日常接触比较多的真正的功能的特性和需求,比如引导到客户到论坛上,其实这个产品就需要有一个常见问题的论坛的链接。这个层次需要我们团队进一步地在交付,在每个迭代之前做进一步的梳理,细化成相应的用户故事。 这个是云智慧团队中,自己做的影响地图的范例,可以看下整个的层级结构。序号表示优先级。 那我们用户影响地图可以总结为:
-
小程序开发实践赞、评论、分享功能(第一部分)