# 外卖小程序首页商品列表和购物车的优化

在项目中,做了一个类似饿了么的外卖小程序,首页效果类似饿了么。但是实际商品多了之后存在卡顿问题,而饿了么明显很流畅,所以通过优化应该能够达到类似的效果。

# 展示方式

第一版是类似饿了么外卖的效果,左边展示商品分类,右边一个长列表展示所有商品。 当商品滚动时,监听滚动,左边自动切换当前分类;当点击左侧分类,右侧滚动到对应位置。同时,每个商品可点击+号,会出现一个小球动画,并向左展开为- 1 +。 滚动监听通过wx.createIntersectionObserver实现;而点击滚动可以用scroll-view的scroll-into-view来实现。

实际测试时,发现对于商品较多的店铺存在明显的卡顿。 发现饿了么的超市商品很多,采用了另外一种展示方式,即右侧列表只展示一个分类的商品,滚动到顶部或者底部时,继续上拉或者下拉,切换分组。 开始在scroll-view内顶部和底部增加提示文字,用touch事件监听,但是不好处理惯性滚动。然后用scroll-view嵌套scroll-view的方法,内部惯性滚动不会触发外侧滚动。再对外侧touch事件监听实现回弹。 发现还是明显卡顿。

# 长列表优化

由于是商品过多造成的卡顿,首先想到是长列表的优化。虽然由全部商品变成了一个分类的商品,但是还是存在大量的dom节点。 于是通过事件监听,来动态渲染商品。对于所有商品,只显示一个view标签用于占位,这个view标签的background用一张骨架图渲染。而对于出现在屏幕可视区域的商品,调用组件方法进行渲染,当商品滚动到可视区域之外,再次切换未骨架图。这样,在整个列表中,除了几个商品之外,外部只有一个dom节点,大大减少了节点数量。

// goods-item.wxml
<wxs src="../../commom/common.wxs" module="m0"></wxs>

<view class="category-goods skeleton" wx:if="{{skeleton || (lazy && !showGoods)}}">
</view>

<view class="category-goods" catch:tap="handleShowDetail" wx:else>
  <view class="goods-img-wrapper">
    <image></image>
  </view>
  <view class="category-cntt">
    <view class="info">
      ...
    </view>

      <view class="right">
        <goods-counter id="goods-{{goods.id}}" goods="{{goods}}" count="{{m0.getGoodsCount(goods.id, cartStore.flatList)}}" businessStatus="{{businessStatus}}" cartStore="{{cartStore}}" catch:skuSelect="handleShowSkuSelect"></goods-counter>
      </view>
    </view>
  </view>
</view>
// goods-item.js
show () {
  // console.log('show')
  if (this.timer || this.data.showGoods === true) return false
  this.timer = setTimeout(() => {
    this.setData({
      showGoods: true,
    })
  }, 500)
},
hide () {
  // console.log('hide')
  if (this.timer) {
    clearTimeout(this.timer)
    this.timer = null
  } 
  if (this.data.showGoods) {
    this.setData({
      showGoods: false,
    })
  }
},
// 页面监听,控制goods显示隐藏
this.observer1 = wx.createIntersectionObserver(this, {
  observeAll: true,
})
this.observer1
  .relativeToViewport({
    top: 200,
    bottom: 400,
  })
  .observe('.goods-item', (res) => {
    // console.log(res)
    if (res.intersectionRatio > 0) {
      this.selectComponent('#' + res.id).show()
    } else if (res.intersectionRatio === 0) {
      this.selectComponent('#' + res.id).hide()
    }
  })

一方面对页面滚动进行监听,一方面对商品的显示和隐藏做debounce处理,防止快速滚动页面时,商品不停地显示、隐藏操作dom。

# 内存优化

实际测试发现,对于存在大量商品的店铺(几百个),在滚动时还好,添加商品到购物车时依然卡顿。因为页面只渲染一个分类的商品(几到几十个),所以应该与渲染无关。单纯滚动页面不算卡,但是进行各种操作时就很明显,应该是内存的问题。 将获取全部分类和商品,改为获取全部分类,并且每次只请求一个分类的商品。这样,内存中的商品从几百个变成了几十个,明显流畅。 另外,连续点击添加购物车,小球动画很不流畅。需要对购物车操作进行优化。

# 添加购物车优化

在分类列表每个商品及购物车窗口每个商品中,都包含了一个加减购物车的组件。 每次添加购物车,都会调用添加购物车接口,然后重新请求购物车商品,重新计算左侧分类商品数量。这样在快速连续点击添加或移除商品时,会明显卡顿,小球动画尤其明显。移除商品同样。 在点击+或-按钮时,做一个debounce效果,立刻修改数量及动画,但是延迟提交请求。这样,在快速连续点击时,可以一次修改商品数量,而非多次请求。

// goods-counter.js
clearTimeout(this.countTimer)
this.setData({
  tempCount: this.data.tempCount + 1
})
this.addbBall()
this.countTimer = setTimeout(() => {
  this.$page().props.cartStore.editCartGoodsNum({
    id: cartItem.cartId,
    number: this.data.tempCount,
  })
}, 200)

修改之后,小球动画非常流畅。

# 长列表优化2

之前的优化,当分类商品数量多时,还是能感觉到卡顿。 改用微信的长列表组件recycle-view来重构代码。recycle-view和scroll-view的写法差不多,但是针对长列表进行了优化。内部通过元素高度和滚动高度来计算哪些元素需要显示,其余的元素不渲染,通过padding来上下撑满高度。这样即使数据很多,页面只渲染固定数量的元素。

# 总结

通过这几方面的优化,终于实现了类似饿了么的流程效果。关键还是对于通过长列表组件来解决了大量数据的性能问题。

Last Updated: 11/22/2019, 5:12:51 PM