做 GIS 项目的人大概都有过这种体验:地图上要铺几千上万个点位,直接全量拉数据浏览器当场卡死;业务方转头又提新需求,说"只显示当前视野里的设施""按行政区给我统计数量""我在这儿画个圈,把圈里的门店都查出来"。这些需求看着零散,落到实现层面其实都指向同一件事——怎么把地图服务端提供的 REST 接口用到位。这篇文章就围绕超图 REST 服务,把加载、分页查询、统计、空间条件过滤这几块串起来讲。它适合已经能看懂基本 HTTP 请求、手上有一台 iServer 或者用过云端地图服务的同学,也适合刚接手 GIS 模块、被"数据量大就崩"折磨过的开发者。我不会只丢接口文档给你,而是把每个参数背后的取舍、踩过的坑、能直接抄的写法都摊开说。
1. 先想清楚:为什么这套活儿要放在服务端做
1.1 客户端硬扛的代价
我刚做 GIS 那会儿图省事,地图初始化一把梭,直接请求数据集的全部要素塞进前端渲染。三千个点还行,三万就开始掉帧,十万直接把标签页拖死。问题不在前端渲染库,而在于你把数据搬运和筛选这两个重活都压在了浏览器身上:网络传输要扛全量 JSON,内存要存全部几何,渲染层还要为每个要素算样式。只要数据量稍微上来,这套流程就没有一处是划算的。
把筛选、统计、分页这些逻辑下沉到服务端,本质上是"谁的数据谁算账"。服务端离数据库近,能做索引过滤,能只返回当前页需要的那几十条记录。浏览器拿到的就是精简结果,渲染压力骤降。所以看到"数据量大"这四个字,第一反应不应该是优化前端渲染,而是先问一句:这批数据非要在客户端全量存在吗?
1.2 REST 接口的职责边界与选型
超图的 REST 服务体系里,跟业务查询关系最紧的是地图服务和数据服务两类。地图服务偏"看",负责出图、切瓦片、返回配图好的图面;数据服务偏"算",负责对数据集做属性查询、空间查询、统计、编辑。很多新手会拿地图服务的查询能力硬做业务检索,结果发现它返回的是渲染后的要素,字段被裁剪、几何被简化,做统计时缺斤少两。
我的经验是:只要是"按条件挑数据、算数量、做聚合"这类需求,一律走数据服务的要素查询接口;只有当需求是"把这批数据按某个风格出成图"时,才回到地图服务。这条边界划清楚了,后面选接口就不会来回纠结。数据服务的查询入口通常挂在类似/rest/data/datasources/{数据源}/datasets/{数据集}/features这样的路径上,也可以先创建查询结果再逐步取数,两种方式后面会具体对比。
1.3 服务地址与鉴权的最小准备
动手之前得把地址和权限理顺。REST 服务地址一般由服务名拼出来,地图服务和数据服务各有各的根路径。要注意的是,很多部署默认关掉了匿名访问,请求必须带 token。token 有两种常见来源:一是服务端配置的长期令牌,二是登录接口换回的短期令牌。短期令牌有有效期,过期后接口会返回 401,这时候如果前端没做无感刷新,用户看到的就是"地图突然白了"。
我一般会在请求封装层统一拦截 401,拿到新 token 后重放一次原请求,业务代码完全无感。还有个细节:token 不要拼在 URL 查询串里到处传,容易在日志和 Referer 里泄露,优先放在请求头。这一步看着琐碎,但它是后面所有分页、统计、空间过滤能稳定跑起来的地基。
2. 服务加载:把地图和数据接进来
2.1 地图服务与数据服务的区别
加载这一步,先明确要加载的是什么。地图服务加载出来是一张配好符号的图,调用方拿到的是图片或瓦片,你没法直接从里面抠出原始属性去做统计。数据服务加载出来是原始要素集合,属性字段完整、几何坐标原始,代价是它不出图,得自己渲染。
实际项目里这两者经常是搭配用的:底图、专题图走地图服务,交互查询、统计、空间过滤走数据服务,两者叠加在同一个容器里显示。有人会问,那能不能只用数据服务、自己渲染所有东西?可以,但等于把配图工作全揽到自己身上,样式一多维护成本就上来了。图层类型分工明确,团队的协作边界也清楚。
2.2 加载时的坐标系与投影陷阱
坐标系是加载环节最容易翻车的地方。地图服务和数据服务的坐标参考系如果不一致,你会看到点位整体偏移,甚至飘到几百公里外。常见情况是底图用了 Web 墨卡托(EPSG:3857),而业务数据集是地理坐标(EPSG:4326),直接叠上去必然对不上。
处理办法有两个方向:要么在服务端给数据集配好投影信息,让服务按统一坐标系返回;要么在客户端加载时声明目标投影,让渲染层帮你转换。我更倾向第一种,因为一旦把转换责任丢给前端,每个消费方都得重复处理一遍,迟早有人漏掉。排查偏移问题时有个快捷判断:如果偏移量随纬度增大而变大,基本就是投影不一致;如果只是整体平移一个固定距离,更可能是数据中心点或参数配错了。
2.3 一个可复用的加载封装
加载代码别散落在各个页面里。我习惯抽一个统一的加载函数,入参是服务地址、图层名、可见性和层级,内部处理 token、错误重试和坐标系声明。这样后面换服务地址、加鉴权头,只改一处。加载完成后再触发一次"初始视野查询",把当前范围内的数据按分页拉一页回来,用户体验上就是地图一出来就有内容,而不是空白等全量。
提示:加载函数里务必加上失败回调。地图服务偶发超时是常态,如果失败后不提示也不重试,用户会以为系统坏了,实际只是某一次请求超时。
这一步做完,地图能显示了,数据也能按需取了,接下来才轮到真正考验功力的分页。
3. 分页查询:从全量拉取到按需翻页
3.1 分页的三个参数到底怎么配
分页的核心就三个概念:每页多少条、从第几条开始、总共有多少条。超图的要素查询里,控制返回窗口的参数一般是"期望返回数量"和"起始记录位置"这一对,前者决定这一页取几条,后者决定从哪一条开始切。很多人第一次配会犯一个错:把起始位置当成页码。起始位置是记录偏移量,第 3 页、每页 20 条,起始位置应该是 40 而不是 3。这个换算错了,翻页就会出现跳记录或者重复记录。
每页条数的选择也有讲究。太小,翻页请求次数多,用户点下一页要等;太大,单次响应变重,渲染变慢。我的经验区间是 20 到 100 条,列表类场景取 20 到 50,纯地图渲染取 100 左右。如果单条要素几何特别复杂(比如行政边界这种多节点面),每页还得再降,因为几何体积比属性大得多。
3.2 创建查询结果加分页取数两步走
超图数据服务提供两种查询路径,理解它们的差别能省很多事。第一种是直接查要素,一次请求把条件和分页参数带上,返回当前页要素。第二种是先"创建查询结果",服务端把符合条件的要素 ID 集合算好并缓存,返回一个结果标识,之后带着这个标识和分页窗口反复取数。
什么时候用第二种?当你要对同一批条件做多次操作时,比如先看总数、再翻几页、最后还要对结果做统计。如果每次都重新提交条件查询,服务端要重复解析条件、重复扫库。先建结果再复用,等于把"筛选"这一步只做一次。代价是结果集在服务端有生命周期,长时间不用会被回收,取数时如果标识失效要能自动回退到重新创建。我一般会把结果标识和创建时间一起存在前端状态里,超过一定时长就主动重建。
3.3 排序、去重与总数统计的配合
分页必须配排序,这是硬规矩。不指定排序字段时,数据库返回顺序不保证稳定,翻页时同一个要素可能在两页里都出现,或者干脆漏掉。指定一个唯一的排序字段(通常是要素 ID)最稳妥;如果业务要求按某个属性排序,那就在该属性后面追加要素 ID 作为次级排序键,保证顺序确定。
总数怎么拿?可以在创建查询结果时顺便要一个总数,也可以在取数响应里看返回的总记录数。前端分页控件需要"共 N 条、第 X 页"这种展示,所以总数是必需的。这里有个坑:如果查询条件是动态的(比如跟着地图视野变),总数每动一次视野就变一次,分页控件的总页数也得跟着刷新,否则用户翻到后面会发现页码对不上。我的做法是视野变化后先请求总数,再请求第一页,两者用同一个条件快照,避免条件在两次请求之间被改动导致不一致。
4. 统计:分组、聚合与张冠李戴的坑
4.1 总数统计与分组统计
统计分两个层次。第一层是计数,就是"符合条件的要素有多少个",这个最常用,也最简单,很多接口直接支持在查询时返回总数。第二层是分组聚合,比如"按行政区统计各类设施数量""算出每个网格内的平均客流"。分组统计要求服务端支持按字段分组并返回每组的值,不是所有部署都默认开启,用之前最好拿一个小数据集验证一遍。
分组统计最容易出问题的是字段类型。分组字段如果是文本,直接按值分组没问题;如果是数值但你按文本分组,会出现"1"和"1.0"被算成两组这种尴尬情况。还有日期字段,按天分组和按时间戳分组结果完全不同,提交条件前得确认服务端把日期解释成了哪个粒度。
4.2 聚合字段与结果解析
聚合除了计数,常见的还有求和、平均、最大最小。用聚合字段时要注意空值处理:某个要素该字段为空,参与平均会不会把分母算进去,不同实现策略不一样。我在客流统计类项目里吃过亏,某些点位当天没上报数据、字段是空,直接平均出来结果偏低,后来改成先过滤空值再聚合才对。
解析统计结果时,建议把原始响应先打印出来看一眼结构,再写映射代码。超图的统计响应通常是一个包含分组键和统计值的数组,但不同接口版本的字段命名可能有差异。与其照着网上抄的字段名硬写,不如自己先发一次请求看清楚,这一步花两分钟,能省后面半小时的调试。
4.3 大数据量下的统计性能
统计是重操作,全表扫一遍再聚合,数据量大时响应明显变慢。优化方向有几个:一是尽量带上空间范围条件,先把统计范围缩小到当前视野或某个行政区,而不是全库算;二是分组字段上如果数据库有索引,速度会好很多,这个需要服务端配合建索引;三是统计结果能缓存就缓存,比如"各行政区设施总数"这种一天不变的数据,没必要每次打开页面都重算。
还有一个务实的做法:统计和明细分开加载。页面先出明细列表,统计数字用异步请求慢慢算,算完再填进去。用户感知上页面是"秒开"的,统计慢一点不影响主体操作。反过来如果让统计阻塞首屏,用户会盯着转圈等很久。
5. 空间条件过滤:几何构造与关系判定
5.1 空间查询模式怎么选
空间过滤的核心是告诉服务端"用这个几何、按这种关系去筛"。常见的关系有相交、包含、被包含、相接、相离等。选哪种关系取决于业务语义,差别很大。比如"查我画的圈里的门店",如果圈的边界正好压住一个门店的点,用"包含"可能把它排除,用"相交"就会包含进来。很多"明明在圈里却查不到"的 bug,根源就是关系选得太严。
我的一般原则:点数据配范围查询,用相交最宽容、最不容易漏;面数据做覆盖分析,按业务看是要"完全落在范围内"还是"碰到就算",前者用被包含,后者用相交。别嫌麻烦,拿一两个边界上的要素手动验证一下,比事后被业务方追着改强。
5.2 前端传几何的正确姿势
前端画完图形后,要把几何传给服务端。这里有两个要点:一是坐标顺序,经度在前还是纬度在前,不同接口约定不同,传反了图形会跑到地球另一边;二是坐标精度,太长的浮点数没必要,保留六位小数对大多数业务足够,还能减小请求体积。
几何类型也要和服务端约定一致。前端画的多边形,传到后端可能被当成"环"或者"面",字段名对不上就解析失败。稳妥做法是找一个最小示例,画一个简单矩形,把请求和响应都打出来,确认格式无误后再上复杂图形。另外,传递的几何坐标系必须和目标数据集一致,不然筛出来的结果要么为空、要么离谱,这个坑和加载时的投影问题是一脉相承的。
5.3 缓冲区与复合条件
缓冲区查询是空间过滤里很实用的一个能力:给一个点或线,按指定距离扩成一个范围再筛。比如"查我当前位置周围一公里内的门店",就不用前端自己算圆了,直接把点加距离传给服务端。距离单位要特别注意,如果数据集是地理坐标,距离单位往往按度算,你得把米换算成度;如果是投影坐标且单位是米,那就直接传米。单位搞错,缓冲区要么小得看不见,要么大到覆盖半个城市。
更复杂的场景是空间条件和属性条件叠加,比如"视野范围内、状态为营业中的门店"。这时候用复合查询,把属性过滤和空间过滤一起提交。要注意两个条件的组合逻辑是"且"还是"或"——绝大多数业务要的是"且",如果服务端默认按"或"处理,结果会莫名多出一堆范围外的数据。提交前想清楚逻辑关系,必要时拆成两次查询在客户端合并,虽然多一次请求,但结果可控。
6. 常见问题速查与避坑心得
6.1 问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 点位整体偏移 | 坐标系不一致 | 核对数据集与地图服务投影,统一到同一参考系 |
| 翻页出现重复或漏项 | 未指定稳定排序 | 加唯一字段排序,属性排序后追加 ID 作次级键 |
| 空间查询结果为空 | 关系模式过严或坐标顺序反了 | 换相交模式,检查经纬度顺序 |
| 统计数字对不上 | 空值参与聚合或分组粒度不一致 | 过滤空值,统一日期和数值分组粒度 |
| 接口偶发 401 | 令牌过期 | 封装层拦截并刷新后重放请求 |
| 缓冲区范围异常 | 距离单位与坐标系不匹配 | 地理坐标按度换算,投影坐标按米传 |
| 首屏加载慢 | 统计阻塞了明细 | 统计改为异步,先出列表后填数字 |
这张表是我这些年反复遇到、反复记录的,基本覆盖了八成以上的现场问题。遇到新故障时先往这几个方向套,比盲目加日志快得多。
6.2 几条压箱底的经验
第一条,永远先用小数据集跑通全流程。拿一个只有几百条记录、几何简单的数据集,把加载、分页、统计、空间过滤全试一遍,确认接口参数和响应结构无误,再换真实数据。很多"接口有问题"的结论,其实是在大数据量下暴露的参数错误。
第二条,把查询条件做成可序列化的对象。前端每次查询都生成一个条件快照,分页、统计、空间过滤共用同一份。这样视野一变,所有相关请求都用新快照重新发起,避免"明细是新的、统计是旧的"这种数据打架。
第三条,分页和服务端结果集的生命周期要对齐。结果集被回收后请求会报错,前端要能识别这种错误并自动重建结果集,而不是弹一个红叉让用户重试。这个自动恢复逻辑写一次,后面能省无数客诉。
第四条,统计接口能不加就不加在首屏。它是最容易拖慢体验的一环,放到视野稳定后延迟触发,配合防抖,用户拖动地图过程中完全不触发统计,停下来几百毫秒后再算,体验和性能兼顾。
第五条,空间过滤的范围尽量收窄。能带视野就不要全库扫,能带行政区就不要全国查。空间查询本身比较重,把范围先卡小,响应速度和稳定性都会好一大截。这些经验不是什么高深技巧,但每一条背后都是我实打实踩过的坑,写出来就是希望后来的人少走一遍。