简介这份压缩包面向希望入门并实践百度地图API的Web开发者尤其适合具备基础JavaScript、正在学习网页地图集成的读者。内容以JavaScript API示例为主线并配有可直接导入的Eclipse工程覆盖地图初始化、中心点设置、标注添加、多边形绘制、拖拽交互、定位获取与路径规划等常见场景同时提示联网运行和申请API密钥等前置条件。包内共53个文件以jsp页面为主辅以js脚本、xml配置、图片及工程配置类文件压缩后约86KB体积轻便便于按模块查阅和调试。已有553人学习适合用来对照代码理解地图服务的调用流程并把示例迁移到自己的项目中。1. 先跑通一张最小地图这份地图 API 学习源代码到底该怎么读拿到一套地图 API 学习源代码最怕一上来就从头读文件。目录里往往同时躺着前端初始化、覆盖物、事件、地理编码、路径规划、坐标转换和服务端请求示例直接顺着读很容易在第一个Map is not defined上翻车。这套资源围绕主流地图开放平台的 Web 端 JS API 与服务端 Web 服务 API 展开前端负责容器、图层、标记和交互服务端负责地理编码、路径规划、配额与签名。它适合前端、全栈和刚接触 GIS 业务的开发者照着跑通不适合直接当生产脚手架。原始标题里的平台名这里统一用“主流地图开放平台”代称代码里用MapSDK占位替换成实际 SDK 命名空间即可。先跑最小地图再拆模块顺序别反。2. 拆目录与跑最小地图四个能力层怎么映射到源代码文件2.1 先看入口和依赖目录里哪些文件值得先读学习型源代码包最容易出现“文件很多、入口很散”的问题。我的习惯是先不看业务页面先用目录命令把两层结构扫出来找到index.html、js/、server/、config/这几类入口再判断每个文件属于前端渲染、前端交互、服务端接口还是公共工具。地图 API 的学习代码通常会把初始化、覆盖物、事件、服务调用混在同一个 demo 里先按能力层分类比逐个文件读注释有效得多。下面这条命令只做目录观察不改文件适合拿到包后的第一分钟执行。# 只展开两层先看入口文件和目录职责 find . -maxdepth 2 -type f | sort # 如果目录里 node_modules 很大先排除依赖目录 find . -maxdepth 2 -type f \ -not -path */node_modules/* \ -not -path */.git/* | sort逻辑说明find先限制深度避免一屏幕刷出几百个依赖文件-not -path把依赖目录和版本库目录排掉留下的才是学习代码。参数上-maxdepth 2对大多数 demo 够用如果服务端按模块分目录可以临时改成3但不要一上来就全量find否则入口判断会被噪声淹没。文件/目录常见职责先读原因index.html地图容器、脚本引入、页面骨架先确认 SDK 怎么加载、密钥放哪js/init*.js地图实例、中心点、缩放级别最小地图能不能出来全看这里js/overlay*.js标记、折线、信息窗口覆盖物绑定顺序容易出错server/地理编码、路径规划、签名服务端密钥和配额在这里config/域名、密钥占位、环境变量先确认有没有硬编码密钥提示如果目录里出现多个index.html优先找带“最小”“basic”“quickstart”字样的那个不要从业务最复杂的页面开始。2.2 最小地图骨架容器、密钥、中心点和缩放级别最小地图只需要四件事一个固定高度的容器、一次 SDK 脚本加载、一个地图实例、一组中心点和缩放级别。很多学习代码把脚本写成同步script页面一加载就执行new MapSDK.Map(...)结果 SDK 还没就绪控制台直接报命名空间不存在。更稳的写法是把初始化放进 SDK 的 ready 回调或者用动态脚本加载后显式调用初始化函数。下面是一个不依赖具体平台的骨架MapSDK是占位命名空间实际接入时替换成对应 SDK 的全局对象。!doctype html html langzh-CN head meta charsetutf-8 / meta nameviewport contentwidthdevice-width, initial-scale1 / title最小地图示例/title style /* 容器必须有明确高度否则地图初始化后高度为 0页面看起来是白屏 */ #mapContainer { width: 100%; height: 480px; } /style /head body div idmapContainer/div script // 实际项目中把 key 放到构建变量或后端下发的配置里不要硬编码进仓库 const MAP_CONFIG { key: YOUR_MAP_KEY, center: [118.123, 32.456], // 示例坐标换成业务点位 zoom: 12 }; function initMap() { const map new MapSDK.Map(mapContainer, { center: MAP_CONFIG.center, zoom: MAP_CONFIG.zoom, // 关闭不必要的默认控件先确认地图能出来 enableScrollWheelZoom: true }); // 挂到 window 只用于学习 demo生产代码用模块闭包管理 window.__map map; } // 动态加载 SDK加载完成后初始化避免脚本顺序问题 function loadMapSdk() { const script document.createElement(script); script.src https://your-map-api-host/jsapi?key encodeURIComponent(MAP_CONFIG.key); script.async true; script.onload initMap; script.onerror function () { console.error(地图 SDK 加载失败先检查网络、域名和密钥); }; document.head.appendChild(script); } loadMapSdk(); /script /body /html逻辑说明#mapContainer的高度是硬条件地图库通常拿容器宽高做画布尺寸高度为 0 时不会报错只会给你一块空白动态加载脚本把“加载完成”和“初始化”拆开避免脚本顺序玄学window.__map只用于调试生产环境应该由模块作用域持有实例。参数说明center用[经度, 纬度]顺序很多平台接受对象写法但数组写法在 demo 里更常见zoom一般 3 到 18城市级看 10 到 12园区级看 15 到 17enableScrollWheelZoom在移动端容易和页面滚动冲突学习阶段先打开上线前按交互再关。密钥参数名不同平台可能是key、token或appkey不要照抄参数名按实际 SDK 文档替换。2.3 覆盖物与事件Marker、信息窗口和点击回调的绑定顺序覆盖物这块最容易“看起来加了实际点不到”。常见原因是标记加在地图实例上但信息窗口绑定在另一个未初始化的实例上或者先绑事件再创建标记回调里拿不到对象。我的做法是先把地图实例、标记实例、信息窗口实例三者的创建顺序写死地图先建标记后建事件最后绑回调里只做数据读取和窗口打开。下面这段代码演示一个标记、一个点击事件和一个信息窗口的最小闭环命名空间仍然是占位。// 假设 initMap 已经创建了 map 实例 function addMarkerWithPopup(map, point, title, content) { // 1. 先创建标记point 为 [lng, lat] const marker new MapSDK.Marker(point, { title: title, // 标记图标按实际 SDK 配置学习 demo 先用默认图标 draggable: false }); // 2. 信息窗口独立创建不要每次点击都 new 一个 const infoWindow new MapSDK.InfoWindow({ content: content, width: 260, height: 120 }); // 3. 事件绑定放在标记加入地图之前或之后都可以但回调里只操作已创建的实例 marker.addEventListener(click, function () { infoWindow.open(map, point); }); // 4. 最后加入地图避免回调触发时地图上还没有这个覆盖物 map.addOverlay(marker); return { marker, infoWindow }; } // 批量加点时不要一个个 addOverlay先攒数组再批量处理 function addBatchMarkers(map, points) { const overlays points.map(function (item) { return new MapSDK.Marker([item.lng, item.lat], { title: item.name }); }); // 部分 SDK 支持 addOverlays 批量接口没有就循环但循环里不要再做 DOM 查询 if (typeof map.addOverlays function) { map.addOverlays(overlays); } else { overlays.forEach(function (marker) { map.addOverlay(marker); }); } }逻辑说明信息窗口只创建一次点击时复用避免每次点击都往 DOM 里塞新节点批量加点先攒数组减少地图重绘次数事件回调里不要做重计算先把数据取出来再把渲染交给独立函数。参数说明width和height是信息窗口的像素尺寸移动端要留小一点draggable在只读展示场景关掉避免用户误拖批量接口不是所有 SDK 都有判断typeof再调用比直接写死平台方法稳。标记数量超过几百个时不要继续堆Marker要换聚合或按视野加载这一点在第 5 章展开。2.4 服务端 Web 服务地理编码与路径规划不要混在前端密钥里学习代码里经常把地理编码、逆地理编码、路径规划也写在前端fetch里页面一打开就直接请求服务端接口。这样做在 demo 阶段能跑但密钥会暴露在浏览器里配额也容易被刷。更合理的分层是前端只负责地图渲染和交互地址解析、路径规划、坐标转换走自己的后端代理后端持有服务端密钥并做签名、缓存和限流。下面是一个后端代理的请求示例接口地址用环境变量占位不写死具体平台域名。import os import time import hashlib import requests from urllib.parse import urlencode MAP_API_BASE os.environ[MAP_API_BASE] # 例如 https://your-map-api-host MAP_API_KEY os.environ[MAP_API_KEY] MAP_API_SECRET os.environ[MAP_API_SECRET] # 仅服务端保存 def build_signed_params(params: dict) - dict: 按参数名排序后拼接待签名串再和 secret 做摘要。 不同平台签名规则不同这里只演示通用思路 参数排序 - URL 编码 - 拼接 secret - md5 - 放入 sn。 ordered sorted(params.items()) query urlencode(ordered) raw query MAP_API_SECRET sn hashlib.md5(raw.encode(utf-8)).hexdigest() signed dict(params) signed[sn] sn return signed def geocode(address: str, city: str ) - dict: params { key: MAP_API_KEY, address: address, city: city, output: json, timestamp: str(int(time.time())) } signed build_signed_params(params) resp requests.get( f{MAP_API_BASE}/geocode, paramssigned, timeout(3.05, 8) ) resp.raise_for_status() data resp.json() # 不要直接把上游原始响应透传给前端先裁掉内部字段 return { status: data.get(status), location: data.get(result, {}).get(location), confidence: data.get(result, {}).get(confidence) }逻辑说明参数排序和签名只在服务端做前端拿不到MAP_API_SECRETtimeout拆成连接超时和读取超时避免上游卡住把整个请求线程拖死返回前裁字段避免把上游错误细节和内部配额信息透给浏览器。参数说明MAP_API_BASE、MAP_API_KEY、MAP_API_SECRET全部走环境变量不要写进仓库timestamp用于防重放服务端和上游时钟偏差太大会导致签名失败city是可选限定地址重名时能提高命中率timeout的连接超时不要设太长3 秒左右足够判断网络是否通。地理编码和路径规划的配额通常分开计算学习阶段先看控制台配额页再决定要不要做本地缓存。3. 密钥、坐标系与配额接入地图 API 前必须算清的三笔账3.1 前端密钥与白名单别把服务端密钥塞进浏览器前端密钥和服务端密钥不是一回事。前端密钥会出现在页面源码、网络请求和构建产物里必须配域名白名单、Referer 限制或应用绑定服务端密钥只放在后端环境变量里用来签名和调用高配额接口。学习源代码里如果只有一个key变量先判断它到底用在哪只用于加载 JS SDK那就是前端密钥如果还拿去调地理编码、路径规划那就是把服务端能力混进了前端。常见翻车场景是本地用file://打开 HTML白名单里没有file://或localhost地图直接白屏控制台只给一个密钥无效。密钥类型使用位置主要风险处理方式前端加载密钥浏览器 script 标签被抄走、被刷配额配域名白名单限制调用来源服务端接口密钥后端环境变量泄露后配额被耗尽不入仓库定期轮换签名密钥后端签名函数泄露后可伪造请求只放服务端配合时间戳注意本地调试不要直接双击 HTML起一个本地 HTTP 服务把localhost加进白名单能省掉一半“密钥无效”的误判。3.2 坐标系转换GPS 原始坐标、国内加密坐标与平台私有坐标坐标偏移是地图 API 学习代码里最典型的血泪经验。GPS 设备输出的是 WGS84国内地图展示常用加密坐标部分平台还有自己的私有坐标。三者混用时标记会整体偏移几百米看起来像“地图不准”实际是坐标系没对齐。我的做法是数据入库时标注crs字段接口返回时带上坐标系前端只做展示转换统一放在服务端或公共工具层。下面这段代码只演示 WGS84 到国内加密坐标的通用转换思路平台私有坐标用接口占位不硬编码算法。// WGS84 - GCJ02 的通用转换函数参数为 [lng, lat] function wgs84ToGcj02(lng, lat) { const PI 3.1415926535897932384626; const a 6378245.0; const ee 0.00669342162296594323; function outOfChina(lng, lat) { return lng 72.004 || lng 137.8347 || lat 0.8293 || lat 55.8271; } function transformLat(x, y) { let ret -100.0 2.0 * x 3.0 * y 0.2 * y * y 0.1 * x * y 0.2 * Math.sqrt(Math.abs(x)); ret (20.0 * Math.sin(6.0 * x * PI) 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0; ret (20.0 * Math.sin(y * PI) 40.0 * Math.sin(y / 3.0 * PI)) * 2.0 / 3.0; ret (160.0 * Math.sin(y / 12.0 * PI) 320 * Math.sin(y * PI / 30.0)) * 2.0 / 3.0; return ret; } function transformLng(x, y) { let ret 300.0 x 2.0 * y 0.1 * x * x 0.1 * x * y 0.1 * Math.sqrt(Math.abs(x)); ret (20.0 * Math.sin(6.0 * x * PI) 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0; ret (20.0 * Math.sin(x * PI) 40.0 * Math.sin(x / 3.0 * PI)) * 2.0 / 3.0; ret (150.0 * Math.sin(x / 12.0 * PI) 300.0 * Math.sin(x / 30.0 * PI)) * 2.0 / 3.0; return ret; } if (outOfChina(lng, lat)) { return [lng, lat]; } let dLat transformLat(lng - 105.0, lat - 35.0); let dLng transformLng(lng - 105.0, lat - 35.0); const radLat lat / 180.0 * PI; let magic Math.sin(radLat); magic 1 - ee * magic * magic; const sqrtMagic Math.sqrt(magic); dLat (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * PI); dLng (dLng * 180.0) / (a / sqrtMagic * Math.cos(radLat) * PI); return [lng dLng, lat dLat]; } // 平台私有坐标不要在前端硬转走服务端转换接口返回结果带 crs 标识 async function toPlatformCoordinate(lng, lat, fromCrs) { const resp await fetch(/api/coordinate/convert, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ lng, lat, from: fromCrs }) }); if (!resp.ok) { throw new Error(坐标转换失败检查接口和目标坐标系); } return resp.json(); // { lng, lat, crs: PLATFORM } }逻辑说明outOfChina先判断是否在转换范围内范围外直接返回原坐标transformLat和transformLng是通用偏移量计算最后按椭球参数反算平台私有坐标不写死算法走服务端接口前端只认crs字段。这样即使平台升级坐标规则前端不用跟着改。参数说明输入顺序统一为[经度, 纬度]不要和[纬度, 经度]混用fromCrs建议用枚举字符串比如WGS84、GCJ02、PLATFORM不要用数字转换接口返回的crs要跟着数据一起入库后续排错时能直接判断是哪一步混了坐标系。3.3 配额与重试QPS、日限额和退避策略怎么设地图 API 的配额通常分 QPS 和日调用量学习代码里最容易出现“循环里直接请求”的写法几十条地址一跑就触发限流。我的做法是批量任务先做本地缓存同一地址一天内不重复请求请求层加指数退避遇到限流错误先等再试不要原地重试路径规划这类重接口单独限并发不要和地理编码抢同一个连接池。下面是一个带退避的重试示例Retry-After优先其次按指数退避。import time import random import requests def request_with_backoff(url, params, max_retries4): last_error None for attempt in range(max_retries): try: resp requests.get(url, paramsparams, timeout(3.05, 8)) # 429 或平台定义的限流码优先读 Retry-After if resp.status_code 429: retry_after resp.headers.get(Retry-After) wait int(retry_after) if retry_after and retry_after.isdigit() else 2 ** attempt time.sleep(wait random.random()) continue resp.raise_for_status() return resp.json() except requests.RequestException as exc: last_error exc # 网络类错误才重试参数错误不要重试 if attempt max_retries - 1: raise time.sleep(2 ** attempt random.random()) raise RuntimeError(f请求失败: {last_error})逻辑说明限流错误和网络错误分开处理限流优先按服务端给的等待时间参数错误、签名错误不应该重试重试只会浪费配额退避里加一点随机抖动避免多个任务同一秒一起重试。参数说明max_retries不建议超过 4超过后整体延迟不可控timeout连接 3 秒、读取 8 秒适合大多数地图服务接口2 ** attempt产生 1、2、4、8 秒的退避叠加随机数后能打散并发。日限额要在业务层做计数不要等上游返回限流才处理。4. 避坑与排查地图 API 源码跑不起来时先查这五处4.1 前端白屏、密钥报错与标记偏移现象一页面白屏控制台报MapSDK is not defined。原因通常是脚本还没加载完就执行了初始化或者 SDK 脚本地址写错但页面没有明显报错。解决方式是把初始化放进onload回调或者用动态加载后显式调用同时检查容器高度是否为 0高度为 0 时地图不会报错只会给你空白。调试时先打印typeof MapSDK再打印容器clientHeight两个都正常再看密钥。现象二控制台提示密钥无效或域名不匹配。原因多半是本地用file://打开、白名单没加localhost、域名带了端口但白名单只写了主域名。解决方式是起本地 HTTP 服务把localhost:端口加进白名单线上检查 Referer 是否被反向代理改写。密钥参数名不要照抄前端加载密钥和服务端签名密钥分开配置。现象三标记整体偏移几百米。原因不是地图不准而是数据源坐标系和地图坐标系不一致。解决方式是入库时加crs字段接口返回时带坐标系前端展示前统一转换不要在前端到处写转换函数集中到公共工具或服务端。排查时拿一个已知点做对照先转坐标再打点看偏移是否消失。4.2 服务端签名、跨域与聚合卡顿现象四服务端接口返回 403 或签名错误。原因通常是参数没排序、URL 编码不一致、时间戳偏差太大或者签名密钥用错。解决方式是按平台签名规则逐项比对参数名排序、空值是否参与、URL 编码是否用同一种、时间戳是否在有效窗口内。签名串先在本地打印出来和官方示例逐字符比对不要只看结果码。现象五浏览器直调服务端接口被 CORS 拦截。原因是把地理编码、路径规划写在前端fetch里而服务端接口没有给浏览器域名开跨域。解决方式是加一层后端代理前端只调自己的/api/...由后端带密钥请求上游代理层做缓存和限流顺手把上游错误码翻译成业务错误码。不要在前端塞服务端密钥来绕跨域这是把问题从网络层搬到安全层。现象六几百个标记加完后地图拖动卡顿。原因是每个标记都独立 DOM 节点事件监听没有解绑或者每次搜索都重新new一个地图实例。解决方式是超过一定数量改用聚合或按视野加载页面切走时调用destroy或dispose把事件监听一并移除批量加点用数组攒起来再统一加入不要在循环里反复触发重绘。排查时看 Performance 面板确认卡顿发生在标记创建还是地图重绘。提示排错顺序固定为“容器高度 → SDK 加载 → 密钥 → 坐标系 → 配额”从外到内别一上来就怀疑算法。5. 把散落 Demo 收成地图服务层配置、请求、渲染三段拆开5.1 封装边界哪些逻辑留在页面哪些下沉到服务层学习代码里的地图调用往往散落在页面事件里点击按钮就new Marker搜索框回车就fetch地理编码页面卸载也不销毁实例。要收成可维护的模块先划边界页面只负责收集用户输入和展示状态地图服务层负责初始化、加覆盖物、销毁请求层负责签名、重试、缓存。这样页面换框架时地图逻辑不用重写。我的习惯是给地图服务层暴露四个方法init、addPoints、clear、destroy其他细节都收在内部。5.2 一个可复用的地图服务类初始化、加点和销毁下面这个类用MapSDK占位重点看结构而不是具体平台方法。初始化只执行一次重复调用直接返回已有实例加点时先清掉旧覆盖物避免多次搜索叠加销毁时移除事件并置空实例防止单页应用切换路由后内存泄漏。class MapService { constructor(containerId, config) { this.containerId containerId; this.config config; // { key, center, zoom } this.map null; this.markers []; this.boundHandlers []; } init() { if (this.map) return this.map; // 单例避免重复初始化 this.map new MapSDK.Map(this.containerId, { center: this.config.center, zoom: this.config.zoom }); // 统一保存事件句柄销毁时能精确移除 const onZoomEnd () { // 按视野加载时在这里触发不要每次拖动都请求 console.debug(zoomend, this.map.getZoom()); }; this.map.addEventListener(zoomend, onZoomEnd); this.boundHandlers.push({ type: zoomend, handler: onZoomEnd }); return this.map; } addPoints(points) { if (!this.map) throw new Error(地图未初始化); this.clear(); this.markers points.map((item) { const marker new MapSDK.Marker([item.lng, item.lat], { title: item.name }); marker.addEventListener(click, () { // 点击只抛事件不在这里直接操作页面 DOM this.emit(marker-click, item); }); return marker; }); if (typeof this.map.addOverlays function) { this.map.addOverlays(this.markers); } else { this.markers.forEach((marker) this.map.addOverlay(marker)); } } clear() { if (!this.map) return; if (typeof this.map.removeOverlays function) { this.map.removeOverlays(this.markers); } else { this.markers.forEach((marker) this.map.removeOverlay(marker)); } this.markers []; } destroy() { if (!this.map) return; this.clear(); this.boundHandlers.forEach(({ type, handler }) { this.map.removeEventListener(type, handler); }); this.boundHandlers []; if (typeof this.map.destroy function) { this.map.destroy(); } this.map null; } emit(name, payload) { // 学习 demo 用简单回调生产可换成 EventBus if (typeof this.onEvent function) { this.onEvent(name, payload); } } }逻辑说明init做单例判断防止同一容器被重复初始化addPoints先clear再加避免旧标记残留事件句柄统一存数组destroy时逐个移除点击回调只抛事件页面自己决定弹窗还是跳转地图服务层不绑业务 DOM。参数说明containerId必须是页面已存在的元素center和zoom从配置注入不要写死在类里points至少包含lng、latname可选destroy里平台方法名可能是destroy、dispose或clearMap用typeof判断再调用。5.3 性能与生命周期按需加载、防抖和 destroy地图页面的性能问题通常不在一次渲染而在“搜索十次、标记叠加十层、路由切走实例还在”。我的做法是三件事搜索输入防抖避免每敲一个字就请求地理编码标记按视野加载拖动结束后只拉当前范围内的点页面卸载强制destroy把事件和覆盖物清干净。下面是一个防抖加视野加载的骨架重点看触发时机。function debounce(fn, wait) { let timer null; return function (...args) { clearTimeout(timer); timer setTimeout(() fn.apply(this, args), wait); }; } // 地图移动结束后再请求避免拖动过程中连续发请求 const loadByViewport debounce(async function (mapService) { const bounds mapService.map.getBounds(); // 平台方法名按实际 SDK 替换 const resp await fetch(/api/points?bounds encodeURIComponent(JSON.stringify(bounds))); const data await resp.json(); mapService.addPoints(data.list); }, 300); // 页面路由离开时调用学习 demo 可以在 beforeunload 里做 window.addEventListener(beforeunload, function () { if (window.__mapService) { window.__mapService.destroy(); } });逻辑说明debounce把连续触发压成最后一次300 毫秒是地图拖动场景的常用值视野加载只传边界不传全量数据beforeunload只是兜底单页应用应该在路由守卫里调用destroy不要等浏览器关闭。参数说明wait不要设太小100 毫秒以内基本等于没防抖bounds的序列化方式要和后端约定别前端传数组、后端按对象解析destroy必须在地图实例还有效时调用置空后再调会报错。6. 进阶验证用一条地址到路径的链路压测地图 API 可用性6.1 链路脚本与观测表学完单个接口后我会用一条链路把“地址解析 → 坐标转换 → 路径规划 → 渲染校验”串起来跑十次看耗时和错误分布。这样做的好处是单个接口正常不代表链路正常坐标转换错一步路径规划结果就会整体偏掉。下面脚本只保留结构接口地址和密钥全部走环境变量不写真实域名。import os import time import requests BASE os.environ[MAP_API_BASE] KEY os.environ[MAP_API_KEY] def timed(name, fn): start time.perf_counter() try: result fn() return {name: name, ok: True, ms: round((time.perf_counter() - start) * 1000), result: result} except Exception as exc: return {name: name, ok: False, ms: round((time.perf_counter() - start) * 1000), error: str(exc)} def geocode(): r requests.get(f{BASE}/geocode, params{key: KEY, address: 某园区北门}, timeout8) r.raise_for_status() return r.json()[result][location] def convert(location): r requests.post(f{BASE}/coordinate/convert, json{lng: location[lng], lat: location[lat], from: GCJ02}, timeout8) r.raise_for_status() return r.json() def route(location): r requests.get(f{BASE}/direction, params{key: KEY, origin: f{location[lng]},{location[lat]}, destination: 118.456,32.567}, timeout8) r.raise_for_status() return r.json()[result][routes][0][distance] for i in range(10): loc timed(geocode, geocode) if loc[ok]: conv timed(convert, lambda: convert(loc[result])) if conv[ok]: print(timed(route, lambda: route(conv[result])))逻辑说明每一步单独计时失败不中断整条链路方便定位是地理编码慢还是路径规划超时坐标转换放在路径规划前确保起终点在同一坐标系跑十次看 P95不要只看单次。参数说明timeout统一 8 秒超过就认为上游异常from字段要和数据源一致起终点坐标用示例值实际跑的时候换成业务点位。观测表可以按下面三项记录。观测项正常表现异常时先查地理编码耗时200 到 800 毫秒地址是否模糊、城市限定是否缺失坐标转换耗时50 到 300 毫秒服务端签名、坐标系枚举是否传错路径规划耗时300 到 1500 毫秒起终点是否跨城、配额是否被限流从那以后我每次接地图项目都会先跑这条链路再写页面因为单接口 demo 太容易给人“一切正常”的错觉。链路跑通、坐标对齐、配额扛住再谈聚合和样式。希望帮到你。本文还有配套的精品资源点击获取