Mapbox GL JS — 矢量瓦片 + WebGL 客户端渲染地图
是什么
Mapbox GL JS 是 Mapbox 公司 2014 年发起的 Web 地图渲染库。日常类比:传统在线地图像翻一本预先印好的相册——服务器把每一格地图拍成一张 PNG(raster tile),你拖动时就翻到下一张照片;Mapbox GL JS 换了套路,发给你的不是照片,而是”街道这条线从 (10,20) 到 (30,40)、是高速公路、限速 80”这种几何数据(vector tile),浏览器拿到后用 WebGL 现场画。结果是同一份瓦片可以缩放、旋转、倾斜成 3D,文字始终保持清晰,配色甚至可以运行时换。
最小例子:
import mapboxgl from 'mapbox-gl';
const map = new mapboxgl.Map({ container: 'map', style: 'mapbox://styles/mapbox/streets-v12', center: [121.47, 31.23], // 上海经纬度 zoom: 11, pitch: 45, // 倾斜 45 度看 3D});style 是一份 JSON(Style Spec),描述”哪些数据源 + 怎么画”;center/zoom/pitch 决定相机视角。整个交互(拖、转、缩、倾)都在客户端 GPU 算,不再回服务端。
注意许可:v1.x 是 BSD-3 开源,v2.0(2020-12)后改成 Mapbox TOS 专有,需 API token;社区从 v1.13 fork 出 MapLibre GL JS,API 几乎一致、继续 BSD-3 开源。下文讲的是两边共有的核心思想。
为什么重要
不理解矢量瓦片 + WebGL 渲染这套思路,下面这些事都没法解释:
- 为什么现代地图(Mapbox / 高德 H5 / Google Maps WebGL 版)能丝滑旋转、3D 倾斜、夜间一键切色——预渲染 PNG 做不到这些
- 为什么 [[deck.gl]] / Kepler.gl / d3 地理可视化都把 Mapbox/MapLibre 当底图层——它定义了 Web 矢量地图的事实接口
- 为什么 Style Spec 这份 JSON 协议被整个生态继承(MapLibre / Tangram / OpenMapTiles)——声明式样式让”换皮肤”变成换 JSON
- 为什么 GIS 行业 2015 年后大量从 Leaflet 迁过来——Leaflet 是 raster 时代的王者,做不了 3D / 旋转 / 数据驱动样式
- 为什么 postgis / tippecanoe 这类后端工具流行起来——它们负责把原始 GeoJSON 切成多 zoom 的 vector tile
核心要点
Mapbox GL JS 是四层架构,看懂这四层基本上看懂整个矢量地图栈:
- Source(数据源):从哪里拿数据。
vector/raster/raster-dem(地形高度)/geojson/image/video。vector source 拉.mvt/.pbf(Protocol Buffers 编码的几何) - Tile(瓦片)单元:z/x/y 三个数索引——zoom 0 全世界 1 张,zoom 22 厘米级。Web Mercator (EPSG:3857) 投影把球面拍成正方形,便于一切二
- Layer(图层)+ Style:每个 layer 声明”用哪个 source、画成什么样”。layer 类型有 fill / line / symbol(文字+图标)/ circle / heatmap / raster / fill-extrusion(3D 楼)。paint/layout 字段用 Expression DSL 写”数据驱动样式”
- Painter(WebGL 调度器):每帧按 layer 顺序调 GPU 着色器,把 tile 几何画到 canvas
线程模型同样关键:
- 主线程:相机控制、事件分发、WebGL draw call
- Worker 线程:tile 下载后的解析、几何简化、空间索引(rbush)、文字 shaping——重活全在这里,不卡帧
Style Spec 的 Expression DSL 是”数据驱动”的核心:
"circle-radius": [ "interpolate", ["linear"], ["zoom"], 5, ["*", ["get", "population"], 0.0001], 15, ["*", ["get", "population"], 0.001]]读法:根据 zoom 在 5 和 15 之间线性插值,半径从 population × 0.0001 渐变到 × 0.001。整套 DSL 让”圆点大小随人口和缩放级别变”这种逻辑写在 JSON 里、运行时即可生效,不用重切瓦片。
实践案例
案例 1:加一个 GeoJSON 图层 + 点击交互
map.on('load', () => { map.addSource('shops', { type: 'geojson', data: '/data/shops.geojson' }); map.addLayer({ id: 'shops-circle', type: 'circle', source: 'shops', paint: { 'circle-radius': 6, 'circle-color': ['match', ['get', 'category'], 'cafe', '#e74c3c', 'bar', '#3498db', '#999'] } }); map.on('click', 'shops-circle', (e) => { const f = e.features[0]; new mapboxgl.Popup() .setLngLat(f.geometry.coordinates) .setHTML(f.properties.name) .addTo(map); });});addSource + addLayer 是动态加图层的唯一入口;match 表达式按属性切色——同一份 GeoJSON 可以画无数种不同样式。
案例 2:3D 楼层 + 地形阴影
map.addLayer({ id: '3d-buildings', source: 'composite', 'source-layer': 'building', type: 'fill-extrusion', minzoom: 14, paint: { 'fill-extrusion-height': ['get', 'height'], 'fill-extrusion-base': ['get', 'min_height'], 'fill-extrusion-color': '#aaa', 'fill-extrusion-opacity': 0.8 }});map.setTerrain({ source: 'mapbox-dem', exaggeration: 1.3 });fill-extrusion 用属性里的高度把 2D 多边形拉成 3D 体;setTerrain 让整张地图按 DEM(数字高程模型)起伏——这是 raster tile 时代彻底做不到的事。
案例 3:自己切 vector tile
把 100 万个商铺 GeoJSON 变成可缩放的矢量瓦片:
tippecanoe -o shops.mbtiles \ --maximum-zoom=14 --minimum-zoom=4 \ --drop-densest-as-needed shops.geojsontippecanoe 是 Mapbox 出的 CLI,自动选每个 zoom 显示哪些点(高 zoom 全显,低 zoom 抽稀),输出 .mbtiles(SQLite 包了一堆 .pbf)。配 tileserver-gl 起一个本地 tile 服务,前端就能直接 type: 'vector', tiles: ['http://...'] 接上。
踩过的坑
- token 与许可:v2+ 不挂
accessToken直接黑屏;公司项目想绕这个就上 MapLibre GL JS(fork、API 99% 兼容、零 token) - WebGL context lost 后没自处理:长时间隐藏 tab 或 GPU 切换时,浏览器会回收 context,需要监听
webglcontextlost后调map.remove()重建 - layer 顺序就是绘制顺序:水面图层加在道路之后会盖住道路;记住”先地、再水、再路、再楼、最上是文字”
- map.queryRenderedFeatures 只查可见瓦片:缩出去的对象查不到;要全量查得用 source 自己的索引或后端
- Worker 数量默认 = CPU 核数:移动端电池吃紧,可设
mapboxgl.workerCount = 2限制 - GeoJSON source 巨大时主线程卡:超过几 MB 就该走 tippecanoe 切成 vector tile,别让浏览器解析整份 JSON
- symbol layer 文字重叠:默认会自动隐藏冲突文字(collision detection),想强制全显式
text-allow-overlap: true,但密集场景视觉灾难
适用 vs 不适用场景
适用:
- 城市级、国家级交互地图(拖、转、缩、3D 倾斜)
- 数据驱动的地理可视化([[deck.gl]] / Kepler.gl 当叠加层)
- 需要运行时换皮肤、白天黑夜模式、品牌定制
- 移动端 WebView(Mapbox 也有同协议的 iOS/Android SDK)
不适用:
- 纯静态出图、印刷品 → matplotlib / d3 投影
- 极简需求 + 想免费 + 不需要 3D → Leaflet + OSM raster tile 更轻
- 完整 3D 球体(行星/卫星视角)→ Cesium 更专业
- 离线优先、弱网 → 需自己缓存 tile,方案复杂;或选 MBTiles + 原生 SDK
学到什么
- 数据 vs 像素的分层:raster 时代服务端把”什么+怎么画”一起决定(出 PNG),vector 时代把”什么”(几何 + 属性)和”怎么画”(style)拆开——这是声明式渲染思想在地图上的体现,和 react / vega-lite 同一类
- 客户端做重计算的边界:把 tile 解析、几何简化、文字 shaping 推到 Worker 线程是关键设计;主线程只剩相机和 draw call——这是 Web 端把 GPU 用满的标准做法
- JSON 协议吃下整个生态:Style Spec 这份 JSON 标准让 MapLibre / Tangram / OpenMapTiles 可以共用同一份样式文件——协议本身比代码值钱
延伸阅读
- 官方 Style Spec:Mapbox Style Specification(一切样式查这里)
- 开源 fork:MapLibre GL JS(v2 转闭源后的社区延续,BSD-3)
- vector tile 规范:Mapbox Vector Tile Specification(.mvt 的 Protocol Buffers 协议,行业事实标准)
- 切瓦片工具:tippecanoe(GeoJSON → MBTiles,处理亿点级输入)
- 教科书:[OpenLayers Cookbook] / [Volodymyr Agafonkin 在 Mapbox blog 的几何算法系列](rbush / 简化 / kd-tree 实现)
- [[deck.gl]] —— Uber 的可视化层,常叠在 Mapbox 底图上做大数据
- d3 —— 声明式图形库,地理投影模块可对照 Web Mercator