问题回溯
前些时候,我们的一码游小程序地图组件一下子渲染了几百个maker,加载特别慢,也特卡,如下图所示:
看着地图上maker一大堆,感觉就很扎心,让人头皮发麻。
产品让我优化一下,她建议一开始只加载50条。
解决方案
那如何保证既能加载maker不卡顿,又能保证我们需要的数据完整不丢失呢?
参考线上他人的想法,我和后端还有产品商量了下,最终解决方案如下:
控制地图可见区域大小,只获取地图当前区域范围内的maker,操作移动地图重新请求并加载maker。
看到这里的小伙伴可能会吐槽,怎么不弄成下图,类似百度那样的搜索效果呢?
百度地图这效果,是随着地图视野发生变化,视野范围较大,一个小区域范围maker太多,在一个maker加数字表示,maker不用都显示出来,视野范围较小,数据不多的时候,则显示maker。
其实我们也想这样做,但是这样的效果,目前后端的数据结构并不支持,主要开发时间也有限,不允许我们这样做,只能退而求其次,用上面提到的方案了。
具体步骤
从微信官方文档小程序( 地图map )查看API,我们可以发现,
- 控制地图可见区域大小,可以使用 min-scale、max-scale、scale;
- 操作(移动,缩放)地图,可以使用 bindregionchange 。
scale可以控制当前地图缩放级别,min-scale、max-scale分别控制了地图最小和最大缩放级别,bindregionchange 在视野发生变化时触发,regionchange事件返回值对象中正好包含了当前地图区域位置中心点的经纬度、以及东北角和西南角位置的经纬度信息,如下:
如此,后端配合修改数据接口,前端将当前地图中心位置经纬度、以及东北角、西南角位置的经纬度信息传递给后端接口,让后端只查询当前的数据,前端再控制好缩放级别,选择一个比较合适的min-scale值,这样就达到了我们最初的预想。
关键部分代码:
<div class="app-wrap">
<div class="item" v-for="(item, i) in appList" :key="i" @click="appClick(i, item, true)">
<image class="icon" :src="$imgBaseUrl + item.icon" lazy-load mode="aspectFill"></image>
<div class="name">{{ item.name }}<div class="name-en">{{ $i18n_en(item.name) }}</div>
</div>
</div>
</div>
<map
id="map"
ref="map"
:markers="markers"
:polyline="polyline"
style="width: 100%; height: 100%;"
:latitude="latitude"
:longitude="longitude"
:scale="scale"
:min-scale="minScale"
:max-scale="maxScale"
:show-location="showLocation"
@markertap="markertap"
@labeltap="markertap"
@regionchange="onMapRegionchange"
>
<cover-view slot="callout">
<cover-view class="c-callout" v-for="(item, i) in markers" :marker-id="i" :key="i">
{{ i + 1 }}
</cover-view>
</cover-view>
</map>
appList: [
{
name: '景区景点',
icon: `/tour/icon-spot.png`,
value: 'scenic',
scale: 15,
noImgUrl: process.env.VUE_APP_IMG_URL + '/playToolsPKG/pic-05.png',
getListFunc: () => this.footerClick('景区景点'),
goDetailFunc: this.goScenicDetail
},
{
name: '酒店民宿',
icon: `/tour/icon-hotel.png`,
value: '',
maxScale: maxScaleValue,
minScale: 17,
scale: 18,
latitude: 37.211836,
longitude: 112.180908,
noImgUrl: process.env.VUE_APP_IMG_URL + '/playToolsPKG/pic-09.png',
getListFunc: this.getHotelList,
goDetailFunc: this.goHotelDetail
},
{
name: '餐饮',
icon: `/tour/icon-food.png`,
value: '',
maxScale: maxScaleValue,
minScale: 17,
scale: 18,
latitude: 37.203548,
longitude: 112.185848,
noImgUrl: process.env.VUE_APP_IMG_URL + '/playToolsPKG/pic-09.png',
getListFunc: this.getFoodList,
goDetailFunc: this.goFoodDetail
},
]
// 顶部app
appClick(index, item) {
this.popupTitle = item.name
this.scrollTop = 0;
this.popupList = [];
this.markers = [];
this.polyline = [];
this.currentItemName = item.name
this.pageNo = 1
this.noImgUrl = item.noImgUrl
this.appItem = item
this.minScale = item.minScale ?? minScaleValue
this.maxScale = item.maxScale ?? maxScaleValue
this.nePoint = undefined
this.swPoint = undefined
this.showFloatMenu = false
item.getListFunc()
},
onMapRegionchange(evt) {
if (evt.type != 'end') {
return
}
// console.log('---onMapRegionchange---end---', evt)
const { centerLocation, region } = evt.detail
this.currentLatitude = centerLocation.latitude
this.currentLongitude = centerLocation.longitude
this.nePoint = region.northeast
this.swPoint = region.southwest
if (this.nePoint && this.swPoint) {
if (["酒店民宿", "餐饮"].includes(this.appItem.name)) {
this.appItem.getListFunc && this.appItem.getListFunc()
}
}
},
在regionchange事件中,开始和结束状态都会触发,如下方官方文档所述:
在目前的场景中,我们并不关心开始状态,因此,我在代码中阻断了开始状态
if (evt.type != 'end') {
return
}
其他问题
其实到这里,针对渲染多个maker卡顿的问题,我们基本算是解决了。 可作为一个负责任的开发者,我们不能只为完成任务,不管质量。
上方的解决方案,开发人员还能明显发现存在下述问题:
在小程序顶部点击类型事件中,特别是地图的Regionchange事件,会频繁多次触发,导致后端接口多次请求。
如何解决 Regionchange 事件多次触发导致后端接口多次请求呢?
我们可以采用防抖处理,uniapp中已经提供此方法 uni.$u.debounce
onMapRegionchange(evt) {
if (evt.type != 'end') {
return
}
const func = () => {
console.log('---onMapRegionchange---end---', evt)
const { centerLocation, region } = evt.detail
this.currentLatitude = centerLocation.latitude
this.currentLongitude = centerLocation.longitude
this.nePoint = region.northeast
this.swPoint = region.southwest
if (this.nePoint && this.swPoint) {
if (["酒店民宿", "餐饮"].includes(this.appItem.name)) {
this.appItem.getListFunc && this.appItem.getListFunc()
}
}
}
uni.$u.debounce(func, 360)
},
debounce函数中的时间我设置为360毫秒,稍微有点大,太小了,我发现控制不住,更大一些,等待时间过长,感觉体验又不好。
到这里,其实 getListFunc 还是会执行两次,一次是在上方点击事件(appClick )执行时,另一次则在Regionchange事件中了,我觉得还可以继续优化。当然下面讲到的优化过程已经和我们这次的主题无关了,不想看的请直接忽略。
回到接口请求两次的问题,我发现第一次请求后端接口的时候,当时并没有获取到当前地图的中心位置和东北角、西南角位置信息,所以我在调用后端接口的方法前,增加了以下代码
if(!this.nePoint){
return
}
由于这个页面的特殊设计,每次在上方重新点击类型切换后,需要重新定位地图位置,而原来的同事(开发者)在请求后端接口获取数据后,才调用了地图定位方法,导致如果不调用一次后端接口,就无法进行定位,恰巧这样的后端请求接口数据根本就不是我现在所需要的(没有地图中心点位置和东北西南角位置数据来限定数据,请求后端返回的数据可能很多,导致卡顿),就譬如下方酒店民宿的请求方法:
getHotelList() {
this.markers = []
this.scale = this.appItem.scale
this.$http.post("/hotel/pageList", {
status: 1,
pageNo: this.pageNo,
pageSize: this.nePoint ? 99 : 50,
latitude: this.latitude,
longitude: this.longitude,
currentLatitude: this.currentLatitude,
currentLongitude: this.currentLongitude,
nePoint: this.nePoint,
swPoint: this.swPoint,
}).then(({ data }) => {
this.markers = data.records.map((item, index) => {
let gId = this.getId()
return {
// 保留自定义参数, 用于查询或其他操作
...item,
iconPath: item.subMchId ? `${this.$imgBaseUrl}/tour/icon-marker-hotel-bind.png` : `${this.$imgBaseUrl}/tour/icon-marker-hotel.png`,
id: gId, //保证有id, 点击时才能显示tip
uid: gId,
dataId: item.hotelId,
latitude: item.latitude,
longitude: item.longitude,
width: item.subMchId ? 28 : 25,
height: 28,
label: {
content: item.hotelName,
borderRadius: 36,
borderWidth: 1,
borderColor: item.subMchId ? '#c69553' : '#999',
bgColor: item.subMchId ? '#fefefc' : '#fff',
color: item.subMchId ? '#c59350' : '#363636',
display: 'ALWAYS',
textAlign: 'center',
padding: 5,
},
callout: {
content: item.hotelName,
padding: 5,
display: 'BYCLICK'
}
}
})
this.remakeList(this.markers, {
title: 'hotelName',
address: 'address',
coverUrl: 'coverPath',
phone: 'telephone',
imgsUrl: 'detailPath'
}, data.total)
this.scale = this.appItem.scale
this.goToCenter({
latitude: this.appItem.latitude,
longitude: this.appItem.longitude
})
})
},
基于上面提到的问题,我将此方法修改如下:
getHotelList() {
this.goToCenter({
latitude: this.appItem.latitude,
longitude: this.appItem.longitude
})
if(!this.nePoint){
return
}
this.markers = []
this.$http.post("/hotel/pageList", {
status: 1,
pageNo: this.pageNo,
pageSize: this.nePoint ? 99 : 50,
latitude: this.latitude,
longitude: this.longitude,
currentLatitude: this.currentLatitude,
currentLongitude: this.currentLongitude,
nePoint: this.nePoint,
swPoint: this.swPoint,
}).then(({ data }) => {
this.markers = data.records.map((item, index) => {
let gId = this.getId()
return {
// 保留自定义参数, 用于查询或其他操作
...item,
iconPath: item.subMchId ? `${this.$imgBaseUrl}/tour/icon-marker-hotel-bind.png` : `${this.$imgBaseUrl}/tour/icon-marker-hotel.png`,
id: gId, //保证有id, 点击时才能显示tip
uid: gId,
dataId: item.hotelId,
latitude: item.latitude,
longitude: item.longitude,
width: item.subMchId ? 28 : 25,
height: 28,
label: {
content: item.hotelName,
borderRadius: 36,
borderWidth: 1,
borderColor: item.subMchId ? '#c69553' : '#999',
bgColor: item.subMchId ? '#fefefc' : '#fff',
color: item.subMchId ? '#c59350' : '#363636',
display: 'ALWAYS',
textAlign: 'center',
padding: 5,
},
callout: {
content: item.hotelName,
padding: 5,
display: 'BYCLICK'
}
}
})
this.remakeList(this.markers, {
title: 'hotelName',
address: 'address',
coverUrl: 'coverPath',
phone: 'telephone',
imgsUrl: 'detailPath'
}, data.total)
})
},
使用!this.nePoint
在后端接口请求前加了阻断,并且将地图定位方法goToCenter提到最前面,执行后端接口请求前先地图定位。
如此,每次在顶部切换类型请求数据的时候(上面提到的事件方法appClick),就能保证只执行1次,又能正确获取到地图视野范围内的数据了。
代码修改前后对比图:
其他类型,比如餐饮,改法跟酒店民宿相同。就不一一介绍了。
在编辑本文档的时候,结果无意中又发现了一个问题,在顶部点击切换类型的过程中(事件方法见开头的方法appClick),点击当前的类型,maker会消失。 可以加一个逻辑,判断是否当前,是不再执行下方的逻辑。修改如下:
<div class="app-wrap">
<div class="item" v-for="(item, i) in appList" :key="i" @click="appClick(i, item,true)">
<image class="icon" :src="$imgBaseUrl + item.icon" lazy-load mode="aspectFill"></image>
<div class="name">{{ item.name }}<div class="name-en">{{ $i18n_en(item.name) }}</div>
</div>
</div>
</div>
// 顶部app
appClick(index, item, isTop=false) {
if(this.currentIndex === index && isTop){
return
}
isTop ? this.currentIndex=index : undefined;
this.popupTitle = item.name
this.scrollTop = 0;
this.popupList = [];
this.markers = [];
this.polyline = [];
this.currentItemName = item.name
this.pageNo = 1
this.noImgUrl = item.noImgUrl
this.appItem = item
this.minScale = item.minScale ?? minScaleValue
this.maxScale = item.maxScale ?? maxScaleValue
this.scale = item.scale?? 14
this.nePoint = undefined
this.swPoint = undefined
this.showFloatMenu = false
item.getListFunc()
},
终于,一切OK了。