Open yanyue404 opened 3 years ago
需求描述
小程序产品列表和 APP 接口统一,实现产品轮播页单页楼梯导航效果
效果预览
App 与 小程序产品页交互效果查看
// 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, } ); });
点击上面的 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); ... }, });
先动态计算再进行精度调整
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(); }, });
(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() { // 可以同步计算精度差 }, });
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(); }); }, });
渲染界面的耗时过长会让用户觉得卡顿,体验较差,出现这一情况时,需要校验下是否同时渲染的区域太大 ---pages/productList/productList
解决方案:页面数据过大,一次渲染耗费时长,按模块分多次渲染,加入骨架屏
由于小程序运行逻辑线程与渲染线程之上,setData 的调用会把数据从逻辑层传到渲染层,数据太大会增加通信时间 pages/productList/productList : swiperList、currentType,变量单次赋值 598k;
解决方案:产品列表页所有数据来源一个接口,数据过大,大量冗余字段;先是接口拆分,然后数据清洗,去除无用字段,减少数据大小,一般不超过 256k;
惯性滚动会使滚动比较顺畅,在安卓下默认有惯性滚动,而在 iOS 下需要额外设置 -webkit-overflow-scrolling: touch 的样式 pages/productList/productList
-webkit-overflow-scrolling: touch
解决方案:使用 scroll-view 组件,添加-webkit-overflow-scrolling: touch 的样式
setData 操作会引起框架处理一些渲染界面相关的工作,一个未绑定的变量意味着与界面渲染无关,传入 setData 会造成不必要的性能消耗
解决方案:按要求处理,减少 setData 的调用 (可以使用 this.data 存储不需要在 wxml 中展示的变量)
短时间内发起太多图片请求会触发浏览器并行加载的限制,可能导致图片加载慢,用户一直处理等待。应该合理控制数量,可考虑使用雪碧图技术、拆分域名或在屏幕外的图片使用懒加载
解决方案:懒加载需要监听滚动的高度,计算当前 dom 的高度,调用 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", }); }, });
前言
需求描述
小程序产品列表和 APP 接口统一,实现产品轮播页单页楼梯导航效果
效果预览
App 与 小程序产品页交互效果查看
实现思路
1. 改造数据
2. 基本实现:scroll-into-view + 滚动监听
点击上面的 label 已经可以正确定位了,如何滚动监听回显?
需要的是详细的高度信息。
3. 如何获取集合所有分组的高度?
先动态计算再进行精度调整
4. 什么时机去更新
(1) 延时 seTimeout
(2) 多个延时 等待更新
(3) 真正的更新时机
5. 直接计算 dom 高度
性能测试优化
存在问题
渲染界面的耗时过长会让用户觉得卡顿,体验较差,出现这一情况时,需要校验下是否同时渲染的区域太大 ---pages/productList/productList
解决方案:页面数据过大,一次渲染耗费时长,按模块分多次渲染,加入骨架屏
由于小程序运行逻辑线程与渲染线程之上,setData 的调用会把数据从逻辑层传到渲染层,数据太大会增加通信时间 pages/productList/productList : swiperList、currentType,变量单次赋值 598k;
解决方案:产品列表页所有数据来源一个接口,数据过大,大量冗余字段;先是接口拆分,然后数据清洗,去除无用字段,减少数据大小,一般不超过 256k;
惯性滚动会使滚动比较顺畅,在安卓下默认有惯性滚动,而在 iOS 下需要额外设置
-webkit-overflow-scrolling: touch
的样式 pages/productList/productList解决方案:使用 scroll-view 组件,添加
-webkit-overflow-scrolling: touch
的样式setData 操作会引起框架处理一些渲染界面相关的工作,一个未绑定的变量意味着与界面渲染无关,传入 setData 会造成不必要的性能消耗
解决方案:按要求处理,减少 setData 的调用 (可以使用 this.data 存储不需要在 wxml 中展示的变量)
短时间内发起太多图片请求会触发浏览器并行加载的限制,可能导致图片加载慢,用户一直处理等待。应该合理控制数量,可考虑使用雪碧图技术、拆分域名或在屏幕外的图片使用懒加载
解决方案:懒加载需要监听滚动的高度,计算当前 dom 的高度,调用 setData 改变图片的显隐状态,会增加另外性能损失,再考虑...
setData 接口的调用涉及逻辑层与渲染层间的线程通过,通信过于频繁可能导致处理队列阻塞,界面渲染不及时而导致卡顿,应避免无用的频繁调用 pages/home/home:onPageScroll 方法 38 次/秒,touchEnd 方法 26 次/秒
解决方案:滚动监听处理数据,使用节流处理;页面其他多次调用,减少非必要的调用,非数据绑定的使用常规赋值方法;
拓展
参考