和后端一起遇到过的麻烦

本来是想写在上一篇里面的,但是上篇已经写得很长了…

而且想到未来可能会遇到更多要解决的问题。就重新记录了一篇文章

优化初始地图打开速度

数据量的问题通过分层解决,我们做了个全美人种分布的图层,首页显示了百万个点。这些点不仅影响了页面的打开速度,还对服务器造成了压力,同时浏览器渲染这些点也比较吃力

把后端做了多个 vector 源,前端写多个 layer,初始只展示数据量最小的源

接着把 script 标签从 api.mapbox.com 的 js 文件迁移到本地,从 network 里看到,mapbox-gl.js 这个文件的加载速度减少了好几秒

1
2
3
4
5
<!-- <script src='https://api.mapbox.com/mapbox-gl-js/v2.3.1/mapbox-gl.js'></script> -->
<script src="/js/plugins/mapbox-2.3.1/mapbox-gl.js"></script>
<!-- 搜索框 -->
<!-- <script src="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v4.7.2/mapbox-gl-geocoder.min.js"></script> -->
<script src="/js/plugins/mapbox-2.3.1/mapbox-gl-geocoder-4.7.2.min.js"></script>

图标卡顿重叠

另一个数据层有三十多万个数据点,并且用图标分类显示每个点。

1
2
3
4
5
6
7
8
9
10
11
12
// addLayer option
"type": "symbol",
"layout": {
"icon-image": "xxx-icon",
"icon-size": {
"stops": [
[5, 0.6],
[14, 0.8]
]
},
"icon-allow-overlap": true,
}

我加了icon-allow-overlap的选项,图标确实不重叠了,页面直接卡爆了…

经过测试,是"type": "symbol"渲染的问题,对比 circle,symble 会卡顿很多。

symble 适合稍微小一点的数据集,大量的数据会造成卡顿。

添加层控制的按钮

需求是点击按钮隐藏层,再次点击显示层,这个例子可以拿来参考

关键方法是

1
2
3
4
5
map.setLayoutProperty(
layerId,
"visibility",
"visible" // none
);

但是地图一个功能有多个层,我把层分类和层 id 写在了 button 元素上,点击按钮的时候先处理一下层的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!-- 四个按钮 -->
<a
class="active"
href="javascript:;"
layer="Ethnicity"
layer-id="Ethnicity_5000,Ethnicity_1000,Ethnicity_500,Ethnicity_200"
><i class="iconfont">&#xe686;</i><span>人种层开关</span></a
>
<a
class="active"
href="javascript:;"
layer="Colleges"
layer-id="Colleges,Colleges-count"
><i class="iconfont">&#xe672;</i><span>院校层开关</span></a
>
<a
class=""
href="javascript:;"
layer="Companies"
layer-id="Companies,Companies-count"
><i class="iconfont">&#xe671;</i><span>公司层开关</span></a
>
<a class="" href="javascript:;" layer="Crimes" layer-id="Crimes"
><i class="iconfont">&#xe873;</i><span>犯罪数据层开关</span></a
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
layerSwitch() {
const layerBtns = document.querySelectorAll('#menu a')
const map = this.map
layerBtns.forEach(link => {
link.addEventListener('click', function (e) {
e.preventDefault()
e.stopPropagation()
const layer = link.getAttribute('layer')
const ids = link.getAttribute('layer-id').split(',')
const legendsElement = document.querySelector(`.${layer}-legends`)
ids.forEach(name => {
const id = name.trim()
const visibility = map.getLayoutProperty(id, 'visibility')
if (visibility === 'visible' || visibility === undefined) {
map.setLayoutProperty(id, 'visibility', 'none')
this.className = ''
legendsElement && (legendsElement.style.display = 'none')
} else {
this.className = 'active'
map.setLayoutProperty(id, 'visibility', 'visible')
legendsElement && (legendsElement.style.display = 'flex')
}
})
})
})
}

地图对数据集大小的限制,导致数据集只能传递一个参数

这是后端遇到的问题,好像 vector 的数据集大小每超过一定限制,都会减少数据集中每个 feature 携带的属性。

传过来的数据集只有一个属性…

后端把需要展示的数据排列在字符串中,用特殊符号分割。

前端使用 mapbox 的表达式从这个字符串中根据特殊符号 split 出属性…这是一个不是办法的办法呢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 我们约定的格式是
data = 'id@param1@param2@...'
// 我需要根据id,把数据分类用不同颜色展示
// 用js切分应该是
data.split('@')[0]
// 但是表达式没有split方法,只有indexof和slice
['slice', ['get', 'data'], 0,['index-of', '@', ['get', 'data']]]
// 完整的匹配颜色表达式
'circle-color': [
'match',
['slice', ['get', 'data'], 0,['index-of', '@', ['get', 'data']]],
'2',
'#686de0',
'3',
'#535c68',
'4',
'#6ab04c',
'5',
'#eb4d4b',
'6',
'#be2edd',
'7',
'#f0932b',
'8',
'#f9ca24',
'9',
'#22a6b3',
'10',
'#f9ca24',
'#f9ca24',
],

鼠标滚轮放在 Popup 上会滚动页面

mapbox 地图中,滚轮有缩放的功能,但是在地图自带的弹出框上滚动鼠标滚轮,页面会滚动。这是非常不友好的交互…

这个还蛮简单的,禁用掉滚动就好啦

审查元素发现 popup 实际上是一个 html 弹出框,是 html 那就简单了。给地图容器元素添加点击事件,冒泡给 popup 包裹的元素,禁用掉滚轮事件

注意滚轮的事件是wheel

1
2
3
4
5
6
7
8
9
10
disablePopupScroll() {
const callback = (e) => {
if(e.target.closest('.mapboxgl-popup')) {
e.preventDefault()
return false
}
}
document.querySelector('#map').addEventListener('wheel', callback)
document.querySelector('#map').addEventListener('touchmove', callback)
}

根据接口数据,初始化隐藏某些点

getHidePoint是一个封装了 Ajax 的 promise

写一个 match 表达式,匹配数据给到的点,用filter或者icon-image隐藏掉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let hide = [];
const expression = ["match", ["get", "code"], "placeholder", ""];
getHidePoint()
.then((res) => {
hide = res;
Object.values(hide).forEach((item) => {
// 需要隐藏的code
expression.push(item.code, "");
// ['match', ['get', 'code'], 'placeholder', '', 'hide-code', '', 'hide-code2', '', ...]
map.addLayer({
// ...
layout: {
"icon-image": expression,
"icon-allow-overlap": true,
},
});
});
})
.catch((err) => console.log(err));