使用 Leaflet 加载 GeoJSON 数据时,通常会把整个 FeatureCollection 一次性渲染到地图容器中。但很多实际项目并不会让所有要素一直可见,而是需要根据用户选择的类型、行政区或输入的关键词实时调整显示内容。直接清空图层组再重新添加,虽然逻辑简单,却会触发完整的地图图层重建,数据量稍大就会出现卡顿和视觉闪烁。更合理的做法是保留原始要素引用,在图层组内部动态切换子图层的 addTo 和 removeFrom 状态,或者基于过滤条件重新生成新的 GeoJSON 图层。本文围绕图层组创建、属性过滤、文本搜索和范围过滤几个关键步骤,给出可直接运行的代码方案,并分析不同实现方式的性能差异。

一、创建包含原始要素的 GeoJSON 图层组
在实现动态过滤之前,先要确保一端持有原始 GeoJSON 数据,一端持有已经生成的 Leaflet 图层。原始数据用于后续条件判断,图层对象则负责地图展示。假设已经从接口或本地文件中拿到了名为 rawGeoJSON 的 FeatureCollection,下面代码将其构造成 L.geoJSON 实例,并放入一个 L.layerGroup 中统一管理。
// 假设 rawGeoJSON 是已经获取到的 FeatureCollection
var rawGeoJSON = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
properties: { name: '中央公园', category: 'park', color: '#2e7d32' },
geometry: { type: 'Polygon', coordinates: [[[116.3, 39.9], [116.4, 39.9], [116.4, 40.0], [116.3, 40.0]]] }
},
{
type: 'Feature',
properties: { name: '科技园区', category: 'tech', color: '#1565c0' },
geometry: { type: 'Polygon', coordinates: [[[116.5, 40.0], [116.6, 40.0], [116.6, 40.1], [116.5, 40.1]]] }
}
]
};
var geoLayer = L.geoJSON(rawGeoJSON, {
filter: function(feature) {
return true;
},
style: function(feature) {
return { color: feature.properties.color || '#3388ff' };
},
onEachFeature: function(feature, layer) {
layer.bindPopup(feature.properties.name || '未命名要素');
}
});
var layerGroup = L.layerGroup([geoLayer]).addTo(map);
上面代码中,L.geoJSON 的 filter 选项返回 true,表示初始加载全部要素;style 函数根据 properties 中的 color 字段设置填充或边框样式;onEachFeature 则为每个要素绑定一个弹出提示。生成后的 geoLayer 被添加到 layerGroup 里,再通过 addTo(map) 展示。layerGroup 除了便于统一控制,也能在后续遍历时提供一个明确的作用域。这里需要注意,L.geoJSON 生成的每个子图层都带有 feature 属性,指向原始 GeoJSON 要素,这为按属性判断显示与否提供了关键支撑。
如果数据量较大,建议不要把 rawGeoJSON 直接丢给 L.geoJSON 后就丢弃,而要在全局或闭包中保持引用。因为后续动态过滤可能会反复从原始数据中提取子集。一旦丢失引用,只能从已有图层的 feature 反向收集,虽然也能工作,但不如直接访问原始数据直观和可控。
二、按属性字段切换子图层显示状态
最简单直接的属性过滤方式,是遍历 layerGroup 中的每个子图层,读取其 feature.properties 中对应的字段值,与过滤条件比较,然后决定是否保留在地图上。Leaflet 提供了 eachLayer 方法,可以遍历图层组中的所有图层,而 map.hasLayer 可以判断某个图层当前是否已经添加到地图。下面的 filterByProperty 函数实现了按指定属性名和属性值进行筛选的逻辑。
function filterByProperty(propName, propValue) {
layerGroup.eachLayer(function(layer) {
var feature = layer.feature;
var matched = feature && feature.properties && feature.properties[propName] === propValue;
if (matched) {
if (!map.hasLayer(layer)) {
layer.addTo(map);
}
} else {
if (map.hasLayer(layer)) {
map.removeLayer(layer);
}
}
});
}
// 例如只显示 category 为 park 的要素
filterByProperty('category', 'park');
这种方法的优点是执行速度快、地图状态稳定,因为每个子图层对象被完整保留,只是调用了 addTo 或 removeFrom。但需要确保每个子图层的 feature 属性存在,并且 properties 结构一致。对于没有 feature 属性的图层(例如手动添加的 Marker 或 Polygon),需要在判断前先做空值保护,避免脚本报错。上面的代码通过 feature && feature.properties 进行了短路判断,已经覆盖了这种情况。
如果过滤条件非常复杂,或者希望每次都生成一个结构完全干净的新图层,也可以采用重建 GeoJSON 的方式。下面是基于原始数据重建过滤结果的示例。
function rebuildFilteredLayer(propName, propValue) {
var filteredData = {
type: 'FeatureCollection',
features: rawGeoJSON.features.filter(function(f) {
return f.properties && f.properties[propName] === propValue;
})
};
layerGroup.clearLayers();
var newLayer = L.geoJSON(filteredData, {
style: function(feature) {
return { color: feature.properties.color || '#3388ff' };
},
onEachFeature: function(feature, layer) {
layer.bindPopup(feature.properties.name || '未命名要素');
}
});
layerGroup.addLayer(newLayer);
}
重建方案的优势在于过滤后的数据集合非常纯粹,后续操作不会受到隐藏图层的干扰,而且可以顺便重新设置样式。但它每次都会销毁旧图层实例、创建新图层实例,并触发一次新的渲染周期。如果数据量只有几百条,这两种方式差异不大;如果 FeatureCollection 包含数千甚至数万个要素,频繁重建会导致明显卡顿。此时推荐使用第一种遍历切换的方式,并结合后文提到的节流手段来平滑交互。
三、结合搜索框与地图范围进行复合过滤
实际项目中往往不会只有一个固定的下拉筛选条件,还会包含文本搜索。比如用户输入地点名称,地图只显示名称中包含该关键词的要素。文本过滤同样可以基于遍历图层组实现,代码与属性过滤类似,只是匹配逻辑改为字符串的 indexOf 判断。
var searchInput = document.getElementById('search-input');
searchInput.addEventListener('input', function(e) {
var keyword = e.target.value.trim();
layerGroup.eachLayer(function(layer) {
var feature = layer.feature;
if (!feature || !feature.properties) return;
var name = feature.properties.name || '';
var matched = keyword === '' || name.indexOf(keyword) !== -1;
if (matched) {
if (!map.hasLayer(layer)) {
layer.addTo(map);
}
} else {
if (map.hasLayer(layer)) {
map.removeLayer(layer);
}
}
});
});
当输入框为空时,keyword 为空字符串,matched 条件会直接返回 true,从而恢复显示全部要素。代码中使用了 !== 和 ||,没有出现需要转义的特殊字符,但在复制到自己的项目时要注意保持逻辑顺序:先做空值判断再访问 properties,避免对 undefined 取值。
除了属性过滤,地图应用还经常需要根据当前可视范围过滤要素,例如只加载屏幕内的标记点。Leaflet 的 moveend 事件可以在地图停止移动后触发,此时用 getBounds 获取当前范围,再遍历图层组判断每个图层的位置是否在范围内。
map.on('moveend', function() {
var bounds = map.getBounds();
layerGroup.eachLayer(function(layer) {
var center;
if (typeof layer.getLatLng === 'function') {
center = layer.getLatLng();
} else if (typeof layer.getBounds === 'function') {
center = layer.getBounds().getCenter();
} else {
return;
}
var visible = bounds.contains(center);
if (visible) {
if (!map.hasLayer(layer)) {
layer.addTo(map);
}
} else {
if (map.hasLayer(layer)) {
map.removeLayer(layer);
}
}
});
});
这段代码先判断图层类型:点图层通常有 getLatLng 方法,面图层通过 getBounds 获取中心点。判断 bounds.contains(center) 可以粗略过滤掉完全在视野外的要素。需要注意的是,对面图层来说,用中心点判断可能会把部分面积在视野内的多边形误隐藏,如果需要精确判断,应该使用 bounds.intersects(layer.getBounds()) 而不是只比较中心点。此外,当地图缩放级别很低、要素数量很多时,moveend 事件触发的遍历开销会变大,可以配合 requestAnimationFrame 或 lodash 的 debounce 进行节流,减少不必要的计算。