第05章 - 样式规范 Style Spec 详解

5.1 style 是一份被严格约束的 JSON

MapLibre 渲染什么、怎么渲染,全部由一份 style JSON 决定。这份 JSON 的结构由 @maplibre/maplibre-gl-style-spec 定义,6.17.0 依赖 ^26.6.0,对应规范版本 v8。规范源文件是 v8.json,主线程与 worker 都用它校验样式。

一个合法的最小样式只有三个必填字段:

{
  "version": 8,
  "sources": {},
  "layers": [
    {"id": "bg", "type": "background", "paint": {"background-color": "#e9eef2"}}
  ]
}

version 的类型是枚举,值只能是 8;sources 与 layers 都是必填。少任何一个,校验就会失败。如果打开 validateStyle(默认开),错误会以 error 事件抛出;关掉之后格式错的内容可能被静默接受,然后在渲染阶段以更难懂的方式失败,生产环境关它之前先想清楚。

5.2 根级字段:除了 sources 和 layers 还剩什么

v8 的根对象共 20 个字段,按用途分四类:

类别 字段 类型/默认值 作用
必填 version 枚举,固定 8 规范版本
必填 sources 对象 六类数据源的定义
必填 layers 数组 图层列表,顺序即绘制顺序(后画的在上)
相机初值 center / centerAltitude 经纬度 / 米 地图初始中心与中心海拔
相机初值 zoom / bearing / pitch / roll 数字 初始缩放、方位角、俯仰、翻滚
资源 sprite {id, url} 数组 图标与图案的雪碧图
资源 glyphs URL 模板字符串 SDF 字体 PBF 的地址模板
资源 font-faces 对象 直接以字体文件描述字形,替代 glyphs 的另一种方式
视觉 light 对象 3D 图层的光照方向、颜色、强度
视觉 sky 对象 天空、地平线、雾的颜色与混合
视觉 terrain {source, exaggeration} 地形源与高程夸张系数
视觉 projection {type} mercator / globe / vertical-perspective
其他 name / metadata 字符串 / 任意 名称与自定义元数据
其他 transition {duration: 300, delay: 0} 全局过渡时长与延迟(毫秒)
其他 state 对象,默认 {} global-state 表达式的默认值表

其中 sprite 的细节值得单独提:v8 支持多个雪碧图,写成数组 [{"id": "default", "url": "..."}, {"id": "poi", "url": "..."}]。引用图片时用 id:image-name 作前缀;id 为 default 时前缀可省略。只有一个雪碧图时,旧写法 "sprite": "https://..." 仍然兼容。

{
  "version": 8,
  "name": "basemap",
  "sprite": [
    {"id": "default", "url": "https://example.com/sprites/basic"},
    {"id": "transit", "url": "https://example.com/sprites/transit"}
  ],
  "glyphs": "https://example.com/fonts/{fontstack}/{range}.pbf",
  "sources": {},
  "layers": [],
  "transition": {"duration": 250, "delay": 0}
}

5.3 layers:公共字段与十个类型

所有图层共享同一组公共字段,差异在 type 与各自的 paint/layout:

字段 必填 说明
id 是 全局唯一的图层名
type 是 见下表
source 除 background 外必填 引用 sources 里的 id
source-layer vector 源必填 矢量瓦片内的图层名
minzoom / maxzoom 否 0–24;小于 minzoom 或大于等于 maxzoom 时隐藏
filter 否 按属性或几何类型筛选要素
layout 否 影响几何生成的属性(是否可见、是否参与碰撞等)
paint 否 影响最终像素的属性(颜色、宽度、透明度等)
metadata 否 任意元数据,不参与渲染,建议加前缀

十个规范内类型如下。第 3 章列过 11 种图层,是把渲染器内部的 custom 也算进去;custom 不是 style 文件里的类型,而是通过 addLayer 传入的自定义渲染对象。

类型 依赖的 source 典型用途 关键 paint 属性
background 无 底色、纯色铺底 background-color、background-opacity
fill vector / geojson 面填充 fill-color、fill-opacity、fill-outline-color
line vector / geojson 线、边、路径 line-color、line-width、line-dasharray
symbol vector / geojson 图标与文字标注 icon-image、text-field、text-size
circle vector / geojson 点要素画圆 circle-radius、circle-color
heatmap vector / geojson 热力分布 heatmap-radius、heatmap-weight
fill-extrusion vector / geojson 三维体块、建筑 fill-extrusion-height、-base
raster raster 影像、栅格底图 raster-opacity、raster-resampling
hillshade raster-dem 山体阴影 hillshade-illumination-direction
color-relief raster-dem 高程分色 color-relief-color

layout 与 paint 的区别不是”静态与动态”,而是影响几何还是影响像素。line-cap 决定线端如何生成顶点,属于 layout;line-color 只改变片元颜色,属于 paint。这个区分直接影响性能:改 layout 往往要重新生成 Bucket,改 paint 可能只是换 uniform(详见 3.5 与 5.6)。

5.4 属性值类型与默认值

v8 给每个属性都定义了类型、默认值、取值范围和是否支持过渡。写样式时最常踩的坑是”以为有默认值其实没有”和”以为能插值其实不能”。

5.4.1 paint 关键默认值

属性 类型 默认值 备注
background-color color #000000 不填就是黑底
background-opacity number 1 0–1
fill-color color #000000  
fill-opacity number 1 影响填充与 1px 描边
fill-outline-color color 无 不填则不描边
fill-antialias boolean true 关掉可省一点性能,边缘变硬
line-color color #000000 与 line-pattern 互斥
line-width number 1 单位像素
line-opacity number 1  
line-blur number 0  
line-gap-width number 0 画双线(描边效果)用
line-offset number 0 正负决定左右偏移
line-dasharray number[] 无 虚线,单位与线宽相关
circle-radius number 5 像素
circle-color color #000000  
circle-stroke-width number 0  
circle-pitch-scale enum map map 随俯仰缩放,viewport 不缩放
heatmap-radius number 30  
heatmap-weight number 1  
heatmap-intensity number 1  
fill-extrusion-height number 0 不填则完全没有立体
fill-extrusion-base number 0  
raster-opacity number 1  
raster-fade-duration number 300 瓦片淡入,ms
raster-resampling enum linear 影像用 linear,分类栅格可改 nearest
hillshade-illumination-direction number 335 度
hillshade-exaggeration number 0.5  

5.4.2 layout 关键默认值

属性 类型 默认值 备注
visibility enum visible 只有 visible / none 两值
line-cap enum butt butt / round / square
line-join enum miter miter / bevel / round
line-miter-limit number 2 超过该值转 bevel
line-round-limit number 1.05 小角度时转 round
text-field formatted "" 空字符串表示不显示
text-size number 16 像素
text-font array ["Open Sans Regular","Arial Unicode MS Regular"] 字体栈
text-max-width number 10 单位 em
text-padding number 2 碰撞框外扩像素
text-allow-overlap boolean false true 时忽略碰撞
icon-size number 1 相对原图倍率
symbol-placement enum point point / line / line-center
symbol-sort-key number 无 越小越优先参与碰撞

layout_symbol 是规范里最大的属性组,共 47 个字段。以上列的是高频项,完整清单可以在 v8.json 的 layout_symbol 里查到。

5.5 filter:两代语法与一条硬规则

filter 有两种写法,都仍然有效:

{
  "filter": ["==", "class", "primary"]
}
{
  "filter": ["==", ["get", "class"], "primary"]
}

第一种是旧式语法,属性名直接写字符串;第二种是表达式语法,用 ["get", ...] 取值。6.x 的默认行为是:混用两代语法会产生警告级别的校验错误,但不再中止样式加载。早期版本会直接把地图画成空白,这个行为已经在 v6 修掉。

filter 支持的算子是一份固定清单:

类别 算子
比较 ==、!=、>、>=、<、<=
集合 in、!in
逻辑 all、any、none
存在性 has、!has

一条容易忽略的硬规则:filter 里不能用 feature-state。规范对 layer.filter 的说明明确写了这一点,feature-state 属于渲染阶段的表达式,只能在 paint/layout 属性里用。需要在过滤层面表达”选中态”,只能用属性字段或拆图层。

另一条规则是:filter 里的 zoom 表达式只在整数 zoom 级别求值。缩放到 10.4 时,用的仍是 zoom=10 时的结果。所以不要把 filter 当作连续插值的工具,那属于 paint。

// 选中态不能用 feature-state 做 filter,正确做法是拆一个高亮图层
map.addLayer({
  id: 'roads-selected',
  type: 'line',
  source: 'roads',
  'source-layer': 'transportation',
  filter: ['==', ['get', 'id'], -1], // 初始不匹配任何要素
  paint: {'line-color': '#ff7a00', 'line-width': 6}
});

function selectRoad(id) {
  // 用 setFilter 切换精确匹配,避免逐要素 feature-state 的复杂度
  map.setFilter('roads-selected', ['==', ['get', 'id'], id]);
}

5.6 过渡、表达式与数据驱动

paint 属性大多支持过渡:样式里可以写 "fill-color-transition": {"duration": 500, "delay": 100},或者在根级 transition 里设全局默认。规范里 transition 的默认值是 {duration: 300, delay: 0},单位毫秒。

过渡能不能生效,取决于属性的 transition 标记与 property-type:

property-type 含义 例子
常量 只能是常量,不能数据驱动 background-color
data-driven 可按 feature 属性逐要素取值,支持插值 line-color、fill-opacity
cross-faded 跨瓦片淡入淡出,不能插值 icon-image、text-field

规范里每个属性都带 expression 元数据,写明允许的输入范围,例如 line-color 是 {"interpolated": true, "parameters": ["zoom", "feature", "feature-state"]},text-size 是 ["zoom", "feature"]。看这份元数据就能判断某个表达式能不能用在某个属性上,比试错快。

{
  "id": "population-fill",
  "type": "fill",
  "source": "districts",
  "paint": {
    "fill-color": [
      "interpolate", ["linear"], ["get", "density"],
      0, "#f7fbff",
      1000, "#c6dbef",
      5000, "#6baed6",
      20000, "#2171b5"
    ],
    "fill-opacity-transition": {"duration": 400, "delay": 0}
  }
}

5.7 校验:把错误挡在样式进入地图之前

规范包同时提供了校验器,可以在 CI 里跑,不必等到浏览器里才发现拼写错误。

# 用官方 CLI 校验一个样式文件
npx @maplibre/maplibre-gl-style-spec gl-style-validate style.json

# 格式化(统一缩进与键顺序,便于 diff)
npx @maplibre/maplibre-gl-style-spec gl-style-format style.json

在运行时,校验由 validateStyle 控制:默认 true,样式里每个不合法的属性都会通过 error 事件报出,并带上路径(例如 layers[3].paint.line-color)。6.x 改进了错误定位,看到这种路径能直接定位到具体图层与属性。

const map = new maplibregl.Map({
  container: 'map',
  style: styleObject,
  validateStyle: true // 默认就是 true,写出来是为了提醒
});

map.on('error', (e) => {
  // 样式校验错误通常带 sourceId 或 style 路径信息
  console.error(e.error?.message);
});

生产环境可以把 validateStyle 设为 false:样式在上线前已经用 CLI 校验过,运行时省掉校验开销,某些打包器还能对校验代码做 tree-shaking。代价是运行时拼错的属性不再报错,所以要有”样式必须过 CI”的流程兜底。

5.8 常见坑

layers 顺序就是绘制顺序。 后声明的图层画在上面。把标注图层写在面图层之前,标注会被盖住——这不是 bug。

fill-outline-color 不等于描边。 它只在填充边缘画 1 像素宽的线,且仅对 fill-antialias: true 有效。要可控线宽,叠一个 line 图层。

line-width 的单位是像素,不随 zoom 变化。 想让线随缩放变粗,必须写 ["interpolate", ["linear"], ["zoom"], ...];直接写 line-width: 2,在任何缩放级别都是 2 px。

text-field 不能直接写 feature 属性名。 要么用 {name} 这类 token 语法,要么用 ["get", "name"]。写 "text-field": "name" 会得到字面量 “name”。

sprite 路径会追加 .json 与 .png。 写成 https://x/y.png 会变成 https://x/y.png.json,正确写法是不带扩展名的 https://x/y。

glyphs 必须是 {fontstack}/{range}.pbf 模板。 少了占位符,字体请求会 404,标注全部不显示且没有明显报错。

version 只能是 8。 写 7 或 9 都过不了校验。

5.9 生产建议

样式分文件维护再合并。 一个城市级底图样式动辄上千行,直接手写一个 JSON 不可维护。常见做法是按图层分文件、构建时合并,或者用 style 生成库。

用 transformStyle 而不是复制粘贴。 换底图、打补丁、加业务图层都在运行时做,比维护两份完整样式可靠。

把 transition 显式写死。 依赖默认 300 ms 在不同版本间可能变化,显式声明能让视觉不随升级漂移。

关掉不用的图层而不是删掉。 调试图层用 "visibility": "none",上线时按环境生成不同样式;删掉再换回来容易丢配置。

样式体积也要进性能预算。 一份 2 MB 的样式 JSON 解析要几百毫秒,首屏卡顿有时不是瓦片慢,而是样式本身太大。压缩与拆分 sprite 是最直接的两招。

5.10 本章小结

主题 结论
必填字段 version: 8、sources、layers
根字段 20 个,含 camera 初值、sprite/glyphs、light/sky/terrain/projection、transition、state
图层类型 规范内 10 种;custom 由 addLayer 传入,不属于样式文件类型
layout vs paint layout 影响几何生成,paint 影响像素;前者改动的重算成本更高
关键默认值 线宽 1、圆半径 5、文字 16 px、背景黑、过渡 300 ms
filter 两代语法可混用(会告警);不支持 feature-state;zoom 只在整数级别求值
表达式约束 每个属性带 expression.parameters,决定能用 zoom/feature/feature-state 中的哪些
校验 CLI 上 CI;运行时 validateStyle 默认 true,错误带路径