我在早几年的项目里,凡是要给客户演示地图效果,十有八九第一反应就是先调一个天地图出来。不是因为它有多酷,而是因为做WebGIS的人心里都有数:天地图是国家级的地理信息公共服务平台,稳定、免费、支持高并发,而且它的坐标体系对国内项目来说几乎就是默认选项。哪怕你现在用的是高德、百度或者Mapbox风格的地图方案,一谈到数据合规、精度校验,绕来绕去还是会回到天地图。
这篇博文我打算从头到尾带你过一遍“加载天地图”的完整实战过程。不是简单丢给你一段拼好的代码,而是把为什么这么做、换一种做法会踩什么坑、遇到瓦片加载失败该怎么查,全部拆开讲清楚。无论是刚接触WebGIS的初学者,还是被底图配置折腾过几次的开发者,这篇文章应该都能给你一些可以直接抄作业的东西。
1. 为什么把“加载天地图”作为WebGIS实战的切入点
很多人在学习WebGIS的时候,第一步都会卡在“底图从哪里来”。用在线地图服务吧,总觉得不太踏实,不知道哪天key就失效了;用离线瓦片吧,又要自己下载、切片、配服务器,学习成本一下子拉高。相比之下,天地图几乎是学习曲线最平滑的一条路。
从技术层面看,加载天地图只是向服务器请求一张张瓦片图片,然后把它们拼接成完整地图。这个“瓦片切片”的概念是所有WebGIS底图的基础,你把这个过程吃透了,后面再接触任何地图服务都会觉得似曾相识。从业务层面看,天地图提供矢量、影像、地形三种常用底图,跨度够大,足够你模拟真实项目里切换图层、叠加业务数据、做坐标转换等一堆典型操作。
更关键的一点是,天地图的门槛低。你需要准备的只有一件事:注册一个账号,申请一个密钥(token)。官网提供了非常清晰的开发者接入流程,整个申请过程基本上几分钟就能搞定。拿到密钥之后,你既可以走官方JavaScript API,也可以用Leaflet、OpenLayers这类开源框架自己加载瓦片,选择面很灵活。
有人可能会问,既然官方API都给了,为什么不直接用,反而要去学Leaflet加载?这个问题问得好。官方API能让你最快看到效果,但它往往是一个封闭的生态:地图容器、图层控制、交互逻辑都被封装在某一套约定里,你想把天地图和自己的数据可视化代码整合到一起,很多时候会束手束脚。而用Leaflet、OpenLayers这类通用框架,你可以在同一个地图容器里自由叠加GeoJSON、接入自己的后端服务、绑定任意图形绘制插件,对整个数据流的掌控会强很多。这也是我把“用通用框架加载天地图”作为这次实战核心的原因。
2. 准备工作:密钥申请与基础配置
2.1 注册账号与创建应用
在天地图官网(我们平时统称“天地图”)注册一个开发者账号。路径一般是“开发资源”或“用户中心”下面找到创建应用的入口。创建应用时需要填写应用名称、应用类型等信息,这些根据你自己的项目情况填就行,比如“某跨平台系统的地图底图模块”。提交之后,系统会生成一个key,这个key就是之后所有瓦片请求的通行证。
这里有一个很多人容易忽略的细节:同一个账号下可以创建多个应用,每个应用对应独立的key。我建议大家不要图省事把所有项目都塞到同一个应用里,因为一旦某个项目的数据量过大,可能会影响其他项目的正常使用。分开管理,每个项目各用各的key,排查问题的时候也方便定位。
创建完成后,天地图还会让你设置“浏览器端”还是“服务端”的应用场景。简单说,如果是在网页里用JavaScript请求瓦片,就选浏览器端;如果是在后端程序里做地图服务请求,就选服务端。这个选择决定了后续key的使用方式和配额限制,建议先搞清楚自己的场景再选。
2.2 密钥的作用与常见误区
天地图的瓦片请求地址里必须带上tk参数(也就是key),不带的请求会被拒绝。这个机制和大多数地图服务一样,目的在于统计调用量、限制恶意请求。
关于密钥,我见过不少新手踩坑,归结起来有几种典型情况:
- 刚申请的key没生效就开始请求,结果返回一堆错误。天地图的key不是即拿即用的,短则几分钟、长则十几分钟才能同步生效,建议申请后稍等片刻再测试。
- 弄混了浏览器端和服务端的key,不同场景的key不能互换用。
- 把key写死在公开代码里,又怕被人盗用。虽然大部分在线地图key对域名做了白名单管理,但你最好还是在天地图后台把域名白名单配置好,避免key被拿到其他站点盗刷。
另外要提醒的是,天地图的瓦片服务对单次请求的URL有长度和参数格式要求,参数顺序不能乱。比如WMTS(Web Map Tile Service)请求里,LAYER、STYLE、TILEMATRIXSET这些字段一个都不能少,少了直接报错。这些细节我们下一节具体展开。
3. 加载天地图的两种主流方案
3.1 方案一:用Leaflet加载天地图WMTS瓦片
先来认识一下WMTS。WMTS是OGC(开放地理空间信息联盟)制定的一个瓦片服务标准,核心思想很简单:把地图按固定的比例尺切分成无数个小方块,客户端按需请求当前视野范围内的那些方块。天地图的瓦片服务就是基于WMTS标准发布的。
在Leaflet里加载WMTS瓦片,最直接的思路是用L.tileLayer。比如加载矢量底图的URL模板大致长这样:
L.tileLayer( 'https://t{0-7}.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=你的key', { minZoom: 1, maxZoom: 18, subdomains: ['0', '1', '2', '3', '4', '5', '6', '7'], noWrap: true, attribution: '© 天地图' } ).addTo(map);这段代码里的URL看起来挺长,但拆开看并不复杂。vec_w表示矢量图层的英文缩写,wmts表示请求的是WMTS服务,{z}、{y}、{x}是Leaflet自动替换的层级、行号、列号。t{0-7}表示请求会轮流打到8个子域名上,这样做可以绕过浏览器对同一域名并发连接数的限制,让瓦片加载得更快。
需要注意的是,Leaflet原生并没有直接内置WMTS解析,上面这种写法其实是把WMTS的请求参数拼成一个符合瓦片规则的URL,然后交给L.tileLayer去拉。这种“手动拼URL”的方式删掉了额外的插件依赖,比较容易让新手理解瓦片请求的本质。如果你想更规范一些,也可以引入leaflet-tilelayer-wmts这个插件,它会把WMTS的复杂度包装好。
3.2 方案二:用OpenLayers加载天地图
OpenLayers对WMTS有原生支持,它内置了WMTS图层类和数据源类。使用OpenLayers时,你不再需要手拼URL,而是通过createFromCapabilities或手动配置请求参数来加载。下面是一个手动配置的示范:
import TileLayer from 'ol/layer/Tile'; import WMTS from 'ol/source/WMTS'; import WMTSTileGrid from 'ol/tilegrid/WMTS'; const projection = getProjection('EPSG:3857'); const projectionExtent = projection.getExtent(); const size = getWidth(projectionExtent) / 256; const resolutions = new Array(19); const matrixIds = new Array(19); for (let z = 0; z < 19; ++z) { resolutions[z] = size / Math.pow(2, z); matrixIds[z] = z; } const tiandituLayer = new TileLayer({ source: new WMTS({ url: 'https://t{0-7}.tianditu.gov.cn/vec_w/wmts', layer: 'vec', matrixSet: 'w', format: 'tiles', projection, tileGrid: new WMTSTileGrid({ origin: [-180, 90], resolutions, matrixIds }), style: 'default', requestEncoding: 'KVP', crossOrigin: 'anonymous', params: { tk: '你的key' } }) });OpenLayers这种写法看起来代码量更大,但它把投影、分辨率、瓦片网格都配置得明明白白。如果你后续要加载WMS服务,或者对接本地切片的瓦片,这种细致程度就会变成优势。
3.3 方案对比与选型建议
我自己的实际感受可以整理成一张对比表:
| 对比项 | Leaflet | OpenLayers |
|---|---|---|
| 上手难度 | 低,API简单直观 | 中高,概念较多 |
| WMTS支持度 | 原生不支持,需手拼URL或插件 | 原生支持,配置项完整 |
| 生态插件 | 丰富,尤其适合轻量交互 | 更偏专业GIS分析 |
| 代码体积 | 轻量,适合快速交付 | 相对重,适合复杂应用 |
| 适合场景 | 展示型项目、可视化大屏 | 数据编辑、空间分析、复杂图层管理 |
我的建议是:如果你是第一次接触WebGIS,想快速看到“地图加载出来”的成就感,优先选Leaflet,它几乎没有心智负担。如果你的项目要做到图层较多、坐标变换频繁、甚至需要在地图上做空间查询和分析,那OpenLayers能让你少走很多弯路。
不过不管选哪个框架,最终我们接触的都是同一套天地图瓦片服务,理解底层请求和坐标系才是最重要的。
4. 坐标系与投影的坑
4.1 为什么说坐标系是WebGIS的第一道门槛
地图坐标系这件事,新手往往觉得抽象,但实际项目中它却是最容易翻车的环节。市面上主流的在线地图,各自选了不同的投影方式:有的用Web Mercator(EPSG:3857),有的在Web Mercator基础上又自定义了一套偏移加密坐标体系。天地图却把两种都做了,场景不同用的坐标系也不同。
天地图在官方说明中明确指出,它的经纬度坐标基于CGCS2000(2000国家大地坐标系),虽然CGCS2000和WGS84在常规尺度下差异非常小,但在测绘级项目中一定要当作不同坐标系来处理。更关键的是,在Web Mercator投影下,天地图采用了EPSG:3857标准,这就和大多数开源地图库的默认坐标系保持一致了,加载起来相对省心。
如果你直接用官方API,可能感觉不到坐标系的存在;但一旦你用Leaflet或者OpenLayers自己加载瓦片,坐标系就会变得无处可藏。尤其是当你需要在底图之上叠加自己业务数据的时候,那核心问题在于:你的数据到底是经纬度坐标,还是投影坐标?你的地图容器到底工作在哪个投影下?这两个问题不解决,数据错位只是迟早的事。
4.2 坐标系不一致的表现与解决
最常见的错误是:业务数据用的是GPS采集的经纬度(WGS84),底图却是天地图影像底图,并且没有做任何坐标转换。结果是数据点在影像图上有明显偏移,市区里可能偏几米,郊区甚至偏几十米。
天地图在不同图层上的坐标系设计一直有版本调整:矢量底图和影像底图在不同时期的服务中,对投影和加密的规定不完全一样。因此我强烈建议你,在任何项目开始前,先到天地图的官方文档里查一下当前使用的服务对应的坐标参考体系。不要凭“以前是这样”“上次那样可以”的经验主义来对待坐标系。
落到代码层面,如果用Leaflet加载EPSG:3857的天地图底图,那么你在添加业务图层时,要么把数据转成3857投影坐标,要么让Leaflet帮你做“经纬度->Web Mercator”的转换。Leaflet默认支持WGS84经纬度坐标输入,但这里隐藏了一个坑:如果底图是天地图影像(同样被要求使用EPSG:3857投影),你却直接叠一个WGS84的GeoJSON上去,虽然Leaflet内部会帮你转,但转换精度和你数据的原始精度不一定匹配,最后呈现出的偏差容易被误判为“天地图瓦片有问题”。
稳妥的做法是:先确认你拿到数据时的坐标系,再统一转换到和底图一致的目标坐标系。转换工具有很多,前端可以用proj4js,后端可以用各类GIS库。总之,“不做转换就直接叠加”是WebGIS里最常见的错误之一。
5. 实战代码:从零搭建一个可复用的加载模块
5.1 完整的Leaflet加载天地图示例
下面这套代码,我在自己多个项目里改来改去,基本可以作为一套模板来用。它包含了矢量底图、影像底图和标注图层。
// index.html 里引入 Leaflet 的 CSS 和 JS:略(依赖CDN) // 关键是JS逻辑部分 const TIANDITU_KEY = '你的天地图key'; const map = L.map('map', { center: [34.3412, 108.9398], zoom: 12, zoomControl: true }); // 矢量底图 const vecLayer = L.tileLayer( `https://t{0-7}.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${TIANDITU_KEY}`, { subdomains: ['0', '1', '2', '3', '4', '5', '6', '7'], maxZoom: 18, noWrap: true, attribution: '© 天地图' } ); // 影像底图(注意LAYER是img) const imgLayer = L.tileLayer( `https://t{0-7}.tianditu.gov.cn/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${TIANDITU_KEY}`, { subdomains: ['0', '1', '2', '3', '4', '5', '6', '7'], maxZoom: 18, noWrap: true, attribution: '© 天地图' } ); // 影像标注层(LAYER是cia) const cvaLayer = L.tileLayer( `https://t{0-7}.tianditu.gov.cn/cia_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${TIANDITU_KEY}`, { subdomains: ['0', '1', '2', '3', '4', '5', '6', '7'], maxZoom: 18, noWrap: true, attribution: '© 天地图' } ); vecLayer.addTo(map); // 图层切换控制 L.control.layers( { '矢量底图': vecLayer, '影像底图': imgLayer }, { '影像标注': cvaLayer }, { position: 'topright' } ).addTo(map);一个小技巧值得说明:在影像模式下,如果你想既看到高清卫星影像,又清晰显示道路地名,可以把imgLayer和cvaLayer叠在一起,也就是影像底图上追加一个“文字标注图层”。这个方案在展示地图效果时特别好用,视觉信息更丰富,也不会像直接切到矢量图那样少了实景感。
5.2 叠加业务图层的关键配置
底图加载完成后,直接叠加业务数据是最常见动作。以Leaflet为例,给地图添加一个GeoJSON数据层,同时自定义每个要素的样式和弹出窗口:
fetch('某业务数据接口') .then(res => res.json()) .then(data => { const businessLayer = L.geoJSON(data, { pointToLayer: (feature, latlng) => { return L.circleMarker(latlng, { radius: 8, color: '#ff6600', weight: 2, fillOpacity: 0.6, fillColor: '#ffaa33' }); }, onEachFeature: (feature, layer) => { layer.bindPopup(`名称:${feature.properties.name}<br>状态:${feature.properties.status}`); } }).addTo(map); map.fitBounds(businessLayer.getBounds(), { padding: [30, 30] }); });同样的,如果业务数据是一堆分散的经纬度点位,也可以直接循环添加Marker。需要提醒的是,如果点位数量很大,比如几千上万个,直接用Marker会明显卡顿,这时候要么转为Canvas图层渲染,要么做聚合处理。这个问题虽然不属于“加载天地图”本身,但它几乎是所有WebGIS项目紧接着会遇到的问题。
5.3 样式与交互优化
除了能加载,还要加载得顺眼。天地图的矢量底图默认是浅色系,比较百搭,但如果你需要做深色大屏,直接上天地图的默认配色会很违和。对此我有两个替代思路:
一是使用天地图提供的影像底图,在夜间大屏里,影像底图往往比矢量底图更有“科技感”。二是通过CSS滤镜对底图做颜色处理,比如给imgLayer的瓦片容器加一层filter: grayscale(0.5) brightness(0.8),可以让卫星图整体变暗,让业务数据点更突出。这个方法简单粗暴,实测下来很稳。
还有一个交互细节:地图滚轮缩放默认是否启用,要按照大屏场景来决定。在展会大屏上,通常建议关掉拖拽和滚轮,防止观众随手一滑把地图弄得不知道跑哪去了:
map.dragging.disable(); map.scrollWheelZoom.disable();当然,这只是针对展示型场景。如果你做的是需要用户操作的产品页面,就别这么干,否则体验会很难受。
6. 常见问题排查与避坑实录
6.1 瓦片加载失败、白屏看不出错
这个问题是我遇到过最多的。页面打开,左下角坐标在动,网格也在动,就是一片白。打开控制台一看,那张瓦片的请求要么返回403,要么返回跨域错误。
排查路径很简单,先直接手动拼一个瓦片URL粘贴到浏览器地址栏里访问,看看能不能正常加载到图片。如果可以,说明问题出在页面侧的key或跨域设置;如果不可以,先检查key是否有效、是否已经生效、是否选关了对应的应用类型。如果提示跨域,需要在天地图后台检查域名白名单配置。
如果确认key没问题,再看一下URL里的LAYER参数写没写对。vec是矢量,img是影像,cia是影像标注,cva是矢量标注,这几种最常用。写错一个单词,服务端就直接报错。
6.2 瓦片错位、加载出来显示乱码
瓦片在屏幕上错位,排列不整齐,网上能搜到的说法五花八门,但绝大多数都指向同一个原因:瓦片坐标系的定义和地图容器的坐标系设置不一致。
比如地图容器用的是EPSG:4326的经纬度显示方式,而天地图瓦片本身是按EPSG:3857切的,两者之间如果没有转换,瓦片拿到手之后彼此之间就会产生偏移。Leaflet通常默认使用3857,但如果你的地图容器自定义过坐标系,就要特别小心。
解决办法:尽量统一到EPSG:3857投影。如果你的业务数据是经纬度,那就在叠加时做一次转换。这里写一段Leaflet中比较容易被忽略的细节:路径取maxZoom时,有些选项卡片的服务在19级才开始提供更好的切片,而有些服务在18级就停了,超过最大级别后再往里缩放,底图会模糊成一片。因此要合理设置maxZoom,不要把maxZoom设为无限延伸。
6.3 加载速度慢,幻灯片式一顿一顿
除开网速因素,最常见的原因是请求并行度太低。浏览器对同一域名下的并发请求数是有限制的,天地图官方提供了t0到t7共8个子域名,目的就是让你把请求分散到不同子域。如果忽略了子域名配置,直接写死t0,瓦片加载速度会明显变慢。
另一个拖慢速度的原因是,页面里同时加载了太多图层。有些项目一开始就把矢量底图、影像底图、标注层全都addTo(map),结果本来两张图就够用的场景,非得同时请求三四套瓦片,速度自然慢。我的习惯是:默认只加一个底图和一个需要的标注层,其他图层存在于图层管理器里,用户切换时才加入。
6.4 常见问题速查表
| 现象 | 大概率原因 | 处理建议 |
|---|---|---|
| 瓦片请求403 | key无效、未生效或域名白名单未配置 | 检查key状态和后台白名单设置 |
| 跨域报错 | 应用类型选错(浏览器端/服务端)或跨域请求被拦截 | 确认应用类型,检查是否使用了匿名跨域参数 |
| 底图空白 | URL中LAYER写错、TILEMATRIXSET不匹配 | 对照官方示例逐一检查参数 |
| 瓦片错位 | 坐标系不一致 | 统一底图和业务数据坐标系为EPSG:3857 |
| 加载缓慢 | 未使用子域名或图层叠加太多 | 配置subdomains,精简默认图层 |
| 数据点偏移 | 业务数据坐标系与底图不一致 | 先做坐标转换再叠加 |
7. 从加载天地图到一个完整的WebGIS项目
7.1 下一步:叠加矢量数据和地物标绘
底图加载稳定后,你就可以开始玩更有意思的东西了。比如在天地图上叠加自己后端的空间数据接口,做点线面要素的实时展示;或者接入地图绘制插件,让用户在地图上画范围、标位置、量距离。这些都是WebGIS产品的高频操作。
以叶面绘制为例,在Leaflet环境下,绘制插件通常支持点、线、面、矩形、圆形等图形。我之前在某项目里用天地图做底图,叠加了绘制功能后,客户可以直接在影像图上圈出自己的业务范围,再辅助导出中心点和面积。这个流程看起来平平无奇,但客户信任度提升得很明显,因为“看得见”的底图 + “画得出”的交互,就是他们对GIS最直观的理解。
7.2 进阶思路:本地瓦片与天地图结合
如果以后项目里遇到了内网部署,无法访问公网天地图,你就需要考虑本地瓦片方案。常见做法是:在公网环境用天地图瓦片工具按需要下载指定层级的瓦片,然后发布到本地服务器,再通过Leaflet的L.tileLayer指到本地瓦片路径。
这个过程中有几个坑要记住:一是下载瓦片时需要固定官方标准切片格式,否则后面切出来接缝很多;二是本地瓦片的URL层级和行列号必须符合切片规则,不能随意修改目录名;三是在内网使用本地瓦片时,地图的坐标系定义仍要保持和底图一致,才能保证业务数据不错位。
我从第一次加载天地图到现在,深切的感受是:看起来只是一个“把瓦片挂上去”的操作,但牵涉的坐标系、跨域、缓存、并发、图层管理这些点,几乎覆盖了WebGIS开发里绝大多数的基本功。把这张底图彻底吃透,你后续玩WMS、矢量切片、地图可视化大屏都会顺畅很多。
如果你卡在某个环节,建议按顺序排查:先确认key能访问,再确认瓦片参数对,然后确认坐标系统一,最后检查页面里的并发和缓存设置。99%的问题都在这个链条里。这也是我这些年踩坑之后总结出的唯一一条经验,希望你少走一些弯路。