news 2026/10/20 7:55:58

天地图离线地图内网部署:js、css与瓦片加载方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
天地图离线地图内网部署:js、css与瓦片加载方案

简介:这份资源面向需要在无外网环境下部署地图服务的前端与内网应用开发者,提供天地图离线化所需的 JavaScript 与 CSS 源码,解决内网无法直连天地图在线接口、地图瓦片与控件无法加载的问题。压缩包共 5 个文件,包含 4 个 js 与 1 个 css,整体约 100KB,其中 js 文件承担天地图 API 封装、请求地址替换、瓦片加载与坐标转换等逻辑调整,css 文件负责图层控制、比例尺、指北针等界面元素的样式定制,使离线状态下的交互体验尽量贴近在线效果。目前已有 2358 人学习下载,说明该方案在受限网络场景中具有较高参考价值。读者可据此理解离线地图的请求改写思路、本地瓦片服务对接方式与控件样式适配方法,并结合自身内网环境调整服务器地址,快速搭建可用的天地图离线显示方案。

1. 天地图离线地图的 js 和 css:内网环境下的瓦片加载方案

很多做内网 GIS 项目的开发者都遇到过同一个尴尬:地图 API 的 js 和 css 文件在外网环境引用得好好的,一旦部署到没有外网出口的局域网里,地图容器直接白屏,控制台一堆ERR_NAME_NOT_RESOLVED或者net::ERR_CONNECTION_TIMED_OUT。天地图离线地图使用的 js 和 css,本质上要解决的就是「把在线地图服务搬到内网」这件事——不是简单地把几个文件下载下来丢进项目目录就完事,而是要理清 API 脚本、样式表、瓦片图片、字体图标这四类资源各自的加载链路,再逐一替换成本地路径。

这套方案适合三类人:一是做政务、能源、军工等内网系统的前端,地图必须离线可用;二是做展厅大屏、车载导航的开发者,网络不稳定但地图不能断;三是想彻底搞懂地图 API 加载机制的工程师,把在线依赖拆开看一遍比读文档管用。下面按「先搞清依赖关系,再动手替换,最后处理踩坑」的顺序讲透。

2. 拆解天地图 API 的加载链路:哪些文件必须本地化

2.1 从一次白屏排查说起:js、css、瓦片、字体的四层依赖

我接手过一个内网项目,地图容器高度设了 600px,但页面上就是一片灰。打开 Network 面板,发现请求分四层:

第一层是 API 主脚本,通常叫api.js或带版本号的t.js,它负责初始化T这个全局对象,挂载T.Map、T.Marker、T.TileLayer等构造函数。第二层是样式表,控制缩放按钮、比例尺、版权信息的视觉呈现,缺了它地图能出来但控件会错位。第三层是瓦片图片,这是地图的「肉」,按 z/x/y 的规则从瓦片服务器拉取,离线场景下必须换成自己的瓦片服务或本地目录。第四层是字体图标,比如缩放按钮上的+-符号,很多版本用 iconfont 实现,css 里通过@font-face引用.woff或.ttf文件。

这四层里,js 和 css 是「骨架」,瓦片是「肉」,字体是「皮肤」。骨架不本地化,地图对象根本创建不出来;肉不本地化,地图出来也是空白格子;皮肤不本地化,控件会显示成方块或乱码。很多人只替换了 js,结果地图能初始化但瓦片 404,误以为是 js 版本不对,其实是瓦片地址没改。

提示:排查离线地图问题时,先看控制台有没有T is not defined,有就是 js 没加载成功;再看有没有大量 404 的图片请求,有就是瓦片路径没配对。

2.2 用浏览器开发者工具抓出真实的资源清单

不要凭记忆列文件,直接抓。以某次内网部署为例,操作步骤如下:

打开外网环境下能正常显示地图的页面,按 F12 进 Network 面板,勾选Disable cache,刷新页面。在筛选框输入tianditu或你用的服务域名,把请求按类型分组。

// 在 Console 里执行,快速统计各类资源的请求数量和域名 const resources = performance.getEntriesByType('resource'); const summary = {}; resources.forEach(r => { const url = new URL(r.name); const key = url.hostname + url.pathname.split('/').slice(0, 3).join('/'); summary[key] = (summary[key] || 0) + 1; }); console.table(summary);

这段代码利用 Performance API 拿到当前页面所有资源的加载记录,按「域名 + 路径前三段」聚合。执行后会看到类似api.tianditu.gov.cn/api出现 1 次,t0.tianditu.gov.cn/DataServer出现几十次(瓦片),api.tianditu.gov.cn/api/css出现 1 次。参数说明:performance.getEntriesByType('resource')返回的是当前页面已加载的资源数组,new URL()用来解析出域名和路径,slice(0,3)是为了把瓦片请求的 z/x/y 参数截掉,方便聚合。

抓到清单后,把 js 和 css 的完整 URL 复制出来,用下载工具保存到本地。注意 css 文件里可能还有url()引用,比如背景图、字体文件,这些也要一并下载。常见做法是:下载 css 后打开搜索url(,把里面引用的相对路径资源按原目录结构放好。

2.3 本地目录结构怎么摆:一个可复用的组织方式

我一般会按下面的结构组织离线资源,放在项目的public或static目录下:

static/ tianditu/ api/ api.js api.css tiles/ {z}/{x}/{y}.png fonts/ tdt-icons.woff tdt-icons.ttf

api.js和api.css放同一层,方便在 HTML 里用相对路径引用。瓦片按z/x/y分目录,这是绝大多数地图库的通用约定,后面配瓦片服务时直接指向这个根目录即可。字体文件放在fonts下,css 里的@font-face的src要改成相对这个 css 文件的路径。

如果瓦片数量大,不建议放项目目录里,而是单独起一个静态文件服务,用 Nginx 或 Python 的http.server托管,前端通过一个内网 IP 访问。这样前端打包体积不会爆炸,瓦片更新也不用重新构建前端。

3. 把在线 js 和 css 改成本地引用:路径替换与初始化参数

3.1 下载与路径替换:从 CDN 地址到本地 static 目录

假设抓到的 js 地址是http://api.tianditu.gov.cn/api?v=4.0&tk=你的密钥,css 地址是http://api.tianditu.gov.cn/api/css?v=4.0。下载时要注意,带查询参数的 URL 直接保存可能拿到的是重定向后的内容,用curl -L跟随跳转:

# 下载 js 和 css 到本地目录,-L 跟随重定向,-o 指定输出文件名 curl -L "http://api.tianditu.gov.cn/api?v=4.0&tk=YOUR_KEY" -o static/tianditu/api/api.js curl -L "http://api.tianditu.gov.cn/api/css?v=4.0" -o static/tianditu/api/api.css # 检查文件大小,正常 js 在几百 KB 量级,css 在几十 KB ls -lh static/tianditu/api/

参数说明:-L是关键,不加的话如果服务端返回 302,你保存下来的可能是一个空文件或跳转页面。YOUR_KEY替换成实际密钥,但离线场景下密钥校验可能失效,后面会讲怎么处理。下载完用ls -lh看大小,如果 js 只有几 KB,大概率是下载到了错误页面,用head -c 200 api.js看开头是不是 HTML。

替换时,在 HTML 里把原来的 CDN 引用改成相对路径:

<!-- 原来 --> <link rel="stylesheet" href="http://api.tianditu.gov.cn/api/css?v=4.0"> <script src="http://api.tianditu.gov.cn/api?v=4.0&tk=YOUR_KEY"></script> <!-- 替换后 --> <link rel="stylesheet" href="/static/tianditu/api/api.css"> <script src="/static/tianditu/api/api.js"></script>

注意去掉tk参数。离线环境下密钥校验请求发不出去,有些版本的 API 会因为校验失败而不初始化地图对象。如果去掉后地图仍不显示,需要在 js 里搜索tk或token相关逻辑,看是否有同步的校验分支。

3.2 初始化代码怎么写:本地瓦片服务的对接方式

js 和 css 本地化后,地图对象能创建了,但瓦片默认还是去外网拉。需要显式指定本地瓦片服务地址。以常见的T.TileLayer为例:

// 初始化地图,中心点和级别按实际需求改 var map = new T.Map('mapContainer'); map.centerAndZoom(new T.LngLat(116.397, 39.908), 10); // 用本地瓦片服务替换默认图层 var localLayer = new T.TileLayer('/static/tianditu/tiles/{z}/{x}/{y}.png', { minZoom: 1, maxZoom: 18, tileSize: 256, errorTileUrl: '/static/tianditu/tiles/blank.png' // 瓦片缺失时的占位图 }); map.addLayer(localLayer);

参数说明:{z}/{x}/{y}是模板占位符,地图库会自动替换成当前层级的瓦片编号。minZoom和maxZoom要和实际下载的瓦片层级一致,如果本地只有 1 到 10 级,却设了 18,放大到 11 级以后就会全是空白。tileSize一般是 256,少数高清瓦片是 512,设错会导致瓦片错位或模糊。errorTileUrl建议配一张 1x1 的透明 PNG,避免缺失瓦片时控制台刷 404。

如果瓦片服务是独立部署的,把路径换成http://内网IP:端口/tiles/{z}/{x}/{y}.png即可。注意跨域问题,瓦片服务要允许前端域名的跨域请求,Nginx 里加add_header Access-Control-Allow-Origin *;。

3.3 密钥校验与跨域:两个最容易卡住的配置点

密钥校验是离线场景下最玄学的一环。在线时,API 会拿tk去服务端换一个临时 token,离线后这个请求必然失败。不同版本的 API 处理方式不同:有的版本校验失败后仍然初始化地图,只是控制台报错;有的版本直接不执行后续逻辑,导致T对象不完整。

我一般会先去掉tk参数试一次,如果地图能出来但控件不全,说明是校验分支影响了部分逻辑。这时在api.js里搜索XMLHttpRequest或fetch,找到发校验请求的位置,看失败回调里做了什么。常见做法是注释掉校验请求,或者把失败回调里的return去掉,让代码继续往下走。但要注意,修改后的 js 不要提交到公开仓库,仅限内网使用。

跨域问题主要出在瓦片服务上。如果前端页面是http://localhost:8080,瓦片服务是http://192.168.1.100:9000,浏览器会拦截瓦片请求。解决方式有两种:一是瓦片服务加 CORS 头,二是用前端开发服务器的代理功能把瓦片请求转发到内网地址。以 Vite 为例:

// vite.config.js export default { server: { proxy: { '/tiles': { target: 'http://192.168.1.100:9000', changeOrigin: true, rewrite: path => path.replace(/^\/tiles/, '') } } } };

这样前端代码里写/tiles/{z}/{x}/{y}.png,开发服务器会转发到内网瓦片服务,生产环境用 Nginx 做同样的反向代理即可。

4. 离线地图避坑排查:白屏、瓦片 404、控件错位的处理

4.1 地图白屏但控制台无报错

现象:页面加载完,地图容器有高度,但里面什么都没有,控制台干净得可疑。

原因:最常见的是 css 没加载成功,地图容器的position或overflow被覆盖,导致瓦片虽然请求了但被裁剪掉。另一种可能是 js 加载了但初始化代码在 DOM 就绪前执行,mapContainer还没渲染出来。

解决:先检查 css 的<link>标签是否 200,再看地图容器的计算样式里position是不是relative或absolute。如果是初始化时机问题,把初始化代码包在DOMContentLoaded事件里,或者把<script>放到</body>前。

4.2 瓦片请求 404 但路径看起来没错

现象:Network 里瓦片请求全是 404,但手动拼接的 URL 在浏览器里能打开。

原因:瓦片服务的路径规则和地图库的模板不匹配。比如地图库请求的是/tiles/10/123/456.png,但瓦片服务实际目录是/tiles/10/123/456/0.png(多了一层),或者瓦片文件名是.jpg不是.png。

解决:在 Network 里复制一个 404 的完整 URL,和实际文件路径逐段对比。重点看层级数、文件扩展名、是否有前导零(有些瓦片服务要求 z 补零成两位)。改T.TileLayer的 URL 模板或改瓦片服务的目录结构,两边对齐即可。

4.3 缩放按钮显示成方块或乱码

现象:地图能显示,但左上角的+-按钮变成了方块,或者显示成□。

原因:字体图标没本地化。css 里@font-face的src还指向外网地址,离线后字体加载失败,浏览器回退到默认字体,而默认字体里没有这些图标字形。

解决:下载字体文件,放到fonts目录,改 css 里的src:

@font-face { font-family: 'tdt-icons'; src: url('../fonts/tdt-icons.woff') format('woff'), url('../fonts/tdt-icons.ttf') format('truetype'); font-weight: normal; font-style: normal; }

注意路径是相对于 css 文件的,不是相对于 HTML。改完清缓存刷新,图标应该就回来了。

4.4 地图能显示但拖拽卡顿

现象:离线地图加载出来了,但拖拽、缩放时明显掉帧,不如在线流畅。

原因:瓦片是本地文件服务托管的,如果服务端没配缓存头,每次拖拽都重新请求瓦片,磁盘 IO 扛不住。另一种可能是瓦片图片没压缩,单张几百 KB,浏览器解码慢。

解决:给瓦片服务加Cache-Control和ETag头,让浏览器缓存已加载的瓦片。Nginx 配置:

location /tiles/ { root /data/tianditu; expires 30d; add_header Cache-Control "public, immutable"; }

同时用工具批量压缩瓦片,PNG 可以用pngquant,JPG 用jpegoptim,通常能压掉 50% 以上体积。

4.5 内网部署后部分瓦片缺失

现象:大部分区域正常,但某些区域或某些层级瓦片是空白。

原因:下载瓦片时没下全。可能是下载脚本中断了,或者瓦片服务对某些区域本来就没有数据。

解决:写个脚本检查瓦片完整性,对比实际文件数和理论数量。理论数量按层级算:第 z 级有 4^z 张瓦片(Web 墨卡托投影下)。如果实际数量远小于理论值,说明下载不全,需要补下。补下时注意去重,已存在的文件跳过。

5. 进阶:用 Service Worker 缓存瓦片与自动化校验脚本

5.1 用 Service Worker 做瓦片缓存层

如果不想单独部署瓦片服务,或者想让已访问过的瓦片在断网后仍可用,可以用 Service Worker 做一层缓存。思路是拦截瓦片请求,先查 Cache Storage,命中就返回缓存,没命中就请求网络并写入缓存。

// sw.js const TILE_CACHE = 'tianditu-tiles-v1'; self.addEventListener('fetch', event => { const url = new URL(event.request.url); // 只拦截瓦片请求,其他请求走默认逻辑 if (!url.pathname.includes('/tiles/')) return; event.respondWith( caches.open(TILE_CACHE).then(cache => cache.match(event.request).then(cached => { if (cached) return cached; // 命中缓存直接返回 return fetch(event.request).then(response => { // 只缓存成功响应,避免把 404 也存进去 if (response.ok) { cache.put(event.request, response.clone()); } return response; }); }) ) ); });

参数说明:TILE_CACHE是缓存空间的名字,改版本号可以强制刷新缓存。event.respondWith用来接管请求响应。response.clone()是因为 Response 流只能读一次,缓存和返回需要两个副本。注意 Service Worker 只在 HTTPS 或 localhost 下生效,内网 HTTP 环境需要额外配置。

注册代码放在主页面:

if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js').then(reg => { console.log('SW registered:', reg.scope); }).catch(err => { console.error('SW registration failed:', err); }); }

这个方案的边界是:首次访问仍然需要网络,只有访问过的瓦片才会被缓存。适合「外网预热、内网使用」的场景,纯离线环境还是得靠本地瓦片服务。

5.2 写一个瓦片完整性校验脚本

前面提到瓦片可能下载不全,手动检查不现实。我一般用 Node.js 写个脚本,遍历目录统计文件数,和理论值对比。

// check-tiles.js const fs = require('fs'); const path = require('path'); const TILE_ROOT = './static/tianditu/tiles'; const MIN_ZOOM = 1; const MAX_ZOOM = 10; // 计算第 z 级瓦片总数:2^z * 2^z function expectedCount(z) { const n = Math.pow(2, z); return n * n; } let totalExpected = 0; let totalActual = 0; for (let z = MIN_ZOOM; z <= MAX_ZOOM; z++) { const dir = path.join(TILE_ROOT, String(z)); if (!fs.existsSync(dir)) { console.log(`层级 ${z}: 目录不存在`); continue; } // 递归统计该层级下的 png 文件数 let count = 0; function walk(d) { fs.readdirSync(d).forEach(f => { const full = path.join(d, f); if (fs.statSync(full).isDirectory()) walk(full); else if (f.endsWith('.png')) count++; }); } walk(dir); const exp = expectedCount(z); totalExpected += exp; totalActual += count; const rate = ((count / exp) * 100).toFixed(1); console.log(`层级 ${z}: 实际 ${count} / 理论 ${exp} (${rate}%)`); } console.log(`\n合计: 实际 ${totalActual} / 理论 ${totalExpected}`);

参数说明:MIN_ZOOM和MAX_ZOOM按实际下载的层级改。expectedCount用的是 Web 墨卡托投影的瓦片数量公式,第 z 级是2^z * 2^z。实际项目中,如果只下载了某个城市范围的瓦片,实际数量会远小于理论值,这时要把expectedCount改成按经纬度范围计算,而不是全球范围。

跑完脚本,如果某个层级的完成率明显偏低,就针对那个层级补下。补下时用wget的-nc参数跳过已存在文件:

# 从瓦片服务批量下载,-nc 跳过已存在文件,-r 递归,-np 不追溯父目录 wget -nc -r -np -nH --cut-dirs=2 -R "index.html*" \ "http://内网瓦片服务/tiles/{z}/{x}/{y}.png" \ -P ./static/tianditu/tiles/

--cut-dirs=2用来去掉 URL 里的前两级目录,让下载下来的结构和本地目录对齐。具体数字按实际 URL 层级调整。

5.3 一个我踩过的坑:瓦片坐标系对不上

最后说个血泪经验。有次离线地图部署完,地图能显示,但所有标注点都偏移了几百米。排查了半天,发现是瓦片坐标系问题:下载的瓦片是 Web 墨卡托(EPSG:3857),但初始化地图时用的坐标系参数是 WGS84(EPSG:4326),两者在投影转换上有偏差。

解决方式是在T.Map初始化时显式指定投影,或者确保瓦片和地图对象的坐标系一致。常见做法是:如果瓦片是按 Web 墨卡托切的,地图初始化也用 Web 墨卡托;如果业务数据是 WGS84 经纬度,在添加标注时做一次坐标转换,而不是改地图投影。

这个坑的教训是:离线地图不只是文件替换,坐标系、投影、层级规则这些「看不见的约定」对不上,地图照样翻车。每次部署完,拿几个已知坐标的点验证一下位置,比看日志管用。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 7:55:29

CVAT任务创建失败系统错误排查:数据库、连接池与存储后端修复指南

前阵子接到项目组的求助&#xff1a;CVAT任务和项目无法创建的报错反复出现&#xff0c;标注小组一到要开新一批任务就卡壳。页面转圈几秒后弹出一句“系统错误”&#xff0c;换浏览器、清缓存、重启容器都试过&#xff0c;问题依旧。老实说&#xff0c;这种“系统错误”在CVAT…

作者头像 李华
网站建设 2026/10/20 7:54:22

电场-热场耦合仿真:从焦耳热到温升的工程实战指南

电磁场耦合仿真这个系列里&#xff0c;主题021的“电场-热场耦合仿真”&#xff0c;几乎是我被问得最多的一个方向。说白了&#xff0c;它就是算电流流过导体之后&#xff0c;导体自己发热、发热到一定程度又反过来改变电阻率、电阻率再改变电流分布&#xff0c;形成一个真实的…

作者头像 李华
网站建设 2026/10/20 7:54:17

AI生成Dashboard代码实战:契约驱动与插件化架构

1. 为什么我决定把Dashboard的代码交给AI来写1.1 一个让我彻底改变想法的下午去年有段时间我同时在维护三个内部工具的后台面板&#xff0c;一个是给运营看的数据概览&#xff0c;一个是给客服用的工单统计&#xff0c;还有一个是给运维盯的接口健康度监控。三个面板长得不一样…

作者头像 李华
网站建设 2026/10/20 7:54:14

可插拔轻量级Dashboard:AI写代码、人定逻辑的配置驱动实践

1. 为什么我决定把“写代码”这件事交出去1.1 一个让我彻底改变工作流的契机去年下半年&#xff0c;我手上同时压着三个内部工具的需求。一个是运营团队要的数据看板&#xff0c;一个是客服组要的工单统计面板&#xff0c;还有一个是给管理层看的周报汇总页。三个东西本质上都是…

作者头像 李华
网站建设 2026/10/20 7:53:37

嵌入式Linux FrameBuffer原理与实践:从驱动到应用完整拆解

1. FrameBuffer到底是什么&#xff0c;为什么嵌入式开发绕不开它做嵌入式UI或者裸机图形开发的朋友&#xff0c;对FrameBuffer这个词应该都不陌生。简单说&#xff0c;它是Linux内核提供给用户空间的一块显存映射区域&#xff0c;应用程序可以直接往这块内存里写像素数据&#…

作者头像 李华
网站建设 2026/10/20 7:53:32

多奖励RL训练中优势崩溃怎么办?GDPO偏好优化实战解析

周五上午的组会刚结束&#xff0c;我还在回味这次讨论的主角——GDPO。如果你最近也在做多奖励RL训练&#xff0c;又在为GRPO的"优势崩溃"头疼&#xff0c;那这篇复盘应该能帮上忙。事情说起来很简单&#xff1a;GRPO本身很强&#xff0c;省掉价值网络、显存友好、收…

作者头像 李华