我最早接触 Mapbox 的线样式时,第一反应是这玩意儿不就调个颜色和粗细吗?真正上手才知道,线条样式在 Mapbox GL 里对应的是一整套以line-开头的 paint 属性体系,从基础的线宽、颜色,到虚线、渐变、描边、偏移,再到数据驱动样式,组合起来能玩的花样非常多。如果你也在用 Mapbox 画路线、边界、等值线或者轨迹,这篇就把线条样式从配置到原理、从静态到动态完整拆开讲清楚,顺便把我踩过的坑也一并交代了。
先说下这篇内容的范围:以 Mapbox GL JS v2/v3 为主,涉及addLayer添加line类型图层、核心 paint 属性的作用机制、GeoJSON 数据格式要求、样式动态更新,最后是几个高频问题的排查过程。适合刚接触 Mapbox 的初学者,也适合已经能跑通基础地图、想在样式细节上做深的朋友。
1. 动手前的装备:Token 获取与首次初始化
很多人在 Mapbox 官网注册账号之后,卡在了第一步:到底去哪拿 token,拿到之后又该放哪。这个环节看似基础,实际上后面所有图层能否正常显示都跟它直接相关,所以先把这块踩实。
1.1 注册与 Token 获取的正确姿势
注册流程没什么特别的,去 Mapbox 官网用邮箱注册,完成邮箱验证后进入账户后台。注意一个细节:Mapbox 的 Access Token 分几种,包括默认的 public token(pk.开头)和 secret token(sk.开头)。前端地图渲染用的是 public token 就够了,secret token 千万别写进前端代码里,它泄露了相当于你的账户额度被人白嫖。
在后台的Account > Tokens页面可以创建新 token。创建时建议手动选择一个 scope 集合,至少要包含styles:read、fonts:read、tiles:read,这几个权限对应了加载样式、字体和瓦片的基础需求。Token 创建完成后有一串很长的字符串,就是类似pk.eyJ1Ijoi...的长文本,复制保存好。
1.2 初始化地图:Token 放哪才不会被坑
初始化地图时 token 的注入方式有两种:一种是在引入 Mapbox GL JS 的 script 标签之前,直接给mapboxgl.accessToken赋值;另一种是在new mapboxgl.Map()的构造参数里通过accessToken字段传入。推荐第一种,代码更清晰:
mapboxgl.accessToken = 'pk.eyJ1Ijoi...'; const map = new mapboxgl.Map({ container: 'map', style: 'mapbox://styles/mapbox/streets-v12', center: [116.4, 39.9], zoom: 10 });这里有个常见的坑:token 放到了 DOM 元素加载之前执行。如果你把mapboxgl.accessToken = '...'写在了#map这个 div 的前面,而且用了document.getElementById('map')去拿容器,大概率会报Container not found。反正我建议把地图初始化代码整体放在页面底部,或者用window.onload/DOMContentLoaded包起来,顺序问题就彻底没了。
初始化完成后,如果你能看到 Mapbox 默认的街道底图,说明环境已经通了。接下来所有线条相关操作,都建立在这张底图之上。
2. line 图层的骨架:Source 数据格式与 addLayer 配置
线条不是凭空画出来的,它必须先有数据源(source),再通过图层(layer)定义"怎么画"。Mapbox 的这套"数据与样式分离"设计,一开始可能会觉得绕,但理解了之后会发现它特别适合做动态换肤和交互高亮。
2.1 GeoJSON 里 LineString 的格式约束
Mapbox 中最常用的 source 类型是 GeoJSON,线条对应的几何类型是LineString或MultiLineString。一个最简单的线条数据长这样:
{ "type": "FeatureCollection", "features": [ { "type": "Feature", "properties": { "name": "路段A", "speed": 80 }, "geometry": { "type": "LineString", "coordinates": [ [116.4, 39.9], [116.5, 39.92], [116.6, 39.88] ] } } ] }有几点要提醒:坐标顺序是经度在前、纬度在后,写反了线条会跑到地图另一头去,我第一次就把这个顺序搞反了,折腾了半天才发现是经纬度反了。另外,LineString至少需要两个坐标点,MultiLineString则是多个LineString的数组,适合表达一条整体逻辑线路的多段不连续几何。
properties 字段是自由扩展的,它不会直接决定样式,但它是后面做"数据驱动样式"的关键。比如你想根据道路的speed属性给线条上不同颜色,靠的就是 properties 里的字段。
2.2 addLayer 的两个核心参数:source 与 paint
给地图添加线条图层的完整代码如下:
map.addLayer({ id: 'route-line', type: 'line', source: { type: 'geojson', data: geojsonData }, layout: { 'line-cap': 'round', 'line-join': 'round' }, paint: { 'line-color': '#ff6b6b', 'line-width': 4, 'line-opacity': 0.8 } });这里我会特别强调type: 'line'这个字段。Mapbox GL 的图层类型不止一种,如果你把type写成了fill,那面的填充逻辑会覆盖掉线的绘制逻辑,线条直接消失。实际开发里我遇到过几次"图层添加了但地图上啥也没有"的情况,排查到最后都是这种低级错误。
layout和paint是两个不同的属性组:layout 控制的是几何层面的渲染方式,比如线帽长什么样、拐角怎么连接;paint 控制的是视觉样式,比如颜色、宽度、透明度。在 Mapbox 文档里这两类属性用不同的前缀标识,line-开头的属性一部分归 layout,一部分归 paint,需要具体区分。我习惯记忆的方式是:改颜色、宽度、透明度、模糊、偏移这类"给人看的效果"是 paint;改端点形状、连接形状这些"几何构造方式"是 layout。
2.3 直接内联 source 与事先定义 source 的区别
上面代码里把 source 直接写在了addLayer的source字段里,这是 Mapbox GL JS 支持的一种快捷写法,数据会被隐式创建一个 source。但如果你需要多个图层共用同一份数据——比如一套线数据既用来画道路,又用来画道路两侧的描边,那最好显式先添加 source:
map.addSource('road-data', { type: 'geojson', data: geojsonData }); map.addLayer({ id: 'road-line', type: 'line', source: 'road-data', paint: { 'line-color': '#333', 'line-width': 6 } }); map.addLayer({ id: 'road-outline', type: 'line', source: 'road-data', paint: { 'line-color': '#fff', 'line-width': 2, 'line-offset': 3 } });这种"一个 source 多个 layer"的模式在实际项目里非常常见,做道路网、地铁线路图时几乎必用。原因是 Mapbox 允许同一条几何数据用不同的 paint 参数渲染多遍,叠加在一起就是描边、发光、阴影等效果,而不用复制数据。
3. 线条样式核心属性:从基础到进阶逐步拆解
这节是全文的重头戏。我把line-开头的常用属性按"基础三件套""几何形态控制""进阶视觉特效""数据驱动"四个层次来讲,每一层都给出可以直接抄的配置。
3.1 基础三件套:颜色、宽度、透明度
line-color是最直观的属性,支持十六进制、rgb()、rgba()、hsl()等格式。特别注意,如果使用了line-gradient(线性渐变),line-color会被忽略,两者不能同时生效。
line-width的单位是像素,但这里的像素是"屏幕像素"而非地理单位。当地图缩放级别变化时,默认为line-width不会随缩放等比变化,除非你手动做了interpolate表达式按 zoom 插值。这是一个地图项目里非常常规的需求:小比例尺时线细一点,大比例尺时线粗一点,如果直接固定一个值,放大后线条会显得过细,缩小后又会糊成一团。
line-opacity控制整体不透明度,取值0到1。如果线条下面还有别的图层,需要露出下面的内容,就用 opacity 做半透明处理。我一般把 opacity 用在背景辅助线上,比如行政区边界叠加到底图上,半透明效果会让主图层更突出。
基础三件套组合的最小配置:
paint: { 'line-color': '#2b6cb0', 'line-width': 3, 'line-opacity': 0.9 }3.2 几何形态:线帽与拐角
line-cap控制线头形状,可选值有butt(平头)、round(圆头)、square(方头)。默认是butt,线条在端点处平直切断。round是最常用的,尤其在绘制道路、轨迹时,圆头看起来自然很多,不会有"被刀切了一刀"的生硬感。
line-join控制折线拐角处的连接方式,可选值bevel(斜切)、round(圆角)、miter(尖角)。道路、河流这种自然元素首选round,拐弯平滑;如果是建筑轮廓、规划红线这类偏硬朗的图形,可以选miter或bevel。
有个细节:miter模式下,如果拐角角度太小,会出现尖角过长甚至超出预期绘制范围的问题。Mapbox 为此提供了line-miter-limit属性,默认值2,意思是当尖角长度超过线宽乘以该值的倍数时,会自动切换为bevel渲染。你不需要经常调它,但知道这个机制后,遇到"尖角变形"就不会慌了。
3.3 虚线、点线与其他 pattern
line-dasharray是画虚线用的属性,值为数字数组,表示实线段和空白段的长度比例,单位是线宽的倍数。例如:
'line-dasharray': [4, 2]表示画 4 倍线宽长度的实线、空 2 倍线宽长度的空白,循环往复。这个"倍数"关系很重要——同一个 dasharray 数组在不同线宽下,虚线的实际疏密会随之变化。比如线宽 2 时,[4, 2]的实线段是 8 像素;线宽 10 时,实线段就是 40 像素。如果希望虚线视觉疏密绝对一致,需要结合 line-width 一起做 interpolate 调整,否则会显得很奇怪。
Mapbox 的线帽和虚线有个联动机制要留意:当line-cap为butt时,虚线段的端点会整齐切断;当line-cap为round时,每个实线段的两端会变成半圆形,线段之间的视觉间距看起来略短。一个常见效果——"铁轨线",就是round线帽加[2, 1]这种短周期 dasharray 产生的,很适合用在跑步路线、规划中的道路这类场景。
3.4 进阶视觉:描边、偏移与模糊
line-offset可以把线条沿垂直方向平移一段像素,正值为向右上偏移,负值为向左下偏移。这个属性的作用非常广:实现道路描边、双线道路、地铁线路的平行线效果,都靠它。
一个典型的双线道路做法:先用一个line-offset: -4的图层画一条较宽的颜色甲线条,再用line-offset: 4的图层画一条较窄的颜色乙线条。因为两条线共用同一个 source,几何一致,偏移方向相反,视觉上就形成了道路中央分隔线的效果。
line-blur给线条添加高斯模糊,单位像素。配合较大的 line-width,可以做出"光带"效果,常用于轨迹高亮、航线展示。要注意的是,line-blur的模糊是向内外两侧均匀扩散的,模糊半径超过线宽一半时线条中心会变淡,出现空心感。我一般把 line-blur 设为线宽的 0.8 到 1.2 倍之间,这样既有光晕感又保留中心实体。
line-gap-width是"空心线"的核心。它在线条内部掏空一块区域,掏空宽度由该属性决定。比如线宽 10、gap-width 4,视觉上就是两条各 3 像素的平行线夹着一条 4 像素的空白。做地铁线路图时,一个线路图层用 gap-width 配合深色背景,就能画出两条平行轨道线,再叠加一个白色小圆点图层当车站,标准的轨道效果就出来了。
// 双轨效果配置 const trackLayer = { id: 'track', type: 'line', source: 'metro-line', paint: { 'line-color': '#333', 'line-width': 8, 'line-gap-width': 5 } };3.5 线性渐变:line-gradient 的使用边界
line-gradient允许线条颜色沿路径方向渐变,例如从起点到终点由绿变红,非常适合表达一条路径的强度变化(性能指标、拥堵程度等)。它的使用机制和普通的 paint 属性有很大不同:必须配合line-gradient表达式,且表达式需要一个["line-progress"]作为输入。
paint: { 'line-width': 6, 'line-gradient': [ 'interpolate', ['linear'], ['line-progress'], 0, '#2ecc71', 0.5, '#f1c40f', 1, '#e74c3c' ] }["line-progress"]是一个特殊变量,取值从 0 到 1,表示线段从头到尾的进度比例。不能直接拿 properties 里的某个数值来做渐变的输入,只能用 line-progress。另外,line-gradient生效时line-color完全失效,且Mapbox 文档里明确 line-gradient 不支持数据驱动,也就是不能根据不同 feature 动态切换渐变规则,它只能做全局统一的一条渐变管线。
可能有人会问:那我想给不同路段设置不同颜色,而不是一条线上渐变,该怎么做?答案是回到数据驱动,用line-color配合match或interpolate表达式,按 properties 里的字段映射颜色。这才是"按属性区分颜色"的正解。
4. 动态改样式与条件式渲染:从静态到交互
静态线条只能看,动态改样式才有灵魂。Mapbox 提供了setPaintProperty这个方法,可以在运行时更新图层样式。这一步掌握之后,悬停高亮、筛选显示、主题切换都手到擒来。
4.1 setPaintProperty 更新单属性
map.setPaintProperty('route-line', 'line-color', '#e74c3c'); map.setPaintProperty('route-line', 'line-width', 8);第一个参数是图层 id,第二个是 paint 属性名,第三个是新值。这个方法会以动画方式更新样式,视觉上非常平滑。需要注意:setPaintProperty 只能更新 paint 属性,不能更新 layout 属性。如果你想换line-cap或line-join,得用setLayoutProperty:
map.setLayoutProperty('route-line', 'line-cap', 'butt');虽然 layout 属性改动不触发 paint 的动画过渡,但也不会造成地图整体重载,所以不用担心闪烁问题。
4.2 数据驱动样式:match 和 interpolate 表达式的实际使用
Mapbox 的样式值支持表达式语法,这意味着你可以在paint里写一个"函数"而不只是一个常量。最常见的两个场景:
场景一:按类别给颜色
假设 route 数据里有type字段,值可能是expressway、local、footpath,想分别显示红、黄、绿:
paint: { 'line-color': [ 'match', ['get', 'type'], 'expressway', '#e74c3c', 'local', '#f1c40f', 'footpath', '#2ecc71', '#95a5a6' // 默认兜底色 ], 'line-width': [ 'interpolate', ['linear'], ['zoom'], 8, 2, 12, 4, 16, 8 ] }match语法第一参是取值表达式,后面是"值-颜色"成对出现的映射,最后必须有一个兜底值,不然匹配不到时会报错。['get', 'type']表示从 feature 的 properties 里取 type 字段。
场景二:数值连续映射颜色
如果属性值是连续数值,比如speed从 0 到 120,想从绿色渐变到红色:
paint: { 'line-color': [ 'interpolate', ['linear'], ['get', 'speed'], 0, '#2ecc71', 60, '#f1c40f', 120, '#e74c3c' ] }注意这里第一参是['linear'],也可以用['exponential', 0.5]来做非线性插值,但一般线性就够用了。数据驱动样式是 Mapbox 最强大的特性之一,它的性能也足够好,Mapbox GL 通过分块检查 feature 的属性值并按需重绘,几千个 feature 的样式切换基本无感。
4.3 鼠标悬停高亮:与 feature-state 的结合
悬停高亮经常会用到feature-state,这个机制比直接改源数据更高效。先在 source 上设置promoteId,比如数据里每条 feature 有id字段:
map.addSource('route-data', { type: 'geojson', data: geojsonData, promoteId: 'id' });然后在 paint 里用['feature-state', 'hover']作为条件:
paint: { 'line-color': [ 'case', ['boolean', ['feature-state', 'hover'], false], '#ff6b6b', '#2b6cb0' ] }鼠标移动时,通过setFeatureState更新状态:
map.on('mousemove', 'route-line', (e) => { if (e.features.length > 0) { const featureId = e.features[0].id; map.setFeatureState({ source: 'route-data', id: featureId }, { hover: true }); } }); map.on('mouseleave', 'route-line', () => { map.removeFeatureState({ source: 'route-data', id: hoveredId }); });这套方案比setPaintProperty去逐条覆盖颜色精准得多,因为它按 feature 粒度管理状态,不会污染其他线条。如果数据里没有 id 字段,Mapbox 会基于坐标生成内部 id,但那个 id 不稳定,尽量保证数据里有稳定的 id。
5. 高频坑与完整排查链路:从"线没显示"到"虚线不生效"
这里写几个我在实际项目中踩过、也帮别人排查过的典型问题。每个问题我都尽量还原完整的排查思路,而不是直接丢结论,因为排查思路本身比结论更有复用价值。
5.1 坑一:图层加了但线完全看不见
现象:代码不报错,地图正常加载,但 target 图层区域空空如也。
排查链路:
- 打开浏览器开发者工具,在 Console 里看有没有
Unable to load source之类的报错,如果有,先查 source 的 data URL 是否正确,或者内联 GeoJSON 是否格式非法。 - 在 Console 执行
map.getLayer('route-line'),返回undefined说明图层压根没加上;返回图层对象但看不到,说明数据或样式有问题。 - 执行
map.getSource('route-data')获取 source 后调用getData(),展开输出的 JSON,检查 coordinates 数组里每个点是否[lng, lat]顺序正确,数值是否在合法范围。 - 检查
type是否为line。我见过有人把 GeoJSON 里的 geometry type 和 layer type 搞混:geometry 是LineString,但 layer 的type写成了fill,结果整个区域被填充而不是线条。 - 检查
line-width是否被设成了0。说实话,我有一段时间为了做"只在某 zoom 级别显示线",用表达式把宽度设为 0,后来忘了改回来,排查了非常久。
5.2 坑二:虚线不生效,显示的还是实线
现象:设置了line-dasharray: [4, 2],但线条依然实线显示。
完整原因分析:Mesh 层面有两种可能。第一种是线条太短。Mapbox 的 dasharray 在短线上会因为 line-width 与比例关系导致虚线变化不明显,如果一条线只有几个像素长,虚线自然看不出效果。建议先拉长测试线,排除数据长度因素。
第二种原因是line-dasharray 与某些属性不兼容。具体来说,line-cap设置为round时,虚线段的端点会绘制成圆头,如果 dasharray 数组的值较小,圆头和圆头连在一起,视觉上就会趋向于一条连续的粗线。另外,line-gap-width与 dasharray 同时使用时,虚线中的空隙区域表现会受 gap-width 影响,出现"实线段看起来被劈开、虚线间距消失"的错觉。
排查方式:先把 line-cap 改成 butt,line-gap-width 改成 0,看虚线是否恢复。如果恢复了,就是这两个属性对 dasharray 的干扰。
有一个隐藏很深的要求:虚线渲染需要图层是line类型且没有开启line-border之类的附加绘制,某些旧版本 GL JS 在部分 GPU 驱动下对 dasharray 支持不完善,会出现虚线渲染成实线或渲染成雪花点。遇到这种情况,要么升级版本,要么把 dasharray 转成数据预处理——在 GeoJSON 层面生成多个线段,配合"实线-空白"交替绘制,不过这个方案复杂而且维护成本高,非必要不建议做。
5.3 坑三:线宽随缩放比例不对,放大后锯齿严重
现象:zoom 级别变大时,线条没有同步变粗,且边缘出现明显锯齿。
原因分析:地图瓦片在放大时会进行插值缩放,矢量数据的线条如果 line-width 没有跟随 zoom 变化,就会出现"缩放像素色块"导致的锯齿。锯齿感最直接的解决办法是把line-width用 interpolate 表达式绑定 zoom,让线宽随 zoom 平滑增加。
paint: { 'line-width': [ 'interpolate', ['linear'], ['zoom'], 5, 1, 10, 3, 15, 8 ] }锚点(zoom 值)和线宽(对应值)不是随便填的,需要结合实际地图场景调试。比如展示城市道路时,zoom 5(全国视野)线宽 1px 比较合适,zoom 10(城市级)3px,zoom 15(街道级)8px。这个没有数学公式,基本靠经验和视觉效果调。
锯齿还有一个来源是超长 LineString 在 WebGL 中被切割成多段绘制,每段边缘的抗锯齿计算是独立的,导致接缝处出现"拼接缝"。这种情况可以通过减小单条 LineString 的长度(拆分 feature)来缓解,或者用 GeoJSON 的tolerance字段在数据源端做几何简化。
5.4 坑四:Token 相关的 401 或样式加载失败
虽然前面说了 token 获取,但每次都要强调:地图不显示且 Console 里报Unauthorized或403,第一个要查的就是 token 是否正确、是否超出配额。
我在调试时经常遇到一种情况:有人把 Mapbox GL JS 的 CDN 地址复制错了,加载的是老版本 v1,而 v1 和 v2 在 token 处理上存在差异。如果发现样式一直挂在style.load事件不触发,检查一下 script 引入的版本号和官网要求是否一致,现在官方推荐直接用 v3 版本的 CDN 或 npm 包。
还有一点:如果你在本地开发用的是file://协议打开 HTML,某些浏览器对跨域读取本地 GeoJSON 文件有限制,也会导致 source 加载失败。开发阶段建议用http-server、vite dev这类本地服务跑,别直接双击 html 文件。
6. 进阶优化与性能注意
样式不只是"能显示",还要考虑性能,尤其数据量上来之后。这一节不展开讲所有性能优化,只讲和线图层直接相关的几个点。
6.1 大量线要素的性能隐患
如果一次加载上万条线,比如全国的河流网、道路网,每条线都有复杂的 LineString,直接塞进 GeoJSON source 会让地图明显卡顿。Mapbox 的矢量渲染效率已经很高,但过度复杂的几何体仍然会在缩放时触发大量 CPU/GPU 计算。
常用策略是数据简化(simplify)。发布前用工具降低坐标点密度,保留几何大轮廓即可。在 Mapbox 层面还能调整line-width的最小值、关闭line-blur之类耗性能的效果,这些在小数据量时感觉不到,大数据量时非常明显。
另外,line-dasharray本身也会带来额外的 GPU 纹理计算,所以虚线图层只加在必要的场景。如果整个地图有几百个虚线图层,帧率下降会很明显。
6.2 用 setData 更新数据与用 setStyle 的取舍
动态更新线数据时,可以直接操作 source 的setData:
map.getSource('route-data').setData(newGeoJSON);这个操作比整个setStyle轻量得多,因为它只更新数据层,不会重新加载底图样式和字体。但是有个注意事项:setData后 source 上的promoteId、feature-state 等配置不会丢,但已设置的 feature-state 会因数据更新而失效,所以高亮状态要在更新数据后重新设置。
如果换了 token 或者想整体换底图风格,再考虑setStyle。
6.3 与高德/Leaflet 线条样式的差异是理解 Mapbox 的关键
用过 Leaflet 或高德 JS API 的话,你可能习惯直接给 polyline 设置color、weight、opacity。Mapbox 的思路不太一样:它把样式按属性拆得非常细,并且允许每个属性用表达式驱动。好处是复杂样式的表达能力非常强,坏处是初学阶段需要多花点时间理解属性体系。
我自己的体会是:一旦你习惯了 "layout/paint" 分离、表达式驱动、feature-state 这套模式,再回去用 Leaflet 会觉得表达能力受限。Mapbox 的线样式本质上是声明式的渲染管线,你描述"什么样 Conditions 下画成什么样",剩下的交给渲染引擎,这和传统命令式绘图有本质区别。
7. 实用技巧与个人经验补充
在收尾之前,再分享几个零散但非常实用的技巧,都是我在实际项目中验证过、觉得值得写下来的。
7.1 线条图层的分层命名习惯
图层一多,命名混乱是灾难。我自己的习惯是{业务}_{类型}_{用途},比如road_line_main、road_line_outline、metro_line_track。这样在代码里查找和维护都方便,也方便其他同事接手。同一个 source 的多层样式之间,用_main、_outline、_shadow这种后缀区分。
7.2 在线的样式调试工具
Mapbox 官方提供的Mapbox Studio里可以可视化编辑样式,但它的样式编辑器对表达式支持有限,复杂的表达式仍然建议在代码里维护。我更常用的调试方式是:浏览器里开着开发者工具,在 Console 里快速执行map.setPaintProperty(...)反复调参,颜色可以先用十六进制,调到满意再固化到代码里。这个"实时调参"的流程比改代码刷新页面高效太多。
7.3 巧用 opacity 叠加实现"霓虹发光"效果
线条发光的实现不复杂:一个线宽较大、透明度较低的图层作为光晕层,叠在上面再画一条线宽较小、高亮颜色的主图层,两者共用同一 source。光晕层用line-blur适当模糊,不用单独做渐变就能达到不错的视觉效果。
// 光晕层 map.addLayer({ id: 'route-glow', type: 'line', source: 'route-data', paint: { 'line-color': '#ff6b6b', 'line-width': 12, 'line-opacity': 0.3, 'line-blur': 4 } }); // 主图层 map.addLayer({ id: 'route-main', type: 'line', source: 'route-data', paint: { 'line-color': '#ff2d2d', 'line-width': 3 } });注意图层顺序:光晕层先 add,主图层后 add,这样主图层压在光晕层上方。如果顺序反了,主图层的颜色会盖住光晕效果。
7.4 千万别忽略 line-width 与 line-blur 的配合
调line-blur时容易踩一个隐含逻辑:blur 的值是像素,但它是基于线宽绘制的纹理上做的。如果线宽只有 2px,blur 设成 20px,那这条线会淡到几乎看不见。我的经验是:blur 的值不要超过 line-width 的 2 倍范围,否则调试时你会怀疑是不是透明度出了问题。
最后再说一个个人体会。Mapbox 的线条样式表看起来只是几十个属性,但真正用好它,考验的是你对"数据与渲染分离"这一模型的理解。很多人在入门时急着去记 API 名字,我却建议先把 GeoJSON 结构和属性表达式搞明白:数据里有什么字段、每条线代表什么含义、希望视觉上如何区分,这些想清楚之后,paint 属性写法水到渠成。
如果遇到文档里没说透的细节,比如某个属性和另一个属性打架、某个效果在不同缩放级别表现不一致,别怕多试。拿一条简单的测试线把可能的组合都调一遍,比翻半天文档更高效。你现在手上有项目的话,挑一段真实路线数据,把这篇文章里的配置依次铺上去,很快就能摸清 Mapbox 线条样式的脾气。