欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

小程序开发实践赞、评论、分享功能(第一部分)

最编程 2024-03-22 17:01:23
...

「这是我参与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>

列表项组件的视图文件基本就是将原本列表组件的内容迁移过来,然后去掉最外层。

关于逻辑部分,一个列表项要维护的内部状态就是它要展示的相关内容,比如头像、昵称、内容和图片。这些都来自外部传入的内容对象,而点赞这类可能会即时发生改变的状态,组件内部可以用一个状态变量进行维护,初始值来自于外部传入的对象。

对于图片预览和头像预览这类操作,由于没有与其他组件的数据通信,所以可以在组件内部进行实现。

比较复杂的部分就是关于点赞即时更新的处理,接下来我们来重点分析这部分的实现细节。

点赞状态更新

首先我们需要改造之前用于存储列表内容的对象结构,增加一项点赞者用于存储内容与点赞用户之间的关联。

image.png

然后对于一项内容的点赞状态更新可以分为页面侧的即时响应和数据库中内容记录的状态变更。因为通过云函数更新数据库中记录的状态会受网络因素影响,如果页面要等待云函数的执行完成再更新点赞状态,在网络条件差的情况下页面的点赞反馈体验会比较差。

因此我们采取前端即时更新点赞状态,同时调用云函数去对内容的点赞状态进行数据库层面的更新,然后在云函数执行完成,根据执行结果进行点赞状态的修正,即对于数据更新失败的情况做点赞变更的撤销。

整体逻辑如下,供大家理解参考

image.png

云函数核心逻辑如下:

// 有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>
详情页跳转

同时,我们为列表项的最大容器增加点击响应事件,用于跳转内容详情页。

image.png

这里需要注意,由于列表项的最大容器内部还包含很多子容器,子容器上如果有事件绑定,由于DOM的事件机制,会在子容器的事件绑定触发后继续触发其父容器上绑定的事件。现象就是当我们点击图片预览时,会展示图片的预览,接着页面还会跳转至详情页,很明显这不符合我们的预期。

所以我们需要使用catchtap方法来阻止这些子元素的点击事件触发整个内容项的详情跳转事件,而仅当点击没有特殊动作的区域时去跳转内容详情页。

跳转逻辑

这里有一个矛盾的点是,我们在列表组件内使用了内容项组件,同时在内容详情页也使用了内容项组件。所以如果我们在内容项组件内实现点击跳转逻辑,则会导致内容详情页的内容区域点击还会继续跳转详情页。

这里的解决办法就是不在内容项组件直接实现跳转逻辑,而是将跳转逻辑向外派发,由组件调用方来实现。由于内容项组件在列表内和在详情页的调用方不同,所以我们仅对列表内的内容项点击做详情页跳转响应即可。

具体实现如下

image.png

image.png

这样我们就完成了内容详情页的开发

image.png

分享设置

当一个页面声明了onShareAppMessage方法后,该页面即拥有了分享能力,即使这个方法为空,默认会将当前页面的标题作为分享标题,将当前页面的缩略图作为分享图片。

当然我们也可以在onShareAppMessage方法返回一个对象来主动设置分享卡片的内容。

我们可以通过在开发者工具的模拟器右上角点击...来查看分享设置效果

image.png

分享唤起

完成了内容详情页的开发和分享设置的熟悉后,我们来为列表页的每个内容项增加更具交互性质的分享按钮。

对于分享的主动唤起,我们需要使用小程序的内置组件button,并设置open-type进行使用,但是小程序原生的button有其默认的样式和伪元素装饰,所以我们需要根据实际使用来覆盖掉这些默认样式。

image.png

另外,由于我们整个容器绑定有事件响应,所以要对这里的分享按钮进行容器包裹并使用catchtap来阻止分享按钮点击后的事件冒泡

动态配置分享卡片

这里还有一个复杂的点,就是假如我们想在列表页根据每个内容项的具体内容来设置对应的分享内容,要怎么做呢?因为分享是针对于当前页面进行设置的,但我们当前的场景是要在首页展示内容列表的情况下,点击某条内容,针对这条具体内容来设置分享卡片。

比如我们点击第一条内容的分享按钮时,分享标题是第一条内容的文本,而点击第二条内容的分享时,分享标题是第二条内容的文本。

这就需要我们在点击某条内容的分享按钮后,将该条内容的相关信息存储在某个全局位置,然后当首页的分享被触发后,去读取全局的分享设置。

image.png

我们试着在之前按钮外部绑定的方法内将当前内容的分享配置存入全局对象。

image.png

这里使用到了getApp()方法,该方法可以读取小程序全局对象,对于在app.js中声明的对象可用于全局访问。

image.png

我们将当前内容的分享配置存入全局对象的shareObj属性中,然后在首页的分享方法中读取该内容。

image.png

这里我们使用了分享方法的promise属性,是因为我们绑定在分享按钮上的自定义事件会在open-type=share之后响应,所以如果不使用promise对分享进行设置的话,首页内读取全局分享设置会发生在实际设置之前。

反之,如果我们通过promise属性让首页的分享设置进行等待,这样我们就可以等到组件更新后的分享设置。

这样我们就完成了分享功能,让我们用前面介绍过的代码管理功能对截止目前的改动进行一次提交。

image.png

这样会对本地的文件修改进行一次提交,git会记录下这次的改动内容,然而远程的项目此时还停留在上一次更新的状态,所以我们将本地的提交记录进行推送,将远程项目同步至本地最新的状态。

image.png

总结

到这里,我们完成了小程序的点赞分享功能,其中在实现点赞过程中将原本的内容列表组件拆分为了独立的列表组件+列表内容项组件。并且在点赞的逻辑实现上实现了页面上的即时响应+数据库记录修改执行后的异步修正。对于分享功能,我们使用了小程序分享API的异步设置属性,实现了将组件内的个性化分享设置传递至页面。

对于相对独立的评论功能我们将放到下篇继续。

推荐阅读