微信小程序map组件渲染几百个marker后,页面卡顿,如何解决?

0 阅读6分钟

问题回溯

前些时候,我们的一码游小程序地图组件一下子渲染了几百个maker,加载特别慢,也特卡,如下图所示:

1743413008122.png

看着地图上maker一大堆,感觉就很扎心,让人头皮发麻。

产品让我优化一下,她建议一开始只加载50条。

解决方案

那如何保证既能加载maker不卡顿,又能保证我们需要的数据完整不丢失呢?

参考线上他人的想法,我和后端还有产品商量了下,最终解决方案如下:

控制地图可见区域大小,只获取地图当前区域范围内的maker,操作移动地图重新请求并加载maker。

看到这里的小伙伴可能会吐槽,怎么不弄成下图,类似百度那样的搜索效果呢?

1743475095474.png 百度地图这效果,是随着地图视野发生变化,视野范围较大,一个小区域范围maker太多,在一个maker加数字表示,maker不用都显示出来,视野范围较小,数据不多的时候,则显示maker。

其实我们也想这样做,但是这样的效果,目前后端的数据结构并不支持,主要开发时间也有限,不允许我们这样做,只能退而求其次,用上面提到的方案了。

具体步骤

从微信官方文档小程序( 地图map )查看API,我们可以发现,

  • 控制地图可见区域大小,可以使用 min-scale、max-scale、scale;
  • 操作(移动,缩放)地图,可以使用 bindregionchange 。

scale可以控制当前地图缩放级别,min-scale、max-scale分别控制了地图最小和最大缩放级别,bindregionchange 在视野发生变化时触发,regionchange事件返回值对象中正好包含了当前地图区域位置中心点的经纬度、以及东北角和西南角位置的经纬度信息,如下:

1743474388750.png

如此,后端配合修改数据接口,前端将当前地图中心位置经纬度、以及东北角、西南角位置的经纬度信息传递给后端接口,让后端只查询当前的数据,前端再控制好缩放级别,选择一个比较合适的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事件中,开始和结束状态都会触发,如下方官方文档所述:

1743478379434.png 在目前的场景中,我们并不关心开始状态,因此,我在代码中阻断了开始状态

if (evt.type != 'end') {
        return
}

其他问题

其实到这里,针对渲染多个maker卡顿的问题,我们基本算是解决了。 可作为一个负责任的开发者,我们不能只为完成任务,不管质量。

上方的解决方案,开发人员还能明显发现存在下述问题:

在小程序顶部点击类型事件中,特别是地图的Regionchange事件,会频繁多次触发,导致后端接口多次请求。

1743477316121.png

1743477358887.png

如何解决 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)
},

1743477932408.png

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次,又能正确获取到地图视野范围内的数据了。

代码修改前后对比图: 1743494353942.png

其他类型,比如餐饮,改法跟酒店民宿相同。就不一一介绍了。

在编辑本文档的时候,结果无意中又发现了一个问题,在顶部点击切换类型的过程中(事件方法见开头的方法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了。