「这是我参与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属性中,之后在首页的分享方式中读取该内容。
这儿我们使用了分享方式的属性,是由于我们绑定在分享按键上的自定义风波会在open-type=share然后响应,所以假如不使用promise对分享进行设置的话,首页内读取全局分享设置会发生在实际设置之前。
反之,假如我们通过promise属性让首页的分享设置进行等待,这样我们就可以等到组件更新后的分享设置。
这样我们就完成了分享功能,让我们用上面介绍过的代码管理功能对截至目前的改动进行一次递交。
这样会对本地的文件更改进行一次递交,git会记录下此次的改动内容,但是远程的项目此时还逗留在上一次更新的状态,所以我们将本地的递交记录进行推送,将远程项目同步至本地最新的状态。
总结
到这儿,我们完成了小程序的点赞和分享功能,其中在实现点赞过程上将原先的内容列表组件分拆为了独立的列表组件+列表内容项组件。而且在点赞的逻辑实现上实现了页面上的即时响应+数据库记录更改执行后的异步修正。对于分享功能,我们使用了小程序分享API的异步设置属性,实现了将组件内的个性化分享设置传递至页面。
对于相对独立的评论功能我们将放在上篇继续。