拓冰建站拓冰建站
首页 / 资讯中心 / 正文

WebGIS开发实战:跨域代理、图层控制与坐标转换全解析

简介这套资源围绕ArcGIS API for JavaScript的WebGIS开发实践展开定位清晰面向Web GIS初学者、前端工程师及需要快速上手地图开发的读者。内容覆盖OGC的Web服务规范、REST风格服务、ArcGIS Server站点架构以及页面布局、图层操作、空间参考系统、几何对象、符号与图形等核心主题并配有从Hello World到自定义小部件、图层控制器的渐进式示例代码。资源包共1241个文件以html页面、js脚本和css样式表为主辅以大量png示意图、运行截图及少量工程配置文件便于对照学习整体仅18.76MB下载和本地部署都很便捷。目录按章节编排结合可运行代码与图文示例可帮助读者理解WebGIS开发流程及ArcGIS API的调用方式。目前已有2026人学习下载适合希望系统掌握WebGIS前端开发技能、并通过实际代码练习内化知识的人群。1. 为什么这套 WebGIS 代码值得从 Hello World 拆开看拿到一套 WebGIS 从基础到开发的实践代码第一眼没看到花哨的三维地球而是 static 的 MapServer 服务、proxy.ashx 转发脚本和一堆基于 ArcGIS API for JavaScript 的页面文件。恰恰是这种朴素的结构把 WebGIS 里最容易踩坑的三件事都摆了出来跨域请求怎么转发、图层和地图容器怎么解耦、空间参考转换在哪里做。如果你正在写地图页面却总被 CORS 和坐标系卡住或者想在现有 ASP.NET 项目里接入地图功能这套代码比纯前端 demo 更有参考价值。它不追求 3D 特效而是在基础服务、Dojo 小部件、几何绘制这条链路上给出完整实现适合 GIS 初级工程师和需要向后端讲清数据流的全栈开发。2. 打通服务链路ArcGIS Server REST 服务与 proxy.ashx 转发脚本2.1 REST 服务 URL 的组成与元数据ArcGIS Server 发布的地图服务本质上是一个 HTTP 接口每个服务都会暴露一个 REST 根地址类似http://host:6080/arcgis/rest/services/项目名/MapServer。这个地址后面可以拼?fjson返回服务的元数据包括坐标系、地图范围、图层列表和 tileInfo。WebGIS 开发的第一步不是写script引入 API而是先确认这个地址能访问、能返回 JSON。curl -s http://localhost:6080/arcgis/rest/services/Demo/MapServer?fjson | head -n 20curl 不会自动跟进浏览器跨域限制所以它能通不代表页面能直接调通真正卡住你的通常是浏览器端的 CORS 预检。ArcGIS Server 自带的跨域支持并不总是开着就算开了生产环境也不建议把地图服务直接裸露给前端。这套代码里出现的 proxy.ashx 就是为了解决这个问题让前端只访问同源的一个 HTTP 处理程序再由它向后端地图服务发请求。这样既规避了浏览器同源策略又能把内网服务地址和真实鉴权信息藏在服务器一侧。看到代码里出现.ashx第一反应应该是「请求转发层」而不是业务逻辑。2.2 proxy.ashx 的典型实现这个资源里的 proxy.ashx 是 ASP.NET 的 IHttpHandler需要部署在 Web 站点根目录下。它接收?url参数把完整参数透传给 ArcGIS Server再把响应写回给前端。我拿到的代码里核心部分一般是这样public class proxy : IHttpHandler { public void ProcessRequest(HttpContext context) { // 1. 从查询参数里取出目标服务地址 string targetUrl context.Request.QueryString[url]; // 2. 白名单校验只允许访问配置好的内网 GIS 主机 if (!IsAllowed(targetUrl)) { context.Response.StatusCode 403; context.Response.End(); return; } // 3. 构建完整请求 URL透传所有除 url 之外的参数 var builder new UriBuilder(targetUrl); var query HttpUtility.ParseQueryString(builder.Query); foreach (string key in context.Request.QueryString.AllKeys) { if (key url) continue; query[key] context.Request.QueryString[key]; } if (string.IsNullOrEmpty(query[f])) { query[f] json; } builder.Query query.ToString(); WebRequest request WebRequest.Create(builder.Uri); using (WebResponse response request.GetResponse()) using (StreamReader reader new StreamReader(response.GetResponseStream(), Encoding.UTF8)) { context.Response.ContentType application/json; context.Response.Write(reader.ReadToEnd()); } } private bool IsAllowed(string url) { // 常见做法是读取 proxy.config 里的 allowUrls 列表而不是硬编码 return url.StartsWith(http://gis-server:6080/arcgis/rest/services/); } }这段代码的逻辑不复杂先拿 url再白名单校验然后通过UriBuilder把查询参数合并最后原样返回 JSON。参数说明url是目标 ArcGIS REST 服务地址f是返回格式json或htmlbbox、size等其余参数在不区分大小写的情况下被透传。这里用HttpUtility.ParseQueryString可以避免手动拼接?和比直接string.Format更稳。IsAllowed我一般会改成 XML 配置方式因为每次发布新服务都改 DLL 不现实。2.2.1 配置白名单与超时资源里经常还带一个 proxy.config 文件ArcGIS 转发脚本的配置结构大致是ProxyConfig allowedReferers* ...里面用ServerUrl url... matchAlltrue /来限制允许的目标地址。我习惯把它收缩成只允许业务网段并设置maxRequestTime为 10 秒ProxyConfig allowedReferers* logFileproxy.log logLevelFINE ServerUrl urlhttp://gis-server:6080/arcgis/rest/services matchAllfalse maxRequestTime10000 / /ProxyConfigmatchAll为true时表示 URL 前缀完全匹配false时还会继续读url后面的路径。如果allowedReferers是*意味着任何来源页面都能打到这个转发接口所以生产环境一定改成你的前端域名。2.3 前端调用转发脚本的方式地图页面里不再直接写服务地址而是先调用同源转发地址。下面这段代码是典型的 JavaScript 请求方式放在 ArcGIS API 加载完之后执行function fetchServiceMetadata(serviceUrl) { var proxyPrefix proxy.ashx?url; var url proxyPrefix encodeURIComponent(serviceUrl); fetch(url) .then(function (res) { if (res.status ! 200) { throw new Error(转发返回状态码 res.status); } return res.json(); }) .then(function (meta) { console.log(spatialReference is meta.spatialReference.wkid); console.log(layer count is meta.layers.length); }) .catch(function (err) { // JavaScript 运行时报错十有八九是这里抛出来的 console.error(err); }); } fetchServiceMetadata(http://gis-server:6080/arcgis/rest/services/Demo/MapServer);fetch的 URL 里必须先对serviceUrl做encodeURIComponent否则会把转发脚本的参数分割掉。这边把spatialReference和layers打印出来目的是确认服务可以被读到而不是只看到地图容器是空的。如果res.status是403去检查 proxy.config 的白名单是否包含目标主机如果是501多半是 ArcGIS Server 服务本身没有启动。这一章我们理清了从 REST 元数据到转发脚本再到前端 fetch 的完整链路下一步要做的是把地图容器和图层控件组织起来。3. 从页面骨架到图层控制器把顶栏、图层开关和地图绑在一起3.1 用 Dojo 小部件搭建页面框架ArcGIS API for JavaScript 早期版本深度依赖 Dojo这套资源里大量出现dojo/_base/declare、dijit/layout/BorderContainer就是延续这个传统。可能你会觉得 Dojo 已经过时但理解小部件化页面的好处没有过时区域拆分、事件订阅、生命周期管理在一张地图页面上依然适用。Dojo 的世界里没有“普通 JavaScript 对象”和“DOM 操作”的割裂感它把函数和对象统一成小部件这是 3.x 时代最常见的 WebGIS 写法。页面框架常用 BorderContainer 把屏幕分成 top、center、left 等区域地图放在 center。看一下典型布局div idapp>require([ esri/map, dojo/parser, dojo/domReady! ], function (Map, parser) { parser.parse().then(function () { var map new Map(mapPane, { center: [116.39, 39.9], zoom: 11, sliders: true }); map.on(load, function () { console.log(map loaded, spatial reference , map.spatialReference.wkid); }); }); });center这里用的是经纬度数组如果底图是 Web MercatorAPI 会自动做转换sliders控制缩放条是否渲染。这个步骤经常遇到的是地图显示成灰色多半是因为容器高度为 0需要在 CSS 里给#mapPane明确高度。3.2 图层类型与选择WebGIS 页面里图层的选择没有银弹切片图层Tile适合不常变化的底图动态图层Dynamic适合实时刷新的业务图层。这套资源第 4 章按继承关系把图层类列得很清楚这里整理成一张用得最多的对照表图层模块适合场景是否切片典型用途ArcGISTiledMapServiceLayer影像底图、基础地形是大范围浏览响应快ArcGISDynamicMapServiceLayer业务要素频繁变化否动态专题图、实时水雨情GraphicsLayer前端临时标记否点线面绘制、高亮KMLLayer外部 KML 数据叠加否应急资源导入MapImageLayer栅格影像、栅格函数否影像分析、波段叠加选型原则很直接底图优先用切片因为浏览器可以缓存业务叠加图层用动态图层因为服务端渲染能保证和数据库一致临时标注用 GraphicsLayer它只存在浏览器内存里。资源第 4.2 节还提到自定义切片图层。如果底图是本地 png 瓦片或 mbtiles不是 ArcGIS Server 发布的标准服务就需要重写getTileUrl来拼接瓦片地址这类代码在离线内网项目里特别常见。下面这段代码演示叠加一个切片底图和一个动态业务图层require([ esri/map, esri/layers/ArcGISTiledMapServiceLayer, esri/layers/ArcGISDynamicMapServiceLayer ], function (Map, TiledLayer, DynamicLayer) { var map new Map(mapPane, { center: [116.39, 39.9], zoom: 11 }); var baseLayer new TiledLayer(http://gis-server:6080/arcgis/rest/services/BaseMap/MapServer); map.addLayer(baseLayer); var businessLayer new DynamicLayer(http://gis-server:6080/arcgis/rest/services/Business/MapServer); map.addLayer(businessLayer); map.on(layers-added-result, function () { console.log(all layers ready, map level , map.getLevel()); }); });addLayer的重载顺序决定绘制顺序先加的在底层。如果两个服务坐标系不一致动态图层会和底图错位这时要回服务目录检查 spatialReference。map.getLevel()能拿到当前缩放级别后面做符号自适应时会用到。3.3 图层控制器小部件与订阅发布资源第 4.5 节单独讲了图层控制器小部件它的作用是把左边面板的 checkbox 和地图图层的visible属性绑定起来。核心是继承dijit/_WidgetBase再监听 checkbox 的onchange事件。一个精简实现如下define([ dojo/_base/declare, dijit/_WidgetBase, dijit/_TemplatedMixin, dojo/_base/lang, dojo/topic ], function (declare, _WidgetBase, _TemplatedMixin, lang, topic) { return declare([_WidgetBase, _TemplatedMixin], { layerId: null, map: null, title: 图层, templateString: div classlayer-item input typecheckbox>map.on(load, function () { topic.publish(map/ready, map); });订阅方即使在小部件创建后才收到消息也能正常取得 map。这套load - publish - subscribe模式在处理多个菜单、弹窗、工具条都依赖同一张地图时特别有用。现在图层控制器已经能控制整张地图的显隐下一步要考虑空间参考和几何对象也就是“画出来的点到底在哪里”。4. 空间参考系统与几何对象画线之前先弄清楚坐标落在哪个球上4.1 空间参考与 wkid空间参考是 WebGIS 里绕不过去的基础概念。一套 ArcGIS Server 服务发布出来元数据里一定带spatialReference.wkid它决定这个服务里的坐标是经纬度还是投影坐标。最常见的两组是 WGS84 经纬度4326和 Web Mercator 投影3857此外国内测绘项目也常用 CGCS20004490。它们的区别在于距离和面积量算的精度在页面上直接量距离时如果无视坐标系结果会差出不少。wkid名称坐标单位常见用途4326WGS84度GPS 经纬度、GeoJSON3857WGS84 Web Mercator米在线瓦片底图、浏览器地图4490CGCS2000度国家基准地理数据4499CGCS2000 / Gauss-Kruger米城市大比例尺地图拿到一个服务地址后不要靠肉眼猜坐标系直接读元数据。下面这段 fetch 代码会输出服务的 spatialReferencefetch(http://gis-server:6080/arcgis/rest/services/Demo/MapServer?fjson) .then(function (res) { return res.json(); }) .then(function (meta) { if (meta.spatialReference) { console.log(wkid: meta.spatialReference.wkid); console.log(latestWkid: (meta.spatialReference.latestWkid || meta.spatialReference.wkid)); } });latestWkid只有在旧版本服务中才有它表示同等坐标系的新版编号。比如旧式102100是3857的前身看到102100不要慌直接把它当3857处理。4.2 几何对象与绘制ArcGIS API 里的几何对象有Point、Polyline、Polygon、Extent它们都带spatialReference属性。最常见的错误是用一个经纬度Point直接往上位 Web Mercator 底图上叠加结果点落在海底。正确做法是先把 GPS 经纬度转为底图坐标系再交给图形图层渲染。require([ esri/geometry/Point, esri/geometry/webMercatorUtils, esri/layers/GraphicsLayer, esri/Graphic, esri/symbols/SimpleMarkerSymbol, esri/symbols/SimpleLineSymbol ], function (Point, webMercatorUtils, GraphicsLayer, Graphic, SimpleMarkerSymbol, SimpleLineSymbol) { var gLayer new GraphicsLayer(); map.addLayer(gLayer); // 从 GPS 设备拿到的经纬度坐标单位是度数 var srcPoint new Point(116.3913, 39.9075); // 常见做法把地理坐标转成 Web Mercator var mercatorPoint webMercatorUtils.geographicToWebMercator(srcPoint); var marker new SimpleMarkerSymbol(); marker.setColor(#ff0000); marker.setSize(10); marker.outline.setWidth(1); var graphic new Graphic(mercatorPoint, marker); gLayer.add(graphic); });new Point(x, y)的默认 spatialReference 是 4326因为 ArcGIS JavaScript API 的默认地理坐标系是 4326geographicToWebMercator返回的空间参考是 3857。如果你画的是一条带拐点的路径Polyline的构造方式是传入一个paths数组里面每个子数组都是一段坐标列表var line new Polyline({ paths: [[[116.3, 39.8], [116.4, 39.9], [116.5, 39.85]]], spatialReference: { wkid: 4326 } });这里paths是三组[经度, 纬度]层级结构是[路径段][顶点][坐标轴]。如果忘了写spatialReferenceAPI 默认也是 4326所以在瓦片底图上显示时还需要再转一次。这种“默认好”反而容易让人忽略显式声明。4.2.1 常见坐标转换排查步骤坐标对不上时按顺序做三件事第一在服务目录里复制spatialReference.wkid第二在页面 console 执行map.spatialReference.wkid对比两者第三如果服务是 3857 而你的输入是 4326调用webMercatorUtils.geographicToWebMercator。多数偏差都是因为只转换了图层没有转换几何。4.3 用 geometryEngine 做距离、缓冲与面积验证几何对象画出来之后业务上常要做量算。ArcGIS API 提供esri/tasks/geometryEngine它在浏览器端直接计算不占用服务器资源。下面这段代码对前面的线段做长度量算和缓冲区分析require([ esri/tasks/geometryEngine, esri/geometry/webMercatorUtils ], function (geometryEngine, webMercatorUtils) { var line new Polyline({ paths: [[[116.3, 39.8], [116.4, 39.9]]], spatialReference: { wkid: 4326 } }); // 注意geometryEngine 要求几何对象与坐标系单位统一 var line_3857 webMercatorUtils.geographicToWebMercator(line); var lengthMeters geometryEngine.geodesicLength(line_3857, meters); var bufferPolygon geometryEngine.buffer(line_3857, 500, meters); console.log(length in meters , lengthMeters); console.log(buffer wkid , bufferPolygon.spatialReference.wkid); });geometryEngine.geodesicLength会在椭球面上计算比平面上的planarLength更接近真实距离buffer的第二参数是距离值第三参数是单位。这里必须把线从 4326 转成 3857或者把geodesic系列方法直接用在经纬度上否则结果为度带进 buffer 就会出现一个偏差巨大的缓冲区。这套代码里常见的错误就是没做转换量出来的 500 米在实际地图上变成 500 度。验证一个坐标转换过程是否成功可以打开浏览器开发者工具在 Console 里输入map.on(click, function (evt) { console.log(mapPoint wkid evt.mapPoint.spatialReference.wkid); console.log(mapPoint x evt.mapPoint.x , y evt.mapPoint.y); });点击地图后evt.mapPoint的输出会显示点击位置的实时坐标。把它与你手动输入的原始点位对比差值如果在浮点精度范围内说明几何链路已经接通。如果差值很大多半是底图坐标系跟你的计算坐标不一致需要回头查服务元数据。到这里我们已经能够把点、线、面画在地图上也知道怎么转换和量算。最后要处理的是形态问题符号与图形让输出结果从“能跑”变成“能看”。5. 用符号与图形对象做一套可交付的地图标注5.1 符号类与图形对象的构成图形Graphic由几何、符号、属性三部分组成符号决定它长什么样。这套资源第 6 章用四类符号覆盖绝大多数需求符号类关键样式参数典型场景SimpleMarkerSymbolsize、color、outline点位标记、设备状态SimpleLineSymbolwidth、style(solid/dash/dot)边界线、路径SimpleFillSymbolcolor、outline行政区、缓冲区面TextSymbolfont、size、color、halo名称标注、坐标显示它们都能直接设颜色、透明度、外框和大小图形对象负责把它们和业务属性粘在一起。5.2 缩放级别变化时动态调整符号大小一个很实际的问题是底图缩小时固定像素大小的点符号会显得越来越密集放大后10 像素的点在地图上又像针尖一样小。更好的做法是监听地图缩放随 level 调整符号尺寸。以点符号为例map.on(zoom-end, function () { var zoom map.getZoom(); var markerSize Math.max(6, Math.min(26, 8 (zoom - 10) * 2)); pointSymbol.setSize(markerSize); // 重新渲染符号而不是重新 add graphic gLayer.redraw(); });setSize的单位是屏幕像素zoom每变化一级符号尺寸调整 2 像素。上限 26 是防止放大后符号盖住整个街区下限 6 是保持宏观可见。然后调用gLayer.redraw()让图形层立即刷新。注意这里不要用gLayer.clear()再add那样会丢失绑定的事件和属性还会闪烁。5.3 验证图形层输出把这段代码接进前面的 gLayer打开页面缩放到不同级别观察点的大小变化和相邻点的间距。如果文字标注和点符号不同步可以给 TextSymbol 的字体大小也按同样公式计算。验证时用map.setZoom(8)和map.setZoom(16)各看一眼确认符号尺寸不超过地图要素间距的一半。最后打开 Network 面板检查没有额外的服务请求说明这套符号逻辑完全跑在浏览器端不增加服务器负担。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门