实现一个外卖双侧联动加载器

需求分析

外卖软件的店面,一般都是分左右两栏,左侧较窄为类别栏,右侧为类别的具体商品.

查看各家店面,会发现有两种加载方式.
一种右侧不能贯通滚动,一次只加载一个类别对应的商品,通过点击左侧类别切换右侧显示的商品.
一种是右侧可以贯通滚动,滚动时,左侧的类别楼层还会跟随切换.

这实际上对应了两种数据加载方式.
前者是先加载类别数据,再按类别分别加载商品数据.如百果园.适用于商品较多的商家
后者是一次性加载所有数据,也是最普遍的商家模板.普通饭馆的菜单不多,一次加载没有压力.

公司的产品是to B产品.商家卖什么的都有,第一期为兼容起见.采用了前者.但是有些商家商品不多,就不合适了.他们更喜欢后者的交互方式.

因此需求就是.如何兼容商品多和商品少的数据,设计一种允许右侧贯通滚动的交互方式?

方案

因为有些商家的类别,商品较多,而且最好避免过大调整后端接口,因此数据加载方式,依然采用前者.但是在返回类别数据时,额外再加上一个数据,每个类别下商品的数量.
这个数量,实际上就决定了右侧它所对应的商品区块的高度.我们可以使用一个固定高度的元素来占位.这样即便在没有加载到任何商品的情况下,在右侧我们也可以滚动,而且可以为这些元素加上类似骨架屏的效果.

然后我们在以下三种情况去请求商品数据.

  1. 页面初始化,请求视口内的类别对应的商品
  2. 点击左侧类别.请求这个类别对应的商品
  3. 右侧滚动,检查在视口内的类别,请求这些类别对应的商品.

要点

仔细分析,以上三种其实都可以合并为一种.
检查视口内的类别,请求它们的商品数据.
因为点击左侧时,右侧会联动,滚动到对应的位置.

因此我们需要实现一个检查元素是否在视口内的方法.再进一步抽象.就是检查一堆纵向排列的盒子,是否与一个固定盒子相交的问题.

要将一个具体的问题抽象成一个简单问题,过程比较繁琐.我们一步一步来.
项目使用vue框架.因此我们以组件的形式来组织代码.我们已有的组件有

  • 商品卡片组件.展示商品,高度固定
  • 滚动组件,允许slot滚动.滚动中,结束,都会派发事件,通知高度.提供方法滚动到指定位置.
    核心逻辑在右侧容器组件.我们创建一个right组件.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <template>
    <Scroll class="right" ref="scroll" @scroll-end="viewCheck">
    <ul class="goods-list">
    <li class="goods-item" v-for="it in cate" :key="it.categoryGuid" ref="cate">
    <h3 class="cate-title">{{it.categoryName}}</h3>
    <ul class="card-list">
    <li class="card-item" v-for="i in it.goodsCount">
    <Card :guid="it.categoryGuid" :index="i-1"/>
    </li>
    </ul>
    </li>
    </ul>
    </Scroll>
    </template>

cate是组件的prop,由外部传入.即类别数组.元素是类别项目.带有其下的商品数量goodsCount及类别GUIDcategoryGuid
将其展开.对应元素cate.再按其商品数量,展开card.(我们先将card改造,没有数据时展示骨架.有数据展示数据).注意v-for展开纯数字时,从1开始.

按照上述逻辑.我们需要抽象出一堆盒子.也就是每个cate元素的顶边与底边距离容器顶部的距离.
获取方式很多.我们可以通过dom.比较简单.但是依赖于dom渲染完成.所以我们通过简单的计算直接获取.

  1. 获取每个cate顶边距离容器顶部的距离.offsetList

    1
    2
    3
    4
    5
    6
    7
    let list = [0]
    let offset = 0
    this.cate.forEach(c => {
    offset += c.goodsCount * 91 + 40 // 91是card组件的高度,40是标题`.cate-title`的高度
    list.push(offset)
    });
    this.offsetList = list
  2. 获取盒子的数据rectList

    1
    2
    3
    4
    5
    this.rectList = this.cate.map((n, i) => ({
    top: this.offsetList[i],
    bottom: this.offsetList[i + 1],
    guid: n.categoryGuid
    }));
  3. 实现检查方法viewCheck
    上述逻辑中,固定盒子也就是视口.这是一个抽象的概念,并没有实际的元素.
    仔细分析.它的顶边实际上就是滚动组件的上移绝对值.底边就是顶边加容器的高度.
    有了视口以及一个盒子的rectList,检查哪个盒子在视口内就可以实现了.
    我们对rectList遍历,如果一个rect,它的底边或顶边任意一个在视口内,那么它就是可被看见的.
    伪代码就是
    (盒子底边 < 视口底边 && 盒子底边 > 视口顶边) 底边在视口内
    ||
    盒子顶边 > 视口顶边 && 盒子顶边 < 视口底边 顶边在视口内

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    viewCheck(value) {
    // 获取视口顶部距容器顶部距离及底部距容器顶部的距离.
    let top = -value, bottom = -value + this.height
    let viewGuids = this.rectList.filter(rect => {
    return rect.bottom < bottom && rect.bottom > top ||
    rect.top > top && rect.top < bottom
    })

    viewGuids.forEach(({guid}) => {
    this.$store.dispatch('getGoodsByGuid', guid)
    })
    },

Right组件完整代码

总结

其他没什么搞头,无非就是监视,联动什么的.
GIT项目代码