ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Angular 集成 ArcGIS Maps SDK:地图初始化与图层叠加实战

Angular 集成 ArcGIS Maps SDK:地图初始化与图层叠加实战 很多人第一次接地图需求时项目名都写成“Augulararcgisjs之初始化地图、图层叠加”Augular 是手快敲错的 Angulararcgisjs 指的是 Esri 那套 ArcGIS Maps SDK for JavaScript现在主推 4.x 的 ESM 版本。这两个东西凑在一起是典型的“重框架 重地图库”组合Angular 管路由、状态、组件通信ArcGIS SDK 管地图渲染、图层管理、空间查询。难的地方从来不是把地图“显示出来”而是把地图当成一个可以被组件生命周期管住、可以被业务图层反复叠加上去、路由进出还不泄漏内存的东西。这篇内容适合刚上手这个组合的前端、也适合从 3.x 迁移过来踩了一路坑的老手我会把初始化、图层叠加、坐标系、销毁清理这几条主线全部拆开讲透。1. 技术选型的底层逻辑为什么是 Angular 加 ArcGIS JS1.1 从一次“地图白屏”反推架构该怎么定我见过太多项目地图代码直接塞进一个叫map.component.ts的文件里八百行初始化、图层、弹窗、点击事件全混在一起。一开始跑得挺欢等到业务方说“我要在三个页面里都嵌地图每个页面叠的图层还不一样”这套代码就彻底废了——因为你没法复用也没法按需叠加。所以第一步不是写代码是想清楚架构边界Angular 负责什么ArcGIS SDK 负责什么。我的划分原则很简单ArcGIS SDK 负责一切跟“地图对象”有关的事情包括 Map、View、图层实例、符号、弹窗模板Angular 负责一切跟“页面状态”有关的事情包括哪个图层该显示、数据从哪个接口来、用户点了哪条业务记录。两者通过一个中间层通信这个中间层通常就是一个可注入的服务Service。服务里持有一个Map实例和若干个图层实例组件只调用服务暴露的方法不直接碰 SDK 的类。这么设计的好处是组件销毁时只需要通知服务“把某个图层删掉”而不是自己去满世界找引用。再往细说一层为什么要这么切因为 ArcGIS JS SDK 的对象生命周期和 Angular 组件生命周期不是一回事。View 一旦创建就会持续监听 DOM、请求瓦片、跑渲染循环而 Angular 组件可能在路由切换时毫秒级地被销毁重建。如果两者绑定得太死要么地图被反复创建性能灾难要么组件销毁了地图还在后台跑内存泄漏。把地图对象的归属权交给一个长生命周期的服务组件只做“订阅和传参”这条链路才稳得住。1.2 4.x 带来的模块化红利别再用 3.x 的写法了3.x 时代的 ArcGIS JS API 是全局脚本require([esri/map], ...)那种 AMD 风格配合 Angular 得用esri-loader动态加载类型提示基本靠猜打包也基本靠外链 CDN。4.x 彻底换了 ESM 模块import Map from arcgis/core/Map这样按需引入配合 Angular 的构建器能直接做 tree-shaking。这个变化带来的实际收益有三点一是类型系统终于完整MapView、FeatureLayer这些类都有完整的 TypeScript 声明写代码时有补全二是构建产物可以按需拆分你没用到的 3D 模块SceneView不会被打进包三是版本升级变得可控通过 npm 语义化版本管理而不是改一个 script 地址。顺着这个逻辑我说的“别用旧写法”具体指什么别再引入esri-loader也别再用import Map from arcgis-js-api/Map这种老包名。正确的依赖就一个arcgis/core。这里有个很容易忽略的点arcgis/core里的类是按路径引入的arcgis/core/Map是一个文件arcgis/core/views/MapView是另一个文件。路径写错一个字母构建时不一定报错但运行时会出现找不到模块的白屏。我的习惯是统一用官方文档给的路径别自己猜。1.3 把地图当成“图层栈”来设计叠加逻辑才有章法“图层叠加”这四个字很多人理解成“往里 add 就完了”。实际项目里图层叠加至少有四个维度要管层级顺序、可见性开关、数据源、以及加载状态。层级顺序决定了谁盖住谁——底图永远在最下面业务面图层在中间点标记和文字标注在最上面可见性开关决定了用户能勾选哪些图层数据源决定了这个图层是走服务端 FeatureServer还是前端本地画上去的加载状态则关系到交互反馈图层没加载完就弹窗用户看到的是空白。把这四个维度抽象出来你就得到了一张图层配置表。每个业务图层对应一条配置包含id、title、type、url或数据、index、visible。组件渲染出来的图层控制面板就是遍历这张表生成的。用户勾选某个图层服务层去map.add()或者layer.visible true。这么做的价值在于图层不再是散落在代码里的 if-else而是一份可序列化的数据将来要做“保存当前视图方案”“图层配置导入导出”都会轻松很多。还有一个容易被低估的设计点图层分组。ArcGIS JS 4.x 提供了GroupLayer可以把若干子图层塞进一个组里统一控制。当你的图层数量超过七八个尤其是有“基础图层 / 专题图层 / 标注图层”这种语义分类时用 GroupLayer 收一层面板的层级感和代码的可读性都会好很多。我一般会在初始化阶段就把组的骨架搭好后面加图层时直接group.add()而不是往顶层 Map 上堆。2. 环境打通依赖、静态资源、样式三件套2.1 依赖安装与构建器适配细节装依赖就一行命令但后面两个配置不做地图一定跑不起来。npm install arcgis/corearcgis/core本身是纯 ESM 的Angular 15 之后的构建器默认就能处理。但如果你还在用比较老的 Angular 版本或者在tsconfig.json里把module设成了commonjs会遇到Cannot use import statement outside a module或者模块解析失败。这种时候的处理思路是优先升级构建配置而不是降级依赖。真要临时兼容检查tsconfig.json里的module和moduleResolutionmoduleResolution建议设成bundler或至少nodeallowSyntheticDefaultImports打开。另一个必须确认的点是TypeScript 版本。arcgis/core较新版本的类型声明会用到一些较新的 TS 特性TS 版本太老比如 4.6 以下会出现一堆莫名其妙的类型报错但代码本身其实是对的。我遇到这种情况的判断方式是看报错位置——如果报错全在node_modules/arcgis/core里的.d.ts文件那八成就是 TS 版本问题升级 TS 即可。2.2 静态资源拷贝不配这一项样式和图标会 404这是新手翻车率最高的一步。ArcGIS JS SDK 运行时要加载一批静态资源——字体图标、主题 CSS 里的图片、WebGL 着色器、还有地区相关的本地化文件。这些资源不在 JS 里而是躺在node_modules/arcgis/core/assets目录下。构建时如果不把它们拷到产物目录控制台会刷一片 404表现是地图能出来但控件图标全是方块或者比例尺、指北针位置错乱。配置写在angular.json的assets数组里{ glob: **/*, input: node_modules/arcgis/core/assets, output: /assets/ }然后在应用启动的地方比如main.ts或者一个app.module.ts的构造函数里指定路径import esriConfig from arcgis/core/config; esriConfig.assetsPath /assets;这里有个很多人绕不过去的弯为什么是/assets而不是/arcgis-assets因为拷贝规则把node_modules/arcgis/core/assets这个目录的内容映射到了产物的/assets/下而这个目录内部本身还有一个esri子目录。所以最终 SDK 实际请求的路径是/assets/esri/themes/light/main.css这类。assetsPath只需要指到/assetsSDK 自己会拼后面的相对路径。如果你把它指成/assets/esri那就多了一层全 404。我建议干脆用/arcgis-assets/这种独立目录名避免和业务自己的静态资源目录混在一起排查时一眼就能分清。2.3 样式引入与容器高度地图不显示的两个经典原因样式引入有两种写法我推荐第一种因为它不依赖 SCSS 的导入解析规则styles: [ src/styles.scss, node_modules/arcgis/core/assets/esri/themes/light/main.css ]如果你非要在styles.scss里用import注意较新的 Angular 已经不再支持~前缀~arcgis/core/...这种写法会失败得写成相对路径或者用构建器的样式入口。这个坑我在两个项目里都踩过报错信息是Cant find stylesheet to import看上去像是包没装其实是路径语法过时了。第二个经典问题是容器高度为 0。ArcGIS 的 View 在初始化时会去读容器的clientWidth和clientHeight如果这两个值是 0地图不会报错但就是不显示控制台干干净净。原因通常是容器 div 的父级没有确定高度或者用了flex但没给flex: 1。我的固定做法是给地图容器套一个定位明确的父容器父容器用position: relative加固定高度地图容器用绝对定位铺满。.map-wrapper { position: relative; width: 100%; height: calc(100vh - 120px); min-height: 420px; } .map-container { position: absolute; inset: 0; }这么写还有个附带好处组件销毁重建时DOM 节点被替换掉新节点的尺寸是确定的地图不会因为拿不到尺寸而白屏。min-height那行是我后来加的起因是有一次在小屏笔记本上calc算出来只剩 80 像素高地图虽然渲染了但等于没渲染。提示容器高度这件事我建议在View创建前用ResizeObserver监听一次尺寸变化尺寸从 0 变成非 0 时再创建 View。这个方案比死等高度的方案更稳尤其在 Tab 面板、折叠面板里嵌地图的场景下。3. 地图初始化从容器到 MapView 的完整链路3.1 组件骨架与生命周期钩子的选择先给一个可以直接抄的组件骨架。核心是三个东西ViewChild拿容器、ngOnInit里创建对象、ngOnDestroy里清理。import { Component, ElementRef, NgZone, OnDestroy, OnInit, ViewChild } from angular/core; import Map from arcgis/core/Map; import MapView from arcgis/core/views/MapView; import GraphicsLayer from arcgis/core/layers/GraphicsLayer; Component({ selector: app-map-panel, templateUrl: ./map-panel.component.html, styleUrls: [./map-panel.component.scss] }) export class MapPanelComponent implements OnInit, OnDestroy { ViewChild(mapNode, { static: true }) mapNode!: ElementRefHTMLDivElement; private map?: Map; private view?: MapView; private gfxLayer?: GraphicsLayer; constructor(private zone: NgZone) {} ngOnInit(): void { this.initMap(); } ngOnDestroy(): void { this.disposeMap(); } private initMap(): void { // 创建逻辑见下一节 } private disposeMap(): void { // 清理逻辑见第 5 章 } }这里static: true很关键。它保证在ngOnInit阶段mapNode就已经可用不需要等ngAfterViewInit。如果你的容器节点是放在*ngIf里面的那就必须用{ static: false }并把创建逻辑挪到ngAfterViewInit否则拿到的永远是undefined。这个区别我建议记牢它决定了你后面的代码写在哪个钩子里。3.2 Map 与 MapView 的参数逐项拆解Map对象本身很轻它只是个数据容器描述“有哪些图层、用什么底图”。真正吃资源的是MapView它负责把Map渲染到 DOM 上。所以初始化的顺序是先建Map再把Map传给MapView。this.map new Map({ basemap: { baseLayers: [this.buildBaseLayer()] } }); this.view new MapView({ container: this.mapNode.nativeElement, map: this.map, center: [116.397, 39.908], zoom: 11, constraints: { minZoom: 3, maxZoom: 18, rotationEnabled: false }, popup: { dockEnabled: true, dockOptions: { position: bottom-right, breakpoint: false } }, ui: { components: [zoom, compass, scale-bar] } });参数逐个说。center是经纬度数组顺序是[经度, 纬度]写反了地图会跑到地球另一边的海里这个错误我犯过不止一次而且因为不报错排查起来很费时间。zoom是初始缩放级别11大概是一个城市的视野范围5是省级16以上是街道级。constraints里的rotationEnabled: false是针对二维业务地图的常见设置——业务系统里用户旋转地图只会带来困扰关掉更省心。ui.components用来裁剪默认控件ArcGIS 默认会挂一堆控件定位、图层列表、指北针、比例尺业务系统里通常只留缩放和比例尺。popup的dockEnabled也值得单独说。默认弹窗是“气泡”形式跟着点击位置飘在密集点位场景下会遮挡其他要素而且窗口小的时候会被挤到屏幕外。开启dockEnabled之后弹窗固定在容器一角内容区宽度更可控表单类弹窗比如点击点位后展示详情、上传附件体验好很多。breakpoint: false的意思是窗口再小也不切成全屏弹窗这个看业务需求移动端可以设成true。3.3 坐标系与投影最容易埋雷的一块ArcGIS JS 4.x 支持两种内置坐标系WGS84WKID 4326和Web MercatorWKID 3857。你在center里传经纬度数组时SDK 会自动按 Web Mercator 渲染。日常用默认值就行不用手动指定spatialReference。问题出在叠加自定义图层的时候。假设你的瓦片服务或者要素服务用的是别的地理坐标系比如某些行业数据用的国家大地坐标系图层和视图的坐标系不一致SDK 会直接抛错The spatial reference of the layer does not match that of the view。这个错误信息其实已经很明确了但很多人第一反应是去改center越改越乱。正确的处理思路有两条。第一条是让视图去适配图层在创建MapView时显式指定spatialReference让整个视图用图层那套坐标系。代价是底图也得换成同坐标系的否则底图会漂移。第二条是让服务端适配视图在请求要素服务时带上outSR3857之类的参数让服务端做动态投影。多数要素服务支持这个能力瓦片服务则通常不支持动态投影只能走第一条路。我的经验是能用 Web Mercator 就用 Web Mercator其他坐标系只在需求硬性要求时才碰因为一旦偏离主流坐标系底图、瓦片、要素、空间分析全都得跟着适配工作量翻倍。还有个小细节center传经纬度时是按地理坐标系解释的如果你在MapView上设了spatialReference为某个投影坐标系center就不能再传经纬度了得传投影后的平面坐标。这个转换关系我一般用 SDK 自带的projection模块做import projection from arcgis/core/geometry/projection调projection.load()之后就能projection.project(point, targetSR)。注意这个模块需要异步加载别在同步流程里直接调。3.4 把 View 创建挪出 Angular 变更检测ArcGIS 的MapView在交互时会高频触发事件——鼠标移动、范围变化、图层加载进度。这些事件如果都在 Angular 的 Zone 里跑每一次都会触发一轮变更检测页面一卡一卡的鼠标拖拽地图时尤其明显。这个问题在图层多、组件树深的情况下会被放大。处理方式是在NgZone的runOutsideAngular里创建 Viewthis.zone.runOutsideAngular(() { this.view new MapView({ container: this.mapNode.nativeElement, map: this.map, center: [116.397, 39.908], zoom: 11 }); this.view.when(() { this.zone.run(() { this.ready true; }); }); });runOutsideAngular让 View 的事件监听脱离 Zone变更检测不再被频繁唤醒。但有个反直觉的点脱离 Zone 之后你在回调里改 Angular 组件的属性视图不会更新。比如你在地图点击事件里给this.selectedName赋值页面上绑定的文字不会变。这时候必须用this.zone.run(() { ... })包一层手动把变更检测拉回来。我一开始不知道这个写了个点击弹详情的功能数据明明变了页面就是不动查了半天才反应过来是 Zone 的问题。同步地view.when()是判断视图就绪的标准姿势。所有依赖“地图已经渲染完成”的操作——加图层、设置范围、飞行定位——都应该放在when()的回调里或者放在view.when()之后的async流程里。直接在外面调会有概率失败因为 View 还没挂载到 DOM。图层对象也有自己的when()用来判断图层数据加载完成layer.when()和view.when()是两件事别混。4. 图层叠加实战从底图到业务图层的完整装配4.1 图层类型速查与选型对照ArcGIS JS 4.x 的图层类型有二十多种实际项目里常用的就那么七八种。我把它们整理成一张表选型的时候对着看能省掉一半试错时间。图层类型数据来源典型场景前端交互能力FeatureLayer要素服务 URL点位、线、面的业务数据强支持查询、编辑、弹窗MapImageLayer地图服务 URL服务端渲染的专题图、图层组中可控制子图层可见性TileLayer缓存瓦片服务大范围底图、影像切片弱只能看WebTileLayer自定义瓦片模板第三方底图、自有切片弱只能看GraphicsLayer前端临时数据绘制、高亮、实时打点强全在前端内存CSVLayerCSV 文件或 URL批量导入的经纬度表格中支持按字段渲染GeoJSONLayerGeoJSON 文件或 URL轻量矢量数据展示中支持弹窗和查询GroupLayer其他图层实例图层分组管理无纯容器选型时的核心判断逻辑是数据量级和交互需求。几百个点需要点击弹窗、还要筛选选FeatureLayer或者前端GraphicsLayer几万个点只需要看颜色分布选服务端渲染的MapImageLayer几十万个点还要带边界和注记那就只能TileLayer或者MapImageLayer。我见过有人把两万个点全用GraphicsLayer前端画页面直接卡死改成要素服务加客户端聚合之后顺滑得不行。这个量级的门槛大概在三千到五千个图形对象超过就该考虑换方案了。4.2 图层堆叠顺序add、addMany 与 reorder4.x 里Map的图层集合是一个Collection顺序即层级索引小的在下面。所以底图的索引是 0业务点位的索引应该往大的方向走。// 逐个添加指定插到哪个位置 this.map.add(baseLayer); this.map.add(areaLayer, 1); this.map.add(pointLayer, 2); // 批量添加从某个索引开始插入 this.map.addMany([areaLayer, pointLayer], 1); // 调整已存在图层的位置 this.map.reorder(pointLayer, 5); // 移除 this.map.remove(pointLayer);add的第二个参数是可选索引不传就加到最上面。这里有个我踩过的坑索引是相对的会随着其他图层的增删而变化。你以为点位图层在索引 3但如果中间有人加了一个图层它就变成 4 了。所以不要用固定的魔法数字来管理层级而是用语义化的分组——底图放一个 GroupLayer、业务面放一个、点标记放一个组内再排序。这样你只需要保证组与组之间的顺序稳定组内的具体位置不影响大局。图层叠加还有个视觉上的细节点被面盖住。这是最常见也最容易被忽略的问题。如果面图层的索引比点图层小点就在面上层正常反过来点就被盖住了。还有一种情况是面图层用了半透明填充视觉上点还在但点击的时候拿不到点的事件因为面把点击事件截走了。判断方法是看FeatureLayer的opacity和点位是否设置了elevationInfo那个是三维的。二维场景下把面图层放到点位下面或者给面图层设popupEnabled: false减少事件干扰都能缓解。注意图层加载是异步的。map.add()返回的瞬间图层可能还没加载完这时候去layer.fullExtent会是null。需要范围信息的话用await layer.when()或者layer.load()之后再取。4.3 业务点位图层GraphicsLayer 与 FeatureLayer 的取舍GraphicsLayer是最自由的一种图层图形对象全在前端内存里你想画什么画什么。适合实时打点、绘制测量、临时高亮。import Graphic from arcgis/core/Graphic; import Point from arcgis/core/geometry/Point; import SimpleMarkerSymbol from arcgis/core/symbols/SimpleMarkerSymbol; this.gfxLayer new GraphicsLayer({ title: 实时点位, listMode: hide }); const graphic new Graphic({ geometry: new Point({ longitude: 116.397, latitude: 39.908 }), symbol: new SimpleMarkerSymbol({ color: [0, 122, 255, 0.9], size: 12, outline: { color: [255, 255, 255], width: 1.5 } }), attributes: { id: P001, name: 一号监测点, status: normal }, popupTemplate: { title: {name}, content: [{ type: fields, fieldInfos: [ { fieldName: id, label: 编号 }, { fieldName: status, label: 状态 } ]}] } }); this.gfxLayer.add(graphic); this.map.add(this.gfxLayer, 10);listMode: hide是个小技巧它让这个图层不出现在默认的图层列表控件里——临时图层通常不需要用户手动开关。popupTemplate里的{name}是字段插值语法直接引attributes里的键不用写 JS 表达式。这些细节文档里有但散布在不同页面实际写的时候容易漏。FeatureLayer则是走服务端的配置项更多但能力也更强import FeatureLayer from arcgis/core/layers/FeatureLayer; const pointLayer new FeatureLayer({ url: https://your-server/arcgis/rest/services/Demo/FeatureServer/0, title: 设备点位, outFields: [*], definitionExpression: status 1, minScale: 50000, maxScale: 0, renderer: { type: simple, symbol: { type: simple-marker, style: circle, size: 8, color: [255, 149, 0], outline: { color: [255, 255, 255], width: 1 } } }, popupTemplate: { title: 设备{deviceName}, content: [{ type: fields, fieldInfos: [ { fieldName: deviceName, label: 名称 }, { fieldName: updateTime, label: 更新时间, format: { dateFormat: yyyy-MM-dd HH:mm } } ]}] } }); await pointLayer.load(); this.map.add(pointLayer, 10);outFields: [*]是个偷懒写法会把所有字段拉回来。字段多的服务这么写会明显拖慢加载正确做法是只列业务需要的字段。definitionExpression相当于服务端的 where 条件把筛选下沉到服务端比前端过滤高效得多。minScale和maxScale控制图层的显示比例范围——比如点位太密的时候在小比例尺下不显示放大到一定级别才出来这个能显著改善视觉混乱。注意这两个值是反的minScale: 50000表示“比例尺小于 5 万时不显示”也就是缩得很远的时候不显示逻辑上确实有点反直觉我第一次配的时候配反了。4.4 自定义瓦片图层模板 URL 与子域名的写法接入自有瓦片服务或者第三方底图用WebTileLayer。它的核心是urlTemplate用占位符拼请求地址import WebTileLayer from arcgis/core/layers/WebTileLayer; const customTiles new WebTileLayer({ urlTemplate: https://{subDomain}.tile.example.com/{level}/{row}/{col}.png, subDomains: [t0, t1, t2, t3], title: 自定义底图, copyright: 数据来源说明 });占位符的含义{level}是缩放级别{row}是行号{col}是列号{subDomain}会从subDomains数组里轮询取值。子域名的作用是绕过浏览器的并发连接数限制——同一个域名下浏览器最多同时开六到八个连接切成四个子域能显著提升瓦片加载速度。这是我在一个瓦片加载慢的案例里改的改完首屏瓦片出图快了将近一倍。坐标系仍然是这里的关键约束。WebTileLayer默认按 Web Mercator 请求瓦片如果你的瓦片是按其他投影切的需要显式指定tileInfo和spatialReference。这个配置比较长我的建议是直接照着服务提供方给的切片方案TileMatrixSet抄参数别自己推。抄完一定要在浏览器网络面板里看实际请求的 URL确认行列号和级别对得上——如果对不上地图上会出现错位或者空白格。4.5 弹窗、符号与图层联动图层加完只是骨架真正让地图“能用”的是弹窗和联动。弹窗这块除了前面说的popupTemplate字段插值更复杂的需求需要用自定义内容函数popupTemplate: { title: {name}, content: (feature) { const div document.createElement(div); div.className popup-card; div.innerHTML pstrong编号/strong${feature.graphic.attributes.id}/p pstrong状态/strong${feature.graphic.attributes.status}/p ; const btn document.createElement(button); btn.textContent 查看详情; btn.onclick () this.openDetail(feature.graphic.attributes.id); div.appendChild(btn); return div; } }用原生 DOM 构造内容好处是不依赖 Angular 模板编译坏处是没有类型检查。更优雅的做法是用content返回一个自定义元素的标签名然后把这个自定义元素定义成一个 Angular 组件——不过这条路需要处理 Angular 的变更检测和自定义元素注册工程量不小。我的建议是简单信息展示用字段插值中等复杂度用 DOM 构造只有弹窗里有复杂表单和状态时才上自定义元素。图层联动是另一个高频需求点击列表里的一条记录地图飞过去并高亮那个点位。实现上分两步第一步是定位this.view.goTo({ target: graphic.geometry, zoom: 16 }, { duration: 800, easing: ease-in-out });第二步是选中态。ArcGIS 的view上有个view.highlight()方法可以直接把某个 Graphic 高亮不需要你手动改符号this.view.when(() { this.view.highlight(graphic); });高亮是叠加在图层之上的视觉层不会改变原图层的渲染器也不会影响其他要素。这个 API 我强烈推荐早期大家都是“复制一个 Graphic 用不同符号盖上去”代码冗余还容易出 bug。goTo的duration参数控制动画时长800毫秒是我觉得比较舒服的值太快没感觉太慢让人等。5. 常见问题与排查技巧实录5.1 地图不显示类问题速查表这类问题的特征是控制台不报错或者只报一些无关紧要的警告但屏幕上什么都没有。我整理了一份速查表按出现频率排序。现象大概率原因排查手段容器区域完全空白容器高度为 0在 DevTools 里选中容器看计算高度有底图无控件图标assets 未拷贝或路径错网络面板看/assets/esri/...是否 404地图显示但样式错乱主题 CSS 未引入检查 styles 入口配置地图尺寸不对、有灰边容器尺寸变化后未重算调用view.container后触发 resize地图出现但点位不显示图层坐标系与视图不匹配看控制台是否有 spatial reference 报错拖拽卡顿严重View 在 Zone 内创建改为runOutsideAngular“有灰边”这个问题值得展开说。它的成因是 View 在创建时记住了容器的初始尺寸后来容器被 CSS 改大了View 内部的画布还是老尺寸。ArcGIS 在窗口 resize 时会自动重算但如果变化来自容器本身比如侧边栏收起它感知不到。解决办法是拿一个ResizeObserver监听容器尺寸变了就调一次view的尺寸同步。SDK 里没有公开的resize()方法实际可行的做法是把view.container重新指一次或者更简单——监听变化后什么都不做只改一下view.padding也能触发重排。这个技巧有点偏方但在多个项目里都奏效。5.2 图层加载异常与跨域问题图层加不上通常分三类。第一类是路径或服务名写错表现为控制台报 404 或者Unable to load layer。这个最好排查把 URL 复制到浏览器地址栏打开服务正常的话会返回一段 JSON。第二类是服务需要鉴权表现是 401 或者返回一段 HTML 登录页。处理方式是配 token可以在esriConfig.request.interceptors里统一拦截并附加参数esriConfig.request.interceptors.push({ urls: /your-server\.com/, before: (params) { params.requestOptions.query params.requestOptions.query || {}; params.requestOptions.query.token this.authToken; } });这里的关键是urls用正则匹配只拦截你自己的服务别把官方底图的请求也改掉否则会出现底图加载失败这种莫名其妙的连带问题。第三类是跨域表现是控制台报 CORS 错误请求被浏览器拦下来。这类问题前端解决不了需要服务端加响应头或者走同源代理。有个例外ArcGIS 的部分官方服务允许跨域而自建服务默认不允许这一点排查时要分清楚。还有一个容易被忽视的情况是图层对象被重复创建。比如在某个按钮的点击事件里new FeatureLayer(...)然后map.add()用户点两次就有两个同名图层视觉上看起来是重影。排查方法是打印map.layers.length或者在图层上加id然后map.findLayerById()判断是否已存在。我的习惯是每个业务图层都给一个稳定的id创建前先查重const exist this.map.findLayerById(device-points); if (exist) { this.map.remove(exist); }5.3 内存泄漏与路由切换白屏这是 Angular 场景下最典型的一类问题也是最难排查的。表现是反复进出地图页面页面越来越卡最后浏览器标签页直接崩掉。原因通常是MapView没有被销毁还在后台跑渲染循环。清理的正确姿势private disposeMap(): void { if (this.view) { this.view.container null as any; this.view.destroy(); this.view undefined; } if (this.map) { this.map.removeAll(); this.map.destroy(); this.map undefined; } }view.container null这一行是 Angular 场景下的特殊处理。ArcGIS 的view.destroy()会把容器节点从 DOM 里移除但在 Angular 里这个节点由框架管理SDK 擅自移除会和 Angular 的视图树状态打架导致路由切换时报Cannot read properties of null之类的错误。先把container置空destroy()就不会去动 DOM 了剩下的交给 Angular 清理。这个坑我印象很深当时排查了两个下午。路由切换白屏是另一个相关的现象。原因经常是地图组件被 Angular 复用了比如没配路由复用策略但地图对象被服务缓存着组件重建时新容器是空的旧 View 还指着已经不在 DOM 里的节点。解决办法是让服务暴露一个attach(container)方法组件重建时把新容器传给服务服务内部调用view.container newContainer把 View 重新挂上去。这样地图状态当前范围、图层勾选全部保留用户切换路由回来还是原来的视野体验比重新加载好得多。6. 性能优化与工程化落地6.1 打包体积与按需加载arcgis/core完整包体积很大但它是按模块引入的只 import 用到的部分构建时能 tree-shaking。前提是别写整体引入// 不推荐会把整包拖进来 import * as esri from arcgis/core; // 推荐按类引入 import Map from arcgis/core/Map; import MapView from arcgis/core/views/MapView;如果你还用了三维视图SceneView那体积会再上一个台阶因为三维模块依赖 WebGL 相关的代码。业务上只做二维的话坚决不要引入SceneView哪怕只是想“留着以后扩展”——以后要用的时候再加成本比现在背着它跑要低。路由层面地图组件应该放进懒加载模块。地图相关的代码和依赖只在访问地图路由时才下载首页加载不受影响。这个配置很简单loadChildren指向一个独立模块即可。6.2 把图层管理封装成一个可复用服务前面聊了很多散点这里收一下给一个服务层的设计。核心是维护一个图层注册表用id索引对外暴露增删改查方法。Injectable({ providedIn: root }) export class MapLayerService { private map?: Map; private registry new Mapstring, Layer(); bindMap(map: Map): void { this.map map; } upsert(id: string, factory: () Layer, index?: number): Layer { if (!this.map) throw new Error(map not bound); const old this.registry.get(id); if (old) { this.map.remove(old); this.registry.delete(id); } const layer factory(); if (index ! undefined) { this.map.add(layer, index); } else { this.map.add(layer); } this.registry.set(id, layer); return layer; } setVisible(id: string, visible: boolean): void { const layer this.registry.get(id); if (layer) layer.visible visible; } remove(id: string): void { const layer this.registry.get(id); if (layer this.map) { this.map.remove(layer); this.registry.delete(id); } } clear(): void { this.registry.clear(); this.map?.removeAll(); } }upsert里用了工厂函数而不是直接传图层实例这样服务内部可以决定“先销毁旧的再建新的”避免调用方自己管理销毁。registry用的是原生Map不是 ArcGIS 的Map这里命名容易混实际项目里我一般把 ArcGIS 的 Map 类型写成EsriMap别名省得自己看晕。这不算什么高深设计但它能解决一个很实际的问题图层被谁创建的、被谁删掉了有据可查。出问题的时候打个日志把 registry 的 key 全列出来一眼就能看出哪个图层没清掉。6.3 几个我踩过的坑和对应经验最后聊几个零散但很实在的点。第一个是图层可见性不等于不加载。layer.visible false只是不渲染图层数据该请求还是请求瓦片该拉还是拉。真正想省流量得把图层从map里remove需要时再add回来。对于几十个图层的大项目这个区别能省下可观的带宽。我通常按“当前视野范围内可见”来动态增删图层而不是全量挂载再控制 visible。第二个是弹窗里的字段格式化。日期字段从服务端拿回来通常是时间戳直接塞进弹窗是一串数字。popupTemplate的fieldInfos支持format配置dateFormat写yyyy-MM-dd HH:mm就能出好。数值字段要加单位的话用format: { places: 2, digitSeparator: true }控制小数位和千分位。这些配置文档里有但很容易被忽略结果就是弹窗里一堆没格式化的原始值。第三个是地图范围缓存。用户拖了半天地图切到别的页面再回来范围回到初始值体验很差。做法是在view.watch(extent, ...)里把范围记到服务里组件重建时用记下来的范围初始化MapView。注意extent变化触发非常频繁一定要做防抖我一般用 300 毫秒既不会丢状态也不会拖慢性能。第四个是测量和绘制的坐标系一致性。地图上做距离测量结果跟用户拿尺子量出来的对不上八成是投影问题。Web Mercator 在高纬度地区面积和距离失真严重测量结果偏差很大。做正经测量功能的话要么用地理坐标系加球面距离计算要么明确告诉用户这是近似值。这个坑我在一个跨省的项目上遇到过客户拿测量结果跟实测数据对比差了百分之十几最后靠改测量算法解决。第五个是地图容器的 CSS 隔离。ArcGIS 的主题 CSS 会定义一堆全局样式如果你的业务页面里也有.esri-开头的类名概率极低但存在或者反过来你的全局样式影响了地图控件就会出现控件错位。我的做法是给地图容器外层套一个特有的类名把地图相关的样式全部限制在这个作用域内避免污染。用ViewEncapsulation.None时要格外小心地图组件里的样式会漏到全局去我一般宁可写在全局样式文件里也不开None。这套组合用下来我的整体感受是难点不在 ArcGIS 的 API 有多复杂而在于它和 Angular 的生命周期、变更检测、DOM 归属这三条线怎么对齐。初始化阶段把 View 挪出 Zone、把容器尺寸钉死、把 assets 路径配对这三件事做对了后面百分之八十的怪问题都不会出现。图层叠加阶段把图层当成数据来管而不是当成代码来写扩展性会好很多。至于内存泄漏只要记住“谁创建谁销毁、销毁前先松容器”这一条基本不会出大问题。
返回列表