ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

mapbox-gl 2.7.0去token实战:自托管资源改造与离线渲染

mapbox-gl 2.7.0去token实战:自托管资源改造与离线渲染

简介:Mapbox GL 2.7.0 去除令牌资源包,面向需要摆脱官方接口密钥限制、实现地图本地化渲染的 WebGIS 开发者,解决开发测试中令牌缺失导致地图无法加载的问题。内容围绕 WebGL 地图渲染、匿名访问与离线部署场景,整理出本地 GeoJSON 数据源、自建瓦片服务、公开图层引用、自定义样式及本地缓存等多种去令牌方案,适合有一定 JavaScript 基础、熟悉 Mapbox GL JS 接口的初中级开发者参考。压缩包共 2000 个文件,大小约 28.07 MB,核心为 2051 个 pbf 矢量瓦片、7 个样式与数据配置(json)、10 个 svg 图标以及 js、css、map 等配套文件,涵盖地图样式定义、矢量数据存储与渲染所需资源,可支撑地图样式调试与瓦片数据替换。目前已有 2559 人学习下载,目录按样式、瓦片、图标分类,结构清晰。开发者可从中快速理解去令牌的合法边界与方法选型,获得可实际运行的离线地图渲染思路,减少对官方服务的依赖。

1. 去 token 不是删参数:mapbox-gl 2.7.0 的校验到底卡在哪

接到一个内网部署的地图项目,样式、瓦片、字体全在自己服务器上,唯独 mapbox-gl 2.7.0 初始化时绕不开 access_token。网上搜去 token,大多数方案是往 URL 里塞一个假 token,或者干脆把 token 校验代码删掉——这两条路都走不通:假 token 会让请求打到官方域名,内网直接超时;删校验代码又会误伤样式里 mapbox:// 协议的资源解析。实际上 2.7.0 的 token 机制核心不在校验本身,而在资源 URL 的规范化:只要 style 里还有一个 mapbox:// 开头的地图资源,就必须有 token 才能换成官方 API 地址。搞清楚这条链路,去 token 的方案就很清晰:让官方域名完全不参与资源加载。本文按这个思路拆解三种做法、参数设置和踩坑记录,目标是让你的项目在只有自托管资源、没有任何 token 的前提下完整渲染。

2. 先搞清 2.7.0 在查什么:token 校验路径与三条去 token 路线

2.1 三个关键调用点:样式规范化、源规范化与字体规范化

mapbox-gl 2.7.0 里对 token 的检查,不是简单判断“有没有传 access_token 字符串”,而是藏在 URL 处理管道里。这个版本内部有一个配置模块,集中维护 API 域名、token 读取和 URL 转换逻辑。你传入的 style 如果是字符串路径,引擎会先拉取这个 JSON,然后逐个字段检查:sprite、glyphs、各个 source 的 url。只要是以mapbox://开头的资源,都会被转换成https://api.mapbox.com/...形式的正式地址,转换时自动追加access_token参数。

我一般会在本地起个代理看请求,很多开发者第一次做去 token 时,只把mapboxgl.accessToken置空,结果控制台报错:A valid Mapbox access token is required to use Mapbox GL JS。这个报错的触发条件很明确:解析到mapbox://资源,但拿不到 token。报错不是发生在你调用new MapboxGL.Map()的那一刻,而是发生在 style 被拉取并开始解析资源 URL 的异步阶段。所以你会看到地图容器一片空白,network 面板里 style.json 已经 200 了,但后面所有资源请求全部停住。

用一个代码块说明 2.7.0 里 URL 规范化做的事,帮助理解这个黑匣子:

// 伪代码:mapbox-gl 2.7.0 的样式资源 URL 规范化逻辑 const ACCESS_TOKEN = getAccessToken(); // 从全局变量或本地存储读取 function normalizeStyleURL(url, accessToken) { if (!url.startsWith('mapbox://')) return url; // mapbox://styles/某用户/某样式 -> https://api.mapbox.com/styles/v1/某用户/某样式 const path = url.replace('mapbox://styles', '/styles/v1'); const params = { access_token: accessToken }; return `https://api.mapbox.com${path}?${toQueryString(params)}`; } function normalizeSourceURL(url, accessToken) { if (!url.startsWith('mapbox://')) return url; // mapbox://tiles/某数据集 -> https://api.mapbox.com/v4/某数据集/{z}/{x}/{y}.vector.pbf const tilesURL = url.replace('mapbox://tiles', '/v4'); return `${API_URL}${tilesURL}/{z}/{x}/{y}.vector.pbf?access_token=${accessToken}`; } function normalizeGlyphsURL(url, accessToken) { if (!url.startsWith('mapbox://')) return url; // mapbox://fonts/某字体/{fontstack}/{range}.pbf const path = url.replace('mapbox://fonts', '/fonts/v1'); return `${API_URL}${path}?access_token=${accessToken}`; }

这段逻辑说明两个重点。第一,token 不是附加参数,而是 URL 规范化管道的一部分,mapbox://协议只有在拿到 token 时才被转换成可用的 HTTP 地址。第二,getAccessToken()的读取顺序是先看构造函数里的 options,再看全局的mapboxgl.accessToken,最后才看本地存储。很多人只在样式里改了自己的域名,却漏了 source 里的mapbox://,这时候报错依旧,而且报错信息不会告诉你具体是哪个字段。

2.2 三条路线对比:资源替换、请求改写与源码去除

理解了 token 在 URL 规范化里起作用,去 token 的思路就变成:让样式 JSON 里一个mapbox://资源都不存在。基于这个目标,业界常见做法分三条路线:

路线改动位置改动量适用场景风险点
A:自托管资源替换style.json 里的 sprite、glyphs、source url中等,需要准备四类资源内网部署、私有化交付资源不全时地图渲染残缺
B:transformRequest 请求改写前端初始化代码小,不碰资源文件已有线上样式但想拦截官方请求治标不治本,漏网请求仍会打到官方
C:改源码后重新构建源码配置模块较大,需维护私有构建长期项目、彻底去官方依赖升级官方版本时 patch 会失效

这三条路线的优先级,我一般建议按 A 为主、B 兜底、C 按需选择来排。路线 A 解决的是“资源从哪里加载”的问题,路线 B 解决的是“残留 token 参数怎么清掉”的问题,路线 C 解决的是“想彻底删掉校验逻辑”的洁癖需求。如果只做 B,不改 style JSON,资源请求还是会发到官方域名,内网场景下必挂。如果只做 C 不改资源 URL,构建出来的库仍然会请求官方 API。所以三者不是互相替代,而是配合关系。

路线 A 里最容易忽略的是 glyphs 和 sprite 这两个静态资源。很多开发者只关心瓦片地址,把 source 换成自己的矢量瓦片服务后,发现地图能拖动了,但是文字全没了、图标全没了,这才意识到字体和雪碧图也需要自托管。下面第三章直接把四类资源一个个写清楚。

3. 自托管资源替换:最小改动去 token 的完整配置

3.1 四类资源准备:style、sprite、glyphs、tile 的目录与格式

做资源替换前,先明确 2.7.0 加载一份完整样式需要哪几类文件。常见目录结构长这样:

assets/ ├── style.json # 样式入口,所有资源引用从这里出发 ├── sprites/ │ ├── sprite.png # 普通分辨率雪碧图 │ ├── sprite@2x.png # 高清雪碧图,可选但推荐 │ ├── sprite.json # 雪碧图坐标索引 │ └── sprite@2x.json ├── fonts/ │ ├── Open Sans Regular/ │ │ ├── 0-255.pbf │ │ ├── 256-511.pbf │ │ └── ... │ └── Open Sans Bold/ │ └── ... └── tiles/ ├── 0/ │ ├── 0/ │ │ └── 0.pbf │ └── 1/ │ └── 0.pbf └── 1/ ...

这几个目录各有讲究。先看 glyphs 字体:mapbox-gl 的字体加载是按区间拉取的,URL 模板里必须保留{fontstack}和{range}两个占位符,实际请求时引擎会把字体名和字形区间填进去。如果你是自己切了字体文件,要把一个字体家族的所有pbf放在以字体名命名的目录下,目录名里的空格在请求时要 URL 编码,这是后面踩坑章节的老大难。

再看 sprite 雪碧图:样式里写的 sprite 是一个不带后缀的前缀,引擎会自动请求.json和.png。所以assets/sprites/sprite这个前缀会对应sprite.json和sprite.png两个文件。sprites 里如果同时存在@2x版本,地图在 high-DPI 屏上会自动选 2x 那套。雪碧图不是随便拼张图就行的,JSON 里记录的每个图标的 x、y、width、height、pixelRatio 必须与 PNG 像素位置严格一致,否则图标要么偏移要么直接空白。

至于瓦片,如果你手头的是.mbtiles文件,常见做法是先解包成目录结构,或者用现成的切片服务直接提供 ZXY 协议的请求地址。style 里的 source 可以写成tiles数组,也可以写url指向一个 TileJSON。去 token 背景下我更推荐直接写tiles数组,少一层 JSON 拉取,排查问题也更直接。

3.2 改写 style JSON:把 mapbox:// 换成自托管 URL 的映射规则

拿到一份官方的 style JSON,需要替换的字段集中在三个位置。以常见样式为例,改前是这样的:

{ "sprite": "mapbox://sprites/mapbox/light-v10", "glyphs": "mapbox://fonts/mapbox/{fontstack}/{range}.pbf", "sources": { "baseMap": { "type": "vector", "url": "mapbox://tiles/mapbox.mapbox-streets-v8" } }, "layers": [ { "id": "background", "type": "background", "paint": { "background-color": "#f2efe9" } }, { "id": "road", "type": "line", "source": "baseMap", "source-layer": "road" } ] }

改后:

{ "sprite": "/assets/sprites/sprite", "glyphs": "/assets/fonts/{fontstack}/{range}.pbf", "sources": { "baseMap": { "type": "vector", "tiles": ["https://tiles.example.com/vector/{z}/{x}/{y}.pbf"], "minzoom": 0, "maxzoom": 14 } }, "layers": [ { "id": "background", "type": "background", "paint": { "background-color": "#f2efe9" } }, { "id": "road", "type": "line", "source": "baseMap", "source-layer": "road" } ] }

映射规则拆开看就是这么几条:sprite 直接写前缀路径,不带.json或.png后缀;glyphs 写成模板路径,保留两个占位符;source 里url改成tiles数组,每个元素是完整的瓦片地址模板,{z}/{x}/{y}是必须的占位符;记得补上minzoom和maxzoom,这决定了引擎在瓦片缺失时是上溯到低级别瓦片还是直接显示空白。

这里有个容易被忽略的细节:style 里引用 source 的方式有两种,一种是用url指向 TileJSON,另一种是用tiles数组直接声明地址。改写成tiles后,原来sources里那个url字段必须删掉或者替换,否则引擎会优先按url字段去解析,而那个url如果是自托管地址还好,如果是mapbox://就前功尽弃。

另一个细节是 layer 里的source-layer字段。换成自己的瓦片服务后,source-layer的名称可能与官方数据集不同。用官方mapbox.mapbox-streets-v8时图层叫road、building,换成自制瓦片后如果图层名不对,整个图层会静默消失。排查时先看 styles 面板,再对比瓦片源里的图层名,这个坑比 token 本身难发现得多。

3.3 transformRequest 兜底:把残留 access_token 参数清掉

资源替换完成后,理论上不会再有mapbox://协议,token 也不再被需要。但实际项目里经常出现一种情况:拿到的 style JSON 是从别的项目拷过来的,里面个别 URL 已经写成了带access_token参数的完整地址;或者瓦片服务是自研的,服务端只认自己的鉴权方式,多一个access_token参数会直接拒请求。这时候用transformRequest做最后一层清洗是成本最低的方案。

const map = new mapboxgl.Map({ container: 'map', style: '/assets/style.json', accessToken: '', // 显式传空字符串,覆盖全局可能存在的 token transformRequest: (url, resourceType) => { // 只处理地图资源请求,不碰业务接口 if (resourceType === 'Source' || resourceType === 'Tile' || resourceType === 'Glyphs' || resourceType === 'Sprite') { const u = new URL(url, window.location.origin); // 清掉 access_token 参数 u.searchParams.delete('access_token'); // 如果官方 API 域名被硬编码在样式里,也一并替换 if (u.hostname === 'api.mapbox.com') { u.hostname = 'internal.example.com'; u.protocol = window.location.protocol; } return { url: u.toString() }; } return { url }; } });

这段代码里,accessToken: ''是很多人忽略的细节。2.7.0 读取 token 时,如果构造函数里显式传了空字符串,就不会再去读全局mapboxgl.accessToken。如果不传这个字段,而页面其他模块设置了全局 token,去 token 验证就不干净了。

transformRequest的返回格式在 2.7.0 里必须是一个包含url的对象,旧版本那种直接返回字符串的写法在这个版本已经不再支持。resourceType的取值有Style、Source、Tile、Glyphs、Sprite、Image,我只清洗后面四类,因为 Style 本身是我们自己服务器上的 JSON,不需要额外处理,Image 则可能是业务图标,动了反而出错。

还要注意new URL(url, window.location.origin)这行的作用。如果 style 里写的是相对路径/assets/fonts/{fontstack}/{range}.pbf,引擎构造请求时会自动补全为当前页面的域名,这里的第二个参数window.location.origin保证了解析时有一个确定的 base,不会因为 url 是完整地址而抛异常。

4. 彻底去 token:编译 2.7.0 源码的改动点

4.1 源码里 token 逻辑集中在哪个文件

如果你不想在每次初始化时都写 transformRequest,或者想把事件上报、统计请求也一并去掉,那就得改源码重新构建。mapbox-gl 2.7.0 的源码在 npm 包里可以直接拿到,安装后用 grep 定位 token 相关逻辑:

# 定位 token 读取和 URL 规范化代码 grep -rn "getAccessToken" node_modules/mapbox-gl/src/util/config.js # 找出所有引用 token 校验逻辑的文件 grep -rn "requireAccessToken\|normalizeStyleURL\|normalizeSourceURL" node_modules/mapbox-gl/src --include="*.js" -l

第一次做这件事的开发者,看到 grep 结果通常会愣一下:token 逻辑不是一个文件独占,而是集中在config.js里定义,再由style.js、source.js、glyph_manager.js等多个模块引用。这种设计导致只改一处往往不够,你得把所有入口都堵上。

具体来说,config.js里干这几件事:定义API_URL和EVENTS_URL两个域名常量;实现getAccessToken()读取 token;实现normalizeStyleURL、normalizeSourceURL、normalizeSpriteURL、normalizeGlyphsURL这几个转换函数。后四个函数就是前面伪代码的真实版本。而requireAccessToken这个开关会在地图初始化、样式解析、资源请求三个地方被查询。

还有一个很多人没注意到的点:事件上报。mapbox-gl 2.x 默认开启遥测,即使你的资源全自托管,它也会向官方事件域名发送请求。这个和 token 无关,但它同样走了API_URL所在的配置模块。做彻底去 token 时,建议把EVENTS_URL一并清掉,否则打开 network 面板,看到一条条外网请求,心里总是悬着的。

4.2 最小 patch 与构建验证

我一般会在config.js上做最小 patch,原则是:不删除任何函数签名,只改函数体,避免影响其他模块的 import 结构。以normalizeStyleURL为例:

// 改动前:mapbox:// 协议转换成官方 API 地址并追加 token export function normalizeStyleURL(url, accessToken) { if (!url.startsWith('mapbox://')) return url; const urlObject = parseUrl(url); urlObject.path = `/styles/v1${urlObject.path}`; urlObject.params.access_token = accessToken; return urlObject; } // 改动后:mapbox:// 协议原样返回,不再解析、不再追加 token export function normalizeStyleURL(url, accessToken) { return url; }

normalizeSourceURL、normalizeGlyphsURL、normalizeSpriteURL这三个函数也照此办理,全部改成直接返回原始 url。normalizeSourceURL稍微特殊一点,官方源码里它还会处理mapbox://之外的相对路径,所以别整个删掉逻辑,只把mapbox://分支改为不触发即可。

EVENTS_URL的处理更简单:

// 改动前 export const EVENTS_URL = 'https://events.mapbox.com'; // 改动后:清空事件上报地址 export const EVENTS_URL = '';

然后执行构建:

npm install npm run build

构建产物在dist/目录下,有 UMD 版和 ESM 版。项目里原来怎么引入 mapbox-gl,改成引本地这份产物即可。构建完成后做一个快速验证:在控制台打印dist/mapbox-gl.js里搜不到api.mapbox.com字样,说明源码层的官方域名依赖已经清干净。

这个方案的代价是后续升级困难。mapbox-gl 后续版本如果调整了config.js的函数结构,你的 patch 可能对不上,需要重新适配。所以源码改写的方案更适合那种锁版本、长期不升级的交付型项目。如果你用的是 npm 管理依赖,记得把 mapbox-gl 的版本锁死,不要用^2.7.0这种允许小版本浮动的写法,否则一次npm install就可能把 2.7.0 变成 2.7.x 的最新版,行为又不一致了。

5. 去 token 后的常见问题排查:5 个典型翻车现场

5.1 只删了 token 参数,样式里的 mapbox:// 没换完,控制台报 401

现象:地图区域一片空白,控制台打出A valid Mapbox access token is required或者个别请求 401。原因:style JSON 里某个 source 的 url 还是mapbox://tiles/...开头,引擎解析到这里时尝试用 token 换取官方地址,没拿到 token 就报错。解决:全文搜索 style 文件里所有mapbox://字符串,逐个替换成自托管地址。不要只看sprite和glyphs字段,source 里的url字段才是最大漏网之鱼。

5.2 glyphs 字体 URL 模板缺占位符,文字直接不显示

现象:地图底图、瓦片、图标都正常,唯独标注文字全部消失,network 面板里看不到任何字体请求,或者字体请求 404。原因:glyphs字段写成固定路径,少了{fontstack}或{range}占位符,引擎不知道按什么规则拼 URL。解决:检查 style JSON 里的 glyphs 字段,确保模板形如/assets/fonts/{fontstack}/{range}.pbf。同时检查字体目录命名,如果样式里用的字体名是Open Sans Regular,那目录名必须一模一样;请求时 URL 里会变成Open%20Sans%20Regular,服务器端要保证 URL decode 后能找到对应目录。

5.3 sprite 雪碧图的 JSON 与 PNG 不配套,图标错位或空白

现象:地图上的 POI 图标全部显示为空白方块,或者图标位置偏差了几十像素。原因:sprite 的 JSON 文件里记录的图标坐标、尺寸和 PNG 实际像素位置对不上,引擎按错误坐标裁剪出空白区域。解决:重新生成 sprite 资源,确认 style 里的 sprite 前缀能同时匹配到.json和.png两个文件。如果项目里有@2x版本,还要确认pixelRatio字段是 2,否则高 DPI 屏上图标会模糊或偏移。

5.4 瓦片服务没开 CORS,浏览器拦截导致地图断瓦

现象:network 面板里瓦片请求显示CORS error,地图拖动后大范围空白,刷新偶尔又能加载一部分。原因:mapbox-gl 通过 fetch 或 XMLHttpRequest 加载瓦片,跨域请求要求服务端返回Access-Control-Allow-Origin头。自托管瓦片服务如果没配置,浏览器会拦截响应。解决:在 Nginx 或网关层加上跨域响应头,允许来源为你的前端域名。像这样配置:

add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods "GET, OPTIONS";

注意*通配符在带凭证的请求里不生效,如果瓦片服务需要 Cookie 鉴权,得把*改成具体来源域名。

5.5 源码构建后 worker 报错,token 判断逻辑没删干净

现象:按第四章改完源码,构建成功,运行时却报Error: A valid Mapbox access token is required,或者地图能出图但 console 里有未捕获的 Promise 异常。原因:mapbox-gl 2.7.0 的 worker 线程有自己的代码副本,某些版本会把 token 校验逻辑打进 worker bundle 里,只改主线程的config.js不够。解决:构建产物里搜索requireAccessToken字符串,看是不是出现在 worker 相关的代码段里。如果是,定位到 worker 源文件里对应的引用点,用同样思路改成直接返回或直接通过。不熟悉构建链路的,建议用transformRequest兜底,比改 worker 代码安全得多。

6. 验证去 token 是否干净:抓请求与离线加载测试

6.1 三步验证法

去 token 做得干不干净,不能只看地图能不能出来,得把浏览器当成黑匣子从外部验证。我的习惯是三步走。第一步,清除本地可能残留的 token 缓存,在控制台执行:

// 清掉 localStorage 里所有 mapbox 相关缓存 Object.keys(localStorage) .filter(key => key.toLowerCase().includes('mapbox')) .forEach(key => localStorage.removeItem(key)); location.reload();

本地存储里有历史 token 的话,getAccessToken()会在你没传全局 token 时自动读到它,导致验证结果失真。清掉再刷新才是真实环境。第二步,在 network 面板筛选api.mapbox.com和events.mapbox.com两个域名,确认没有任何请求发出。如果 filtered 列表为空,说明官方域名已经完全退出请求链路。第三步,有条件就在断网环境或内网环境跑一遍完整加载流程,重点观察瓦片、字体、图标三样是否齐全,地图拖动、缩放时是否出现局部空白。

6.2 用空字符串 token 跑一次,把验证沉淀成习惯

最后给一个我自己的收尾习惯:把初始化代码里的accessToken显式写成空字符串,再跑一次全流程。

const map = new mapboxgl.Map({ container: 'map', style: '/assets/style.json', accessToken: '', attributionControl: false, maxPitch: 60 }); map.on('error', (e) => { // 去 token 后这里不应出现与 token 相关的错误 console.error(e.error); });

这个写法和不写accessToken字段有本质区别。不写的话,引擎会回溯读全局变量mapboxgl.accessToken,一旦页面里其他模块设置过全局 token,你的验证就白做了。显式传空字符串,直接切断这层回溯,确保引擎不会从任何渠道拿到 token。error事件监听是我调试时必留的一行,很多渲染层的问题不会抛全局异常,只会安静地触发这个事件,提前挂上监听能少走弯路。

这个验证习惯我建议保留到每次改动样式资源之后。换过一次瓦片服务后,字体加载经常出问题,顺手跑一遍这套验证,几分钟就能定位是清 token 的问题还是资源本身的问题。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进