yanyue404/blog

微信小程序产品页改版分享

yanyue404 opened this issue · 0 comments

前言

需求描述

小程序产品列表和 APP 接口统一,实现产品轮播页单页楼梯导航效果

效果预览

App 与 小程序产品页交互效果查看

实现思路

1. 改造数据

// clearnData 方法
Page({
  clearnData(data) {
    // ...
    data.forEach((o, dataIndex) => {
      objList.push({
        key: o[0].moduleKey, // allHotProd
        name: o[0].parentTitle, // 全部
        currentLabel: "page" + dataIndex + "_" + o[0].moduleKey, // page0_allHotProd
        info: [...o], // 所有数据集合 - dataList 分组集合
      });
    });
  },
   this.setData(
      {
        swiperList: objList,
        currentType: objList[0].key,
      }
    );
});

2. 基本实现:scroll-into-view + 滚动监听

点击上面的 label 已经可以正确定位了,如何滚动监听回显?

需要的是详细的高度信息。

<!-- ! swiperList 页切换 -->
<scroll-view scroll-x="true" class="head-type">
  <view
    class="type-item {{item.key == currentType ? 'active' : ''}}"
    wx:for="{{swiperList}}"
    wx:key="key"
    bindtap="changeType"
    data-type="{{item.key}}"
  >
    <view class="type-item-name">{{item.name}}</view>
    <view class="type-item-line"></view>
  </view>
</scroll-view>
<!-- swiper 实际区域 -->
<swiper>
  <swiper-item
    wx:for="{{swiperList}}"
    wx:key="key"
    wx:for-index="pageIndex"
    wx:for-item="page"
  >
    <view class="label-content">
      <!-- 子标签切换 -->
      <view
        wx:for="{{page.info}}"
        wx:for-item="info"
        wx:key="key"
        wx:for-index="infoIndex"
        hidden="{{info.dataList.length === 0 || info.show === 'false'}}"
        class="label-item {{info.key == page.currentLabel ? 'active' : ''}}"
        data-label="{{info.key}}"
        bindtap="changeLabel"
        >{{info.title}}</view
      >
      <scroll-view
        class="scrollBox"
        scroll-y="true"
        scroll-into-view="{{toView}}"
        scroll-with-animation="true"
        enable-back-to-top="true"
        bindscroll="listenScroll"
      >
        <view
          class="group"
          id="{{infoitem.key}}"
          wx:for="{{page.info}}"
          wx:for-item="infoitem"
          wx:key="key"
          data-rol="{{infoitem.key}}"
          wx:for-index="groupIndex"
          hidden="{{infoitem.dataList.length === 0 || infoitem.show === 'false'}}"
        >
          <!-- 组标题:爆款热销 -->
          <!-- - 广告 ads -->
          <!-- - 产品 product -->
          <!-- - 产品 product -->
          <!-- - 产品 product -->
        </view>
      </scroll-view>
    </view>
  </swiper-item>
</swiper>
Page({
  data: {
    swiperList: [], // 新的数据容器
    toView: "", // 滚动目标 scroll-into-view 的 id
    currentType: "all",
    currentTypeIndex: 0, //当前轮播图的第几页
  },
  //切换标签
  changeLabel(e) {
    let label = e.currentTarget.dataset.label;
    let param = "swiperList[" + this.data.currentTypeIndex + "].currentLabel";
    this.setData({
      [param]: label,
      toView: label,
      isClickLabel: true,
    });
  },
  listenScroll: debounce(function (e) {
    this.scrollFn(e);
  }, 8),
  scrollFn(e) {
      ...
    const infoList = swiperList[currentTypeIndex].info;
    let init = 0;
    // [521, 686, 1184, 1685, 1850, 2015, 2180, 2345]
    const heightMap = infoList.map((v, index) => {
      if (v.height === 0) {
        return 0;
      }
      init += v.height;
      return init;
    });
    let heightMapIndex = heightMap.findIndex((v) => v > e.detail.scrollTop);
    ...
  },
});

3. 如何获取集合所有分组的高度?

先动态计算再进行精度调整

Page({
  data: {
    heightInfo: {
      // (参照单位 : width 375 的设计稿)
      title: 86, // 标题种类 1:仅有标题
      desc: 126, // 标题种类 2:带有标题描述
      ads: 228, // 广告高度
      product: 222, // 产品的高度
      gap: 22, // 间隔线
    },
  },
  getGroupHeight(datas, descText) {
    const { title, desc, gap, ads, product } = this.data.heightInfo;
    let base = descText ? desc + gap : title + gap;
    let num_ads = datas.filter((m) => m.itemType === "ads").length;
    let num_products = datas.filter((m) => m.itemType === "product").length;
    if (datas.length > 0) {
      return Number(
        Math.floor((base + num_ads * ads + product * num_products) / 2)
      );
    } else {
      return 0;
    }
  },
  calculationAccuracy() {
    let query = wx.createSelectorQuery();
    query
      .select(".page0_firstGroup")
      .boundingClientRect((rect) => {
        if (rect === null) {
          console.error("首次获取dom失败");
        } else if (rect.height) {
          let height = rect.height;
          const default_firstGroupHeight = this.data.swiperList[0].info[0]
            .height;
          let scale = toFixed(height / default_firstGroupHeight, 3);
          //   更新 swiperList 的真实的高度
        }
      })
      .exec();
  },
});

4. 什么时机去更新

(1) 延时 seTimeout

Page({
  onReady() {
    setTimeout(() => {
      // ! 响应式屏幕精度调整(时机: 页面完全渲染完后)
      this.calculationAccuracy();
    }, 2000);
  },
});

(2) 多个延时 等待更新

Page({
  onReady() {
    // ! 响应式屏幕精度调整(时机: 页面完全渲染完后)
    this.calculationAccuracy();
  },
  calculationAccuracy() {
    let query = wx.createSelectorQuery();
    const calc = () => {
      return new Promise((resolve, reject) => {
        // ! 取第一个 group 实际高度与理论高度获取精度差
        query
          .select(".page0_firstGroup")
          .boundingClientRect((rect) => {
            if (rect === null) {
              reject("首次获取dom失败");
            } else if (rect.height) {
              resolve(rect.height);
            }
          })
          .exec();
      });
    };
    const resetHeightInfo = (height) => {
      const default_firstGroupHeight = this.data.swiperList[0].info[0].height;
      let scale = toFixed(height / default_firstGroupHeight, 3);
      console.log("精度比例:", scale);
      const temp = deepCopy(this.data.swiperList);
      temp.forEach((v) => {
        v.info.forEach((m) => {
          m.height = Math.floor(m.height * scale);
        });
      });
      console.log("调整精度后设置 swiperList", temp);
      this.setData({
        scale: scale,
        swiperList: temp,
      });
    };
    setTimeout(() => {
      calc()
        .then((first_height) => {
          resetHeightInfo(first_height);
        })
        .catch((error) => {
          console.log("error", error, "尝试第二次调整精度");
          setTimeout(() => {
            calc()
              .then((second_height) => {
                resetHeightInfo(second_height);
              })
              .catch((err) => {
                console.log("error", error, "尝试第三次调整精度");
                setTimeout(() => {
                  calc()
                    .then((third_height) => {
                      resetHeightInfo(third_height);
                    })
                    .catch(() => {
                      throw new Error("三次调整精度失败!!!请过会儿再试");
                    });
                }, 2000);
              });
          }, 1500);
        });
    }, 1500);
  },
});

(3) 真正的更新时机

Page({
  data: {
    text: "This is page data.",
  },
  onLoad: function (options) {
    // 页面创建时执行*是在首次数据渲染之前执行),非阻断性逻辑尽量不要写在这里,会影响首屏数据渲染的时间
  },
  onReady: function () {
    // 页面首次渲染完毕时执行,一个页面只会调用一次,代表页面已经准备妥当(可以操作 Dom)
    // ! 如果需要获取 ajax 动态渲染的 dom 元素,可以在获取数据后设置 setData 时的回调中获取
  },
  clearnData(data) {
    this.setData(
      {
        swiperList: objList,
        currentType: objList[0].key,
      },
      () => {
        // ! 响应式屏幕精度调整(时机: 页面完全渲染完后)
        this.calculationAccuracy();
      }
    );
  },
  calculationAccuracy() {
    // 可以同步计算精度差
  },
});

5. 直接计算 dom 高度

Page({
  clearnData(data) {
    this.setData(
      {
        swiperList: objList,
        currentType: objList[0].key,
      },
      () => {
        this.setRelDomHeight();
      }
    );
  },
  setRelDomHeight() {
    // 获取真实 dom 高度后设置 swiperList
    const temp = deepCopy(this.data.swiperList);
    temp.forEach((v) => {
      v.info.forEach(async (m) => {
        m.height = await this.getGroupHeight2(m.key);
      });
    });
    this.setData({
      swiperList: temp,
    });
  },

  // 单个 dom 高度获取 by id
  getGroupHeight2(id) {
    let query = wx.createSelectorQuery();
    return new Promise((resolve, reject) => {
      query
        .select(`#${id}`)
        .boundingClientRect((rect) => {
          try {
            let height = rect.height;
            resolve(height);
          } catch (error) {
            reject("获取 dom #" + id + "失败");
          }
        })
        .exec();
    });
  },
});

性能测试优化

存在问题

  1. 存在渲染界面的耗时过长的情况;

渲染界面的耗时过长会让用户觉得卡顿,体验较差,出现这一情况时,需要校验下是否同时渲染的区域太大 ---pages/productList/productList

解决方案:页面数据过大,一次渲染耗费时长,按模块分多次渲染,加入骨架屏

  1. 存在 setData 的数据过大

由于小程序运行逻辑线程与渲染线程之上,setData 的调用会把数据从逻辑层传到渲染层,数据太大会增加通信时间
pages/productList/productList : swiperList、currentType,变量单次赋值 598k;

解决方案:产品列表页所有数据来源一个接口,数据过大,大量冗余字段;先是接口拆分,然后数据清洗,去除无用字段,减少数据大小,一般不超过 256k;

  1. 滚动区域没有开启惯性滚动

惯性滚动会使滚动比较顺畅,在安卓下默认有惯性滚动,而在 iOS 下需要额外设置 -webkit-overflow-scrolling: touch 的样式
pages/productList/productList

解决方案:使用 scroll-view 组件,添加-webkit-overflow-scrolling: touch 的样式

  1. 存在将未绑定在 WXML 的变量传入 setData

setData 操作会引起框架处理一些渲染界面相关的工作,一个未绑定的变量意味着与界面渲染无关,传入 setData 会造成不必要的性能消耗

解决方案:按要求处理,减少 setData 的调用 (可以使用 this.data 存储不需要在 wxml 中展示的变量)

  1. 存在短时间内发起太多的图片请求

短时间内发起太多图片请求会触发浏览器并行加载的限制,可能导致图片加载慢,用户一直处理等待。应该合理控制数量,可考虑使用雪碧图技术、拆分域名或在屏幕外的图片使用懒加载

解决方案:懒加载需要监听滚动的高度,计算当前 dom 的高度,调用 setData 改变图片的显隐状态,会增加另外性能损失,再考虑...

  1. 存在 setData 的调用过于频繁

setData 接口的调用涉及逻辑层与渲染层间的线程通过,通信过于频繁可能导致处理队列阻塞,界面渲染不及时而导致卡顿,应避免无用的频繁调用
pages/home/home:onPageScroll 方法 38 次/秒,touchEnd 方法 26 次/秒

解决方案:滚动监听处理数据,使用节流处理;页面其他多次调用,减少非必要的调用,非数据绑定的使用常规赋值方法;

Page({
  data: {
    swiperList: [], // 新的数据容器,开关 isRender 来控制渲染
    currentType: "all", // 轮播大 标签
    currentTypeIndex: 0, //当前轮播图的第几页
    domHeightList: [], // 单独维护所有高度
    isClickLabel: false,
  },
  clearnData(data) {
    this.setData(
      {
        swiperList: objList,
        currentType: objList[0].key,
        "swiperList[0].isRender": true,
      },
      () => {
        this.setRelDomHeight(0);
      }
    );
  },
  // 设置 dom 高度
  setRelDomHeight(index) {
    let arr = [];
    this.data.swiperList[index].info.forEach(async (v) => {
      let height = await this.getGroupHeight2(v.moduleKey);
      if (height) {
        if (arr.length == 0) {
          arr.push(height);
        } else {
          arr.push(arr[arr.length - 1] + height);
        }
      }
    });
    this.data.domHeightList[index] = arr;
  },
  //切换标签
  changeLabel(e) {
    let label = e.currentTarget.dataset.label;
    let param = "swiperList[" + this.data.currentTypeIndex + "].currentLabel";
    this.setData({
      [param]: label,
      toView: label,
    });
    this.data.isClickLabel = true;
  },
  //切换类型
  changeType(e) {
    let type = e.currentTarget.dataset.type;
    let index = this.data.swiperList.findIndex((item) => item.key === type);
    let isRender = "swiperList[" + index + "].isRender";
    if (this.data.swiperList[index].isRender) {
      this.setData({
        currentType: type,
        currentTypeIndex: index,
      });
    } else {
      this.setData(
        {
          currentType: type,
          currentTypeIndex: index,
          [isRender]: true,
        },
        () => {
          this.setRelDomHeight(index);
        }
      );
    }
  },
  // 轮播图切换
  switchType(e) {
    var source = e.detail.source;
    // FIX 轮播切换 bug
    if (source === "autoplay" || source === "touch") {
      let current = e.detail.current;
      let type = this.data.swiperList[current].key;
      let isRender = "swiperList[" + current + "].isRender";
      if (this.data.swiperList[current].isRender) {
        this.setData({
          currentType: type,
          currentTypeIndex: current,
        });
      } else {
        this.setData(
          {
            currentType: type,
            currentTypeIndex: current,
            [isRender]: true,
          },
          () => {
            this.setRelDomHeight(current);
          }
        );
      }
    }
  },
  //  滚动监听 scroll-view 回显 label
  listenScroll: debounce(function (e) {
    this.scrollFn(e);
  }, 50),
  scrollFn(e) {
    const { isClickLabel, swiperList, currentTypeIndex } = this.data;
    // 点击 label 不需要加入滚动监听
    if (isClickLabel) {
      this.data.isClickLabel = false;
      return;
    }
    const infoList = swiperList[currentTypeIndex].info;
    let heightMapIndex = this.data.domHeightList[currentTypeIndex].findIndex(
      (v) => v > e.detail.scrollTop
    );
    // ! 设置 page.currentLabel && toView
    if (heightMapIndex === -1) {
      heightMapIndex = this.data.domHeightList[currentTypeIndex].length - 1;
    }
    let param = "swiperList[" + this.data.currentTypeIndex + "].currentLabel";
    let currentLabel = this.data[param];
    let infoKey = infoList[heightMapIndex].moduleKey;
    let currentIndex = infoList.findIndex((v) => v.moduleKey === currentLabel);
    if (currentIndex !== heightMapIndex) {
      this.setData({
        [param]: infoKey,
      });
    }
  },
  // 跳转到搜索页
  toSearch() {
    wx.navigateTo({
      url: "/pages/search/search",
    });
  },
});

拓展

参考