據(jù)驅(qū)動(dòng)動(dòng)態(tài)渲染)
我最早接觸 Mapbox 的線樣式時(shí)第一反應(yīng)是這玩意兒不就調(diào)個(gè)顏色和粗細(xì)嗎真正上手才知道線條樣式在 Mapbox GL 里對(duì)應(yīng)的是一整套以line-開(kāi)頭的 paint 屬性體系從基礎(chǔ)的線寬、顏色到虛線、漸變、描邊、偏移再到數(shù)據(jù)驅(qū)動(dòng)樣式組合起來(lái)能玩的花樣非常多。如果你也在用 Mapbox 畫(huà)路線、邊界、等值線或者軌跡這篇就把線條樣式從配置到原理、從靜態(tài)到動(dòng)態(tài)完整拆開(kāi)講清楚順便把我踩過(guò)的坑也一并交代了。先說(shuō)下這篇內(nèi)容的范圍以 Mapbox GL JS v2/v3 為主涉及addLayer添加line類型圖層、核心 paint 屬性的作用機(jī)制、GeoJSON 數(shù)據(jù)格式要求、樣式動(dòng)態(tài)更新最后是幾個(gè)高頻問(wèn)題的排查過(guò)程。適合剛接觸 Mapbox 的初學(xué)者也適合已經(jīng)能跑通基礎(chǔ)地圖、想在樣式細(xì)節(jié)上做深的朋友。1. 動(dòng)手前的裝備Token 獲取與首次初始化很多人在 Mapbox 官網(wǎng)注冊(cè)賬號(hào)之后卡在了第一步到底去哪拿 token拿到之后又該放哪。這個(gè)環(huán)節(jié)看似基礎(chǔ)實(shí)際上后面所有圖層能否正常顯示都跟它直接相關(guān)所以先把這塊踩實(shí)。1.1 注冊(cè)與 Token 獲取的正確姿勢(shì)注冊(cè)流程沒(méi)什么特別的去 Mapbox 官網(wǎng)用郵箱注冊(cè)完成郵箱驗(yàn)證后進(jìn)入賬戶后臺(tái)。注意一個(gè)細(xì)節(jié)Mapbox 的 Access Token 分幾種包括默認(rèn)的 public tokenpk.開(kāi)頭和 secret tokensk.開(kāi)頭。前端地圖渲染用的是 public token 就夠了secret token 千萬(wàn)別寫(xiě)進(jìn)前端代碼里它泄露了相當(dāng)于你的賬戶額度被人白嫖。在后臺(tái)的Account Tokens頁(yè)面可以創(chuàng)建新 token。創(chuàng)建時(shí)建議手動(dòng)選擇一個(gè) scope 集合至少要包含styles:read、fonts:read、tiles:read這幾個(gè)權(quán)限對(duì)應(yīng)了加載樣式、字體和瓦片的基礎(chǔ)需求。Token 創(chuàng)建完成后有一串很長(zhǎng)的字符串就是類似pk.eyJ1Ijoi...的長(zhǎng)文本復(fù)制保存好。1.2 初始化地圖Token 放哪才不會(huì)被坑初始化地圖時(shí) token 的注入方式有兩種一種是在引入 Mapbox GL JS 的 script 標(biāo)簽之前直接給mapboxgl.accessToken賦值另一種是在new mapboxgl.Map()的構(gòu)造參數(shù)里通過(guò)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 });這里有個(gè)常見(jiàn)的坑token 放到了 DOM 元素加載之前執(zhí)行。如果你把mapboxgl.accessToken ...寫(xiě)在了#map這個(gè) div 的前面而且用了document.getElementById(map)去拿容器大概率會(huì)報(bào)Container not found。反正我建議把地圖初始化代碼整體放在頁(yè)面底部或者用window.onload/DOMContentLoaded包起來(lái)順序問(wèn)題就徹底沒(méi)了。初始化完成后如果你能看到 Mapbox 默認(rèn)的街道底圖說(shuō)明環(huán)境已經(jīng)通了。接下來(lái)所有線條相關(guān)操作都建立在這張底圖之上。2. line 圖層的骨架Source 數(shù)據(jù)格式與 addLayer 配置線條不是憑空畫(huà)出來(lái)的它必須先有數(shù)據(jù)源source再通過(guò)圖層layer定義怎么畫(huà)。Mapbox 的這套數(shù)據(jù)與樣式分離設(shè)計(jì)一開(kāi)始可能會(huì)覺(jué)得繞但理解了之后會(huì)發(fā)現(xiàn)它特別適合做動(dòng)態(tài)換膚和交互高亮。2.1 GeoJSON 里 LineString 的格式約束Mapbox 中最常用的 source 類型是 GeoJSON線條對(duì)應(yīng)的幾何類型是LineString或MultiLineString。一個(gè)最簡(jiǎn)單的線條數(shù)據(jù)長(zhǎng)這樣{ 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] ] } } ] }有幾點(diǎn)要提醒坐標(biāo)順序是經(jīng)度在前、緯度在后寫(xiě)反了線條會(huì)跑到地圖另一頭去我第一次就把這個(gè)順序搞反了折騰了半天才發(fā)現(xiàn)是經(jīng)緯度反了。另外LineString至少需要兩個(gè)坐標(biāo)點(diǎn)MultiLineString則是多個(gè)LineString的數(shù)組適合表達(dá)一條整體邏輯線路的多段不連續(xù)幾何。properties 字段是自由擴(kuò)展的它不會(huì)直接決定樣式但它是后面做數(shù)據(jù)驅(qū)動(dòng)樣式的關(guān)鍵。比如你想根據(jù)道路的speed屬性給線條上不同顏色靠的就是 properties 里的字段。2.2 addLayer 的兩個(gè)核心參數(shù)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 } });這里我會(huì)特別強(qiáng)調(diào)type: line這個(gè)字段。Mapbox GL 的圖層類型不止一種如果你把type寫(xiě)成了fill那面的填充邏輯會(huì)覆蓋掉線的繪制邏輯線條直接消失。實(shí)際開(kāi)發(fā)里我遇到過(guò)幾次圖層添加了但地圖上啥也沒(méi)有的情況排查到最后都是這種低級(jí)錯(cuò)誤。layout和paint是兩個(gè)不同的屬性組layout 控制的是幾何層面的渲染方式比如線帽長(zhǎng)什么樣、拐角怎么連接paint 控制的是視覺(jué)樣式比如顏色、寬度、透明度。在 Mapbox 文檔里這兩類屬性用不同的前綴標(biāo)識(shí)line-開(kāi)頭的屬性一部分歸 layout一部分歸 paint需要具體區(qū)分。我習(xí)慣記憶的方式是改顏色、寬度、透明度、模糊、偏移這類給人看的效果是 paint改端點(diǎn)形狀、連接形狀這些幾何構(gòu)造方式是 layout。2.3 直接內(nèi)聯(lián) source 與事先定義 source 的區(qū)別上面代碼里把 source 直接寫(xiě)在了addLayer的source字段里這是 Mapbox GL JS 支持的一種快捷寫(xiě)法數(shù)據(jù)會(huì)被隱式創(chuàng)建一個(gè) source。但如果你需要多個(gè)圖層共用同一份數(shù)據(jù)——比如一套線數(shù)據(jù)既用來(lái)畫(huà)道路又用來(lái)畫(huà)道路兩側(cè)的描邊那最好顯式先添加 sourcemap.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 } });這種一個(gè) source 多個(gè) layer的模式在實(shí)際項(xiàng)目里非常常見(jiàn)做道路網(wǎng)、地鐵線路圖時(shí)幾乎必用。原因是 Mapbox 允許同一條幾何數(shù)據(jù)用不同的 paint 參數(shù)渲染多遍疊加在一起就是描邊、發(fā)光、陰影等效果而不用復(fù)制數(shù)據(jù)。3. 線條樣式核心屬性從基礎(chǔ)到進(jìn)階逐步拆解這節(jié)是全文的重頭戲。我把line-開(kāi)頭的常用屬性按基礎(chǔ)三件套幾何形態(tài)控制進(jìn)階視覺(jué)特效數(shù)據(jù)驅(qū)動(dòng)四個(gè)層次來(lái)講每一層都給出可以直接抄的配置。3.1 基礎(chǔ)三件套顏色、寬度、透明度line-color是最直觀的屬性支持十六進(jìn)制、rgb()、rgba()、hsl()等格式。特別注意如果使用了line-gradient線性漸變line-color會(huì)被忽略兩者不能同時(shí)生效。line-width的單位是像素但這里的像素是屏幕像素而非地理單位。當(dāng)?shù)貓D縮放級(jí)別變化時(shí)默認(rèn)為line-width不會(huì)隨縮放等比變化除非你手動(dòng)做了interpolate表達(dá)式按 zoom 插值。這是一個(gè)地圖項(xiàng)目里非常常規(guī)的需求小比例尺時(shí)線細(xì)一點(diǎn)大比例尺時(shí)線粗一點(diǎn)如果直接固定一個(gè)值放大后線條會(huì)顯得過(guò)細(xì)縮小后又會(huì)糊成一團(tuán)。line-opacity控制整體不透明度取值0到1。如果線條下面還有別的圖層需要露出下面的內(nèi)容就用 opacity 做半透明處理。我一般把 opacity 用在背景輔助線上比如行政區(qū)邊界疊加到底圖上半透明效果會(huì)讓主圖層更突出?;A(chǔ)三件套組合的最小配置paint: { line-color: #2b6cb0, line-width: 3, line-opacity: 0.9 }3.2 幾何形態(tài)線帽與拐角line-cap控制線頭形狀可選值有butt平頭、round圓頭、square方頭。默認(rèn)是butt線條在端點(diǎn)處平直切斷。round是最常用的尤其在繪制道路、軌跡時(shí)圓頭看起來(lái)自然很多不會(huì)有被刀切了一刀的生硬感。line-join控制折線拐角處的連接方式可選值bevel斜切、round圓角、miter尖角。道路、河流這種自然元素首選round拐彎平滑如果是建筑輪廓、規(guī)劃紅線這類偏硬朗的圖形可以選miter或bevel。有個(gè)細(xì)節(jié)miter模式下如果拐角角度太小會(huì)出現(xiàn)尖角過(guò)長(zhǎng)甚至超出預(yù)期繪制范圍的問(wèn)題。Mapbox 為此提供了line-miter-limit屬性默認(rèn)值2意思是當(dāng)尖角長(zhǎng)度超過(guò)線寬乘以該值的倍數(shù)時(shí)會(huì)自動(dòng)切換為bevel渲染。你不需要經(jīng)常調(diào)它但知道這個(gè)機(jī)制后遇到尖角變形就不會(huì)慌了。3.3 虛線、點(diǎn)線與其他 patternline-dasharray是畫(huà)虛線用的屬性值為數(shù)字?jǐn)?shù)組表示實(shí)線段和空白段的長(zhǎng)度比例單位是線寬的倍數(shù)。例如line-dasharray: [4, 2]表示畫(huà) 4 倍線寬長(zhǎng)度的實(shí)線、空 2 倍線寬長(zhǎng)度的空白循環(huán)往復(fù)。這個(gè)倍數(shù)關(guān)系很重要——同一個(gè) dasharray 數(shù)組在不同線寬下虛線的實(shí)際疏密會(huì)隨之變化。比如線寬 2 時(shí)[4, 2]的實(shí)線段是 8 像素線寬 10 時(shí)實(shí)線段就是 40 像素。如果希望虛線視覺(jué)疏密絕對(duì)一致需要結(jié)合 line-width 一起做 interpolate 調(diào)整否則會(huì)顯得很奇怪。Mapbox 的線帽和虛線有個(gè)聯(lián)動(dòng)機(jī)制要留意當(dāng)line-cap為butt時(shí)虛線段的端點(diǎn)會(huì)整齊切斷當(dāng)line-cap為round時(shí)每個(gè)實(shí)線段的兩端會(huì)變成半圓形線段之間的視覺(jué)間距看起來(lái)略短。一個(gè)常見(jiàn)效果——鐵軌線就是round線帽加[2, 1]這種短周期 dasharray 產(chǎn)生的很適合用在跑步路線、規(guī)劃中的道路這類場(chǎng)景。3.4 進(jìn)階視覺(jué)描邊、偏移與模糊line-offset可以把線條沿垂直方向平移一段像素正值為向右上偏移負(fù)值為向左下偏移。這個(gè)屬性的作用非常廣實(shí)現(xiàn)道路描邊、雙線道路、地鐵線路的平行線效果都靠它。一個(gè)典型的雙線道路做法先用一個(gè)line-offset: -4的圖層畫(huà)一條較寬的顏色甲線條再用line-offset: 4的圖層畫(huà)一條較窄的顏色乙線條。因?yàn)閮蓷l線共用同一個(gè) source幾何一致偏移方向相反視覺(jué)上就形成了道路中央分隔線的效果。line-blur給線條添加高斯模糊單位像素。配合較大的 line-width可以做出光帶效果常用于軌跡高亮、航線展示。要注意的是line-blur的模糊是向內(nèi)外兩側(cè)均勻擴(kuò)散的模糊半徑超過(guò)線寬一半時(shí)線條中心會(huì)變淡出現(xiàn)空心感。我一般把 line-blur 設(shè)為線寬的 0.8 到 1.2 倍之間這樣既有光暈感又保留中心實(shí)體。line-gap-width是空心線的核心。它在線條內(nèi)部掏空一塊區(qū)域掏空寬度由該屬性決定。比如線寬 10、gap-width 4視覺(jué)上就是兩條各 3 像素的平行線夾著一條 4 像素的空白。做地鐵線路圖時(shí)一個(gè)線路圖層用 gap-width 配合深色背景就能畫(huà)出兩條平行軌道線再疊加一個(gè)白色小圓點(diǎn)圖層當(dāng)車站標(biāo)準(zhǔn)的軌道效果就出來(lái)了。// 雙軌效果配置 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允許線條顏色沿路徑方向漸變例如從起點(diǎn)到終點(diǎn)由綠變紅非常適合表達(dá)一條路徑的強(qiáng)度變化性能指標(biāo)、擁堵程度等。它的使用機(jī)制和普通的 paint 屬性有很大不同必須配合line-gradient表達(dá)式且表達(dá)式需要一個(gè)[line-progress]作為輸入。paint: { line-width: 6, line-gradient: [ interpolate, [linear], [line-progress], 0, #2ecc71, 0.5, #f1c40f, 1, #e74c3c ] }[line-progress]是一個(gè)特殊變量取值從 0 到 1表示線段從頭到尾的進(jìn)度比例。不能直接拿 properties 里的某個(gè)數(shù)值來(lái)做漸變的輸入只能用 line-progress。另外line-gradient生效時(shí)line-color完全失效且Mapbox 文檔里明確 line-gradient 不支持?jǐn)?shù)據(jù)驅(qū)動(dòng)也就是不能根據(jù)不同 feature 動(dòng)態(tài)切換漸變規(guī)則它只能做全局統(tǒng)一的一條漸變管線。可能有人會(huì)問(wèn)那我想給不同路段設(shè)置不同顏色而不是一條線上漸變?cè)撛趺醋龃鸢甘腔氐綌?shù)據(jù)驅(qū)動(dòng)用line-color配合match或interpolate表達(dá)式按 properties 里的字段映射顏色。這才是按屬性區(qū)分顏色的正解。4. 動(dòng)態(tài)改樣式與條件式渲染從靜態(tài)到交互靜態(tài)線條只能看動(dòng)態(tài)改樣式才有靈魂。Mapbox 提供了setPaintProperty這個(gè)方法可以在運(yùn)行時(shí)更新圖層樣式。這一步掌握之后懸停高亮、篩選顯示、主題切換都手到擒來(lái)。4.1 setPaintProperty 更新單屬性map.setPaintProperty(route-line, line-color, #e74c3c); map.setPaintProperty(route-line, line-width, 8);第一個(gè)參數(shù)是圖層 id第二個(gè)是 paint 屬性名第三個(gè)是新值。這個(gè)方法會(huì)以動(dòng)畫(huà)方式更新樣式視覺(jué)上非常平滑。需要注意setPaintProperty 只能更新 paint 屬性不能更新 layout 屬性。如果你想換line-cap或line-join得用setLayoutPropertymap.setLayoutProperty(route-line, line-cap, butt);雖然 layout 屬性改動(dòng)不觸發(fā) paint 的動(dòng)畫(huà)過(guò)渡但也不會(huì)造成地圖整體重載所以不用擔(dān)心閃爍問(wèn)題。4.2 數(shù)據(jù)驅(qū)動(dòng)樣式match 和 interpolate 表達(dá)式的實(shí)際使用Mapbox 的樣式值支持表達(dá)式語(yǔ)法這意味著你可以在paint里寫(xiě)一個(gè)函數(shù)而不只是一個(gè)常量。最常見(jiàn)的兩個(gè)場(chǎng)景場(chǎng)景一按類別給顏色假設(shè) route 數(shù)據(jù)里有type字段值可能是expressway、local、footpath想分別顯示紅、黃、綠paint: { line-color: [ match, [get, type], expressway, #e74c3c, local, #f1c40f, footpath, #2ecc71, #95a5a6 // 默認(rèn)兜底色 ], line-width: [ interpolate, [linear], [zoom], 8, 2, 12, 4, 16, 8 ] }match語(yǔ)法第一參是取值表達(dá)式后面是值-顏色成對(duì)出現(xiàn)的映射最后必須有一個(gè)兜底值不然匹配不到時(shí)會(huì)報(bào)錯(cuò)。[get, type]表示從 feature 的 properties 里取 type 字段。場(chǎng)景二數(shù)值連續(xù)映射顏色如果屬性值是連續(xù)數(shù)值比如speed從 0 到 120想從綠色漸變到紅色paint: { line-color: [ interpolate, [linear], [get, speed], 0, #2ecc71, 60, #f1c40f, 120, #e74c3c ] }注意這里第一參是[linear]也可以用[exponential, 0.5]來(lái)做非線性插值但一般線性就夠用了。數(shù)據(jù)驅(qū)動(dòng)樣式是 Mapbox 最強(qiáng)大的特性之一它的性能也足夠好Mapbox GL 通過(guò)分塊檢查 feature 的屬性值并按需重繪幾千個(gè) feature 的樣式切換基本無(wú)感。4.3 鼠標(biāo)懸停高亮與 feature-state 的結(jié)合懸停高亮經(jīng)常會(huì)用到feature-state這個(gè)機(jī)制比直接改源數(shù)據(jù)更高效。先在 source 上設(shè)置promoteId比如數(shù)據(jù)里每條 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 ] }鼠標(biāo)移動(dòng)時(shí)通過(guò)setFeatureState更新?tīng)顟B(tài)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去逐條覆蓋顏色精準(zhǔn)得多因?yàn)樗?feature 粒度管理狀態(tài)不會(huì)污染其他線條。如果數(shù)據(jù)里沒(méi)有 id 字段Mapbox 會(huì)基于坐標(biāo)生成內(nèi)部 id但那個(gè) id 不穩(wěn)定盡量保證數(shù)據(jù)里有穩(wěn)定的 id。5. 高頻坑與完整排查鏈路從線沒(méi)顯示到虛線不生效這里寫(xiě)幾個(gè)我在實(shí)際項(xiàng)目中踩過(guò)、也幫別人排查過(guò)的典型問(wèn)題。每個(gè)問(wèn)題我都盡量還原完整的排查思路而不是直接丟結(jié)論因?yàn)榕挪樗悸繁旧肀冉Y(jié)論更有復(fù)用價(jià)值。5.1 坑一圖層加了但線完全看不見(jiàn)現(xiàn)象代碼不報(bào)錯(cuò)地圖正常加載但 target 圖層區(qū)域空空如也。排查鏈路打開(kāi)瀏覽器開(kāi)發(fā)者工具在 Console 里看有沒(méi)有Unable to load source之類的報(bào)錯(cuò)如果有先查 source 的 data URL 是否正確或者內(nèi)聯(lián) GeoJSON 是否格式非法。在 Console 執(zhí)行map.getLayer(route-line)返回undefined說(shuō)明圖層壓根沒(méi)加上返回圖層對(duì)象但看不到說(shuō)明數(shù)據(jù)或樣式有問(wèn)題。執(zhí)行map.getSource(route-data)獲取 source 后調(diào)用getData()展開(kāi)輸出的 JSON檢查 coordinates 數(shù)組里每個(gè)點(diǎn)是否[lng, lat]順序正確數(shù)值是否在合法范圍。檢查type是否為line。我見(jiàn)過(guò)有人把 GeoJSON 里的 geometry type 和 layer type 搞混geometry 是LineString但 layer 的type寫(xiě)成了fill結(jié)果整個(gè)區(qū)域被填充而不是線條。檢查line-width是否被設(shè)成了0。說(shuō)實(shí)話我有一段時(shí)間為了做只在某 zoom 級(jí)別顯示線用表達(dá)式把寬度設(shè)為 0后來(lái)忘了改回來(lái)排查了非常久。5.2 坑二虛線不生效顯示的還是實(shí)線現(xiàn)象設(shè)置了line-dasharray: [4, 2]但線條依然實(shí)線顯示。完整原因分析Mesh 層面有兩種可能。第一種是線條太短。Mapbox 的 dasharray 在短線上會(huì)因?yàn)?line-width 與比例關(guān)系導(dǎo)致虛線變化不明顯如果一條線只有幾個(gè)像素長(zhǎng)虛線自然看不出效果。建議先拉長(zhǎng)測(cè)試線排除數(shù)據(jù)長(zhǎng)度因素。第二種原因是line-dasharray 與某些屬性不兼容。具體來(lái)說(shuō)line-cap設(shè)置為round時(shí)虛線段的端點(diǎn)會(huì)繪制成圓頭如果 dasharray 數(shù)組的值較小圓頭和圓頭連在一起視覺(jué)上就會(huì)趨向于一條連續(xù)的粗線。另外line-gap-width與 dasharray 同時(shí)使用時(shí)虛線中的空隙區(qū)域表現(xiàn)會(huì)受 gap-width 影響出現(xiàn)實(shí)線段看起來(lái)被劈開(kāi)、虛線間距消失的錯(cuò)覺(jué)。排查方式先把 line-cap 改成 buttline-gap-width 改成 0看虛線是否恢復(fù)。如果恢復(fù)了就是這兩個(gè)屬性對(duì) dasharray 的干擾。有一個(gè)隱藏很深的要求虛線渲染需要圖層是line類型且沒(méi)有開(kāi)啟line-border之類的附加繪制某些舊版本 GL JS 在部分 GPU 驅(qū)動(dòng)下對(duì) dasharray 支持不完善會(huì)出現(xiàn)虛線渲染成實(shí)線或渲染成雪花點(diǎn)。遇到這種情況要么升級(jí)版本要么把 dasharray 轉(zhuǎn)成數(shù)據(jù)預(yù)處理——在 GeoJSON 層面生成多個(gè)線段配合實(shí)線-空白交替繪制不過(guò)這個(gè)方案復(fù)雜而且維護(hù)成本高非必要不建議做。5.3 坑三線寬隨縮放比例不對(duì)放大后鋸齒嚴(yán)重現(xiàn)象zoom 級(jí)別變大時(shí)線條沒(méi)有同步變粗且邊緣出現(xiàn)明顯鋸齒。原因分析地圖瓦片在放大時(shí)會(huì)進(jìn)行插值縮放矢量數(shù)據(jù)的線條如果 line-width 沒(méi)有跟隨 zoom 變化就會(huì)出現(xiàn)縮放像素色塊導(dǎo)致的鋸齒。鋸齒感最直接的解決辦法是把line-width用 interpolate 表達(dá)式綁定 zoom讓線寬隨 zoom 平滑增加。paint: { line-width: [ interpolate, [linear], [zoom], 5, 1, 10, 3, 15, 8 ] }錨點(diǎn)zoom 值和線寬對(duì)應(yīng)值不是隨便填的需要結(jié)合實(shí)際地圖場(chǎng)景調(diào)試。比如展示城市道路時(shí)zoom 5全國(guó)視野線寬 1px 比較合適zoom 10城市級(jí)3pxzoom 15街道級(jí)8px。這個(gè)沒(méi)有數(shù)學(xué)公式基本靠經(jīng)驗(yàn)和視覺(jué)效果調(diào)。鋸齒還有一個(gè)來(lái)源是超長(zhǎng) LineString 在 WebGL 中被切割成多段繪制每段邊緣的抗鋸齒計(jì)算是獨(dú)立的導(dǎo)致接縫處出現(xiàn)拼接縫。這種情況可以通過(guò)減小單條 LineString 的長(zhǎng)度拆分 feature來(lái)緩解或者用 GeoJSON 的tolerance字段在數(shù)據(jù)源端做幾何簡(jiǎn)化。5.4 坑四Token 相關(guān)的 401 或樣式加載失敗雖然前面說(shuō)了 token 獲取但每次都要強(qiáng)調(diào)地圖不顯示且 Console 里報(bào)Unauthorized或403第一個(gè)要查的就是 token 是否正確、是否超出配額。我在調(diào)試時(shí)經(jīng)常遇到一種情況有人把 Mapbox GL JS 的 CDN 地址復(fù)制錯(cuò)了加載的是老版本 v1而 v1 和 v2 在 token 處理上存在差異。如果發(fā)現(xiàn)樣式一直掛在style.load事件不觸發(fā)檢查一下 script 引入的版本號(hào)和官網(wǎng)要求是否一致現(xiàn)在官方推薦直接用 v3 版本的 CDN 或 npm 包。還有一點(diǎn)如果你在本地開(kāi)發(fā)用的是file://協(xié)議打開(kāi) HTML某些瀏覽器對(duì)跨域讀取本地 GeoJSON 文件有限制也會(huì)導(dǎo)致 source 加載失敗。開(kāi)發(fā)階段建議用http-server、vite dev這類本地服務(wù)跑別直接雙擊 html 文件。6. 進(jìn)階優(yōu)化與性能注意樣式不只是能顯示還要考慮性能尤其數(shù)據(jù)量上來(lái)之后。這一節(jié)不展開(kāi)講所有性能優(yōu)化只講和線圖層直接相關(guān)的幾個(gè)點(diǎn)。6.1 大量線要素的性能隱患如果一次加載上萬(wàn)條線比如全國(guó)的河流網(wǎng)、道路網(wǎng)每條線都有復(fù)雜的 LineString直接塞進(jìn) GeoJSON source 會(huì)讓地圖明顯卡頓。Mapbox 的矢量渲染效率已經(jīng)很高但過(guò)度復(fù)雜的幾何體仍然會(huì)在縮放時(shí)觸發(fā)大量 CPU/GPU 計(jì)算。常用策略是數(shù)據(jù)簡(jiǎn)化simplify。發(fā)布前用工具降低坐標(biāo)點(diǎn)密度保留幾何大輪廓即可。在 Mapbox 層面還能調(diào)整line-width的最小值、關(guān)閉line-blur之類耗性能的效果這些在小數(shù)據(jù)量時(shí)感覺(jué)不到大數(shù)據(jù)量時(shí)非常明顯。另外line-dasharray本身也會(huì)帶來(lái)額外的 GPU 紋理計(jì)算所以虛線圖層只加在必要的場(chǎng)景。如果整個(gè)地圖有幾百個(gè)虛線圖層幀率下降會(huì)很明顯。6.2 用 setData 更新數(shù)據(jù)與用 setStyle 的取舍動(dòng)態(tài)更新線數(shù)據(jù)時(shí)可以直接操作 source 的setDatamap.getSource(route-data).setData(newGeoJSON);這個(gè)操作比整個(gè)setStyle輕量得多因?yàn)樗桓聰?shù)據(jù)層不會(huì)重新加載底圖樣式和字體。但是有個(gè)注意事項(xiàng)setData后 source 上的promoteId、feature-state 等配置不會(huì)丟但已設(shè)置的 feature-state 會(huì)因數(shù)據(jù)更新而失效所以高亮狀態(tài)要在更新數(shù)據(jù)后重新設(shè)置。如果換了 token 或者想整體換底圖風(fēng)格再考慮setStyle。6.3 與高德/Leaflet 線條樣式的差異是理解 Mapbox 的關(guān)鍵用過(guò) Leaflet 或高德 JS API 的話你可能習(xí)慣直接給 polyline 設(shè)置color、weight、opacity。Mapbox 的思路不太一樣它把樣式按屬性拆得非常細(xì)并且允許每個(gè)屬性用表達(dá)式驅(qū)動(dòng)。好處是復(fù)雜樣式的表達(dá)能力非常強(qiáng)壞處是初學(xué)階段需要多花點(diǎn)時(shí)間理解屬性體系。我自己的體會(huì)是一旦你習(xí)慣了 layout/paint 分離、表達(dá)式驅(qū)動(dòng)、feature-state 這套模式再回去用 Leaflet 會(huì)覺(jué)得表達(dá)能力受限。Mapbox 的線樣式本質(zhì)上是聲明式的渲染管線你描述什么樣 Conditions 下畫(huà)成什么樣剩下的交給渲染引擎這和傳統(tǒng)命令式繪圖有本質(zhì)區(qū)別。7. 實(shí)用技巧與個(gè)人經(jīng)驗(yàn)補(bǔ)充在收尾之前再分享幾個(gè)零散但非常實(shí)用的技巧都是我在實(shí)際項(xiàng)目中驗(yàn)證過(guò)、覺(jué)得值得寫(xiě)下來(lái)的。7.1 線條圖層的分層命名習(xí)慣圖層一多命名混亂是災(zāi)難。我自己的習(xí)慣是{業(yè)務(wù)}_{類型}_{用途}比如road_line_main、road_line_outline、metro_line_track。這樣在代碼里查找和維護(hù)都方便也方便其他同事接手。同一個(gè) source 的多層樣式之間用_main、_outline、_shadow這種后綴區(qū)分。7.2 在線的樣式調(diào)試工具M(jìn)apbox 官方提供的Mapbox Studio里可以可視化編輯樣式但它的樣式編輯器對(duì)表達(dá)式支持有限復(fù)雜的表達(dá)式仍然建議在代碼里維護(hù)。我更常用的調(diào)試方式是瀏覽器里開(kāi)著開(kāi)發(fā)者工具在 Console 里快速執(zhí)行map.setPaintProperty(...)反復(fù)調(diào)參顏色可以先用十六進(jìn)制調(diào)到滿意再固化到代碼里。這個(gè)實(shí)時(shí)調(diào)參的流程比改代碼刷新頁(yè)面高效太多。7.3 巧用 opacity 疊加實(shí)現(xiàn)霓虹發(fā)光效果線條發(fā)光的實(shí)現(xiàn)不復(fù)雜一個(gè)線寬較大、透明度較低的圖層作為光暈層疊在上面再畫(huà)一條線寬較小、高亮顏色的主圖層兩者共用同一 source。光暈層用line-blur適當(dāng)模糊不用單獨(dú)做漸變就能達(dá)到不錯(cuò)的視覺(jué)效果。// 光暈層 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這樣主圖層壓在光暈層上方。如果順序反了主圖層的顏色會(huì)蓋住光暈效果。7.4 千萬(wàn)別忽略 line-width 與 line-blur 的配合調(diào)line-blur時(shí)容易踩一個(gè)隱含邏輯blur 的值是像素但它是基于線寬繪制的紋理上做的。如果線寬只有 2pxblur 設(shè)成 20px那這條線會(huì)淡到幾乎看不見(jiàn)。我的經(jīng)驗(yàn)是blur 的值不要超過(guò) line-width 的 2 倍范圍否則調(diào)試時(shí)你會(huì)懷疑是不是透明度出了問(wèn)題。最后再說(shuō)一個(gè)個(gè)人體會(huì)。Mapbox 的線條樣式表看起來(lái)只是幾十個(gè)屬性但真正用好它考驗(yàn)的是你對(duì)數(shù)據(jù)與渲染分離這一模型的理解。很多人在入門(mén)時(shí)急著去記 API 名字我卻建議先把 GeoJSON 結(jié)構(gòu)和屬性表達(dá)式搞明白數(shù)據(jù)里有什么字段、每條線代表什么含義、希望視覺(jué)上如何區(qū)分這些想清楚之后paint 屬性寫(xiě)法水到渠成。如果遇到文檔里沒(méi)說(shuō)透的細(xì)節(jié)比如某個(gè)屬性和另一個(gè)屬性打架、某個(gè)效果在不同縮放級(jí)別表現(xiàn)不一致別怕多試。拿一條簡(jiǎn)單的測(cè)試線把可能的組合都調(diào)一遍比翻半天文檔更高效。你現(xiàn)在手上有項(xiàng)目的話挑一段真實(shí)路線數(shù)據(jù)把這篇文章里的配置依次鋪上去很快就能摸清 Mapbox 線條樣式的脾氣。