ArcGIS API for JavaScript 3.x 到 4.x 的升级手册
众所周知,3.x 版本主要是构建二维地图,且基本不会再添加新功能;而 4.x 版本主要是构建于三维地图,与 3.x 相比并不是简单的升级,基本上就是重写了。所以当我们需要把 API 从 3.x 升级到 4.x 时,应用程序基本上是需要重写的,这里将对 API 升级过程中涉及到的相关变动进行记录与描述。
以下皆以 vue2 框架为基础进行 API 版本升级。
首先放上 ArcGIS API for JavaScript 4.x 的官方文档链接,当前最新版本是 4.30(2024年6月更新)
https://developers.arcgis.com/javascript/latest/api-reference/
1. ArcGIS API for JavaScript 4.x 版本调研
ArcGIS API for JavaScript 的版本与 vue 的版本有些对应关系,vue2 可以装 4.27 及以下版本,4.27 以上需要vue3 版本。
对应关系 | 4.27及以下 | 4.28 | 4.29 | 4.30 |
---|---|---|---|---|
Vue2 | √ | × | × | × |
Vue3 | √ | √ | √ | √ |
2. 升级步骤
-
安装 @arcgis/core,为了适配 Vue2,选择版本为 4.27
npm i @arcgis/core@4.27.0
-
新版本 ArcGIS API for JavaScript 引用了 ES2020 新版本的规范,不做处理会导致旧版本 Webpack 解析出错,所以安装好 @arcgis/core 后需要安装依赖或升级框架。
-
下载依赖(用于处理相关语法)
npm i -D @babel/core @babel/plugin-proposal-nullish-coalescing-operator @babel/plugin-proposal-optional-chaining babel-loader
-
配置 vue.config.js(开发环境与生产环境都要加上如下配置代码)
module: { rules: [ { test: /\.m?js$/, exclude: { and: [/node_modules/], not: [ /@arcgis[\\/]core/, /@esri/ ] }, use: { loader: "babel-loader", options: { plugins: [ ["@babel/plugin-proposal-nullish-coalescing-operator", { loose: true }], ["@babel/plugin-proposal-optional-chaining", { loose: true }] ] } } } ] }
[ 参考文档:https://blog.csdn.net/qq_34443031/article/details/127285197 ]
-
-
字体文件配置
当前版本的字体库不支持微软雅黑等字体,需要自行部署本地 PBF 字体地址,可通过 esriConfig 的 fontsUrl 来自定义字体调用地址。如果不指定,则默认的字体调用地址,是 esri 的
https://static.arcgis.com/fonts
。import esriConfig from '@arcgis/core/config' // 需修改为自定义字体地址 esriConfig.fontsUrl = "http://localhost:1570/fonts";
做好相关配置,执行 npm i
和 npm run dev
运行即可。
3. 相关组件变化
4.x 无论是在调用方式和使用上与 3.x 相比都做了很大的变革,是全新的版本,所以 4.x 无法向下兼容 3.x。举个 Graphic
组件的例子:
// 3.x 的调用方式,其构造函数为:new Graphic(geometry?, symbol?, attributes?, infoTemplate?)
var graphic = new Graphic(pt,sms,attr,infoTemplate);
// 4.x 的调用方式,new Graphic(properties)
const polylineGraphic = new Graphic({
geometry: polyline,
symbol: polylineSymbol,
attributes: polylineAtt
});
3.x 直接传递参数给构造函数,而 4.x 通过传递参数对象来创建实例。可以看出来其实 4.x 的传递方式是更加合理的。在 4.x 中几乎所有的组件调用都是通过这个方式来传递的。
除了调用方式,在组件的使用上也发生了很大的变化,很多组件都是无法继续沿用 3.x 的方法和属性的。接下来对相关组件的使用进行介绍。
3.1. 依赖的引入
在 3.x 中对于 API 的载入是使用了 esri-load 的 loadModules 方法,而在 4.x 中已经可以使用 ESM 的模块规范导入。可以看出来 4.x 的依赖引入方式简洁的多。
-
3.x
loadModules([ 'dojo/_base/Color', 'esri/graphic', 'dojo/domReady!' ]).then( ([Color, Graphic]) => { // ... } )
-
4.x
import Color from '@arcgis/core/Color' import Graphic from '@arcgis/core/Graphic' // ...
其实 4.x 也支持 loadModules 或使用 AMD(require)的方式来引入,但我个人比较喜欢实用 import 的方式。
3.2. Map 地图加载
-
3.x
// 只需要Map this._mainMap = new Map(this._divId, { center: this.initCenter, logo: false, slider: false, showLabels: true, zoom: tmpZoomSize, minZoom: 3, maxZoom: 20 })
-
4.x
// Basemap可选,但需要Map、MapView const basemap = new Basemap({ baseLayers: [layer1, layer2, layer3], title: "basemap", id: "basemap" }); this._mainMap = new Map({ basemap }) this._mainView = new MapView({ map: this._mainMap, showLabels: true, container: this._divId, center: this.initCenter, constraints: { lods: TC.LODS_MAP_VIEW, minZoom: 3, maxZoom: 20 }, zoom: tmpZoomSize, })
在 3.x 中对底图的加载都是由 Map 类进行统一管理,加载方式比较直接简单,而在 4.x 中底图加载需要 Map 与 MapView 共同运作。在官网的解释是,MapView 是用于显示 Map 实例的 2D视图,必须创建 MapView 的实例以 2D 的方式呈现地图。如果想要在 3D 中渲染地图及其图层,请参考 SceneView 的文档。
所以可以看出因为 4.x 新增了 3D 的功能所以把视图的展示分开了。
3.3. MapImageLayer 动态图层
在 3.x 中对于动态地图的加载有很多不同种的类,例如 ArcGISDynamicMapServiceLayer
、ArcGISDynamicLayer
等,在 4.x 中已经废除了这种用法,统一使用 MapImageLayer
类进行使用。
-
3.x
const layer = new ArcGISDynamicMapServiceLayer(url + this.gisToken)
-
4.x
import MapImageLayer from '@arcgis/core/layers/MapImageLayer' import esriId from '@arcgis/core/identity/IdentityManager' // 注册权限 esriId.registerToken({ token: this.gisToken, server: arcgisServer + 'arcgis/rest/services' }) let layer = new MapImageLayer({ url: url, // 控制子类图层显隐,具体用法可查看官网解说 sublayers: [] })
这里对 4.x 的动态图层用法进行解释。
在 4.x 中图层的加载,直接在 URL 加上 token 参数传递凭证的方式是不允许的,如果直接在 url 后加上 token 参数会报 token 无效警告,且地图也无法展示。那么在加载使用 FeatureLayer 时,FeatureLayer 类提供了一个 apiKey 的参数可以用来传递 token,而 MapImageLayer 动态图层中我没发现类似的参数可以传递 token,通过多次尝试和查看官方文档最终发现 4.x 中有一个专门进行权限注册的类 IdentityManager,可以通过其 registerToken 的方式对 token 进行权限注册,这样 MapImageLayer 动态图层也可以正确的显示了。
针对动态图层,当下只发现了这种 token 注册方式,后续可以再继续研究一下有无更好更简易的方法。
扩展思考
既然有这种通过注册权限的方式来对 MapImageLayer 进行 token 的注册,是否可以尝试,直接对 token 进行统一的管理,全局统一注册,即无论是何种类型的图层,都可以不用在加载图层时再手动的去加上 token 的参数,由系统自动注册加载。
这种方式还未进行过尝试,需要对 IdentityManager 这个类进行更深更进一步的研究和了解。
3.4. Sketch 地图绘制
在 3.X 中地图的绘制是通过创建一个 Draw 这个类的实例来进行绘制,而 4.X 在图形的绘制上提供了一种更为便捷的方式,即 SketchViewModel 小工具类。如下为两种方式的一些编写区别
-
3.x
require([ "esri/toolbars/draw", "esri/graphic", ... ], function(Draw, Graphic, ... ) { function createToolbar(map) { // 创建一个图形绘制对象 var toolbar = new Draw(map); // 监听事件结束事件 toolbar.on("draw-end", addToMap); } function addToMap(evt) { var graphic = new Graphic(evt.geometry, symbol); map.graphics.add(graphic); } ... });
-
4.x
// 创建一个图形绘制对象 this.a_sketchViewModel = new SketchViewModel({ view: this.mapItemView, layer: new GraphicsLayer(), polygonSymbol: this.a_selPolygonSymbol }) // 创建绘制 this.a_sketchViewModel.create(newVal[0], { mode: 'hybrid' }) // 监听事件 this.a_sketchViewModel.on('create', evt => { if (evt.state === 'complete') { this.selEnd(evt) } })
这么看下来似乎区别并不大。实际上在 4.x 中也提供了 Draw 这个绘制类,但是他与 SketchViewModel 有一些使用上的区别。
SketchViewModel 用于提供一个工具库让开发人员能够很简单的去构建一个集合图形,而 Draw 则为开发人员提供了高级绘图功能,比如说防止用户绘制具有自相交线条或重叠多边形的图形,则可以使用此类来实现这些规则。所以开发人员可以根据实际的需要去调用。
3.5. Popup 弹窗
在 3.x 中弹窗组件是 InfoWindow,而在 4.x 中则去除了这个类,可以使用 Popup 相关类来做弹窗的功能。
-
3.x
this.a_infoWindow = new InfoWindow(null, domConstruct.create('div', null, dom.byId('map-content'))) this.a_infoWindow.setMap(this.mapItem) this.a_infoWindow.resize(380, 50) // 完成小部件的创建 this.a_infoWindow.startup() this.a_infoWindow.setContent(innerHTML) // 设定弹窗显示的位置 this.a_infoWindow.show(obj.mapPoint, this.P_InfoWindow.ANCHOR_UPPERRIGHT)
-
4.x
// 4.27 才有的使用方法 this.mapItemView.openPopup({ location: this.a_mouseInFeature.geometry, content: t, actions: [], collapseEnabled: false, dockOptions: { buttonEnabled: false // 隐藏固定标签页 } }) this.PKDLayer.popupTemplate = { location: this.a_mouseInFeature.geometry, content: t }
可以看到其实 4.x 的弹窗调用方式比 3.x 的更清晰明了,但同时 MapView.openPopup 这个方法的使用也比较新,在 4.27 的版本中才能使用。
通过查看官方文档可以发现,在 4.x 中弹窗相关的类有三个,PopupTemplate、Popup、PopupViewModel,他们在使用和含义上是有些区别的。
PopupTemplate
:用于定义弹出窗口内容和结构的模板对象,它允许开发者预定义弹出窗口的布局、字段显示和样式。通常与图层(如FeatureLayer
)关联,并在图层的popupTemplate
属性中设置。以控制弹出窗口中显示哪些字段、如何格式化这些字段以及如何组织弹出窗口的布局。Popup
:实际的弹出窗口实例,它在地图上显示与特定地理要素(Feature
)相关的信息,Popup
会根据PopupTemplate
的定义来显示内容。Popup
对象包含了弹出窗口的配置选项,如位置、锚点、可见性等。PopupViewModel
:视图模型,用于管理Popup
的状态和行为,它提供了对弹出窗口数据的双向绑定支持,允许开发者在视图中更灵活地响应用户交互和数据变化。 通常与Popup
组件一起使用,以实现更复杂的交互逻辑和动态内容更新。
所以总结来说PopupTemplate
定义了弹出窗口的结构和样式,Popup
是实际显示在地图上的窗口实例,而 PopupViewModel
则提供了一个用于管理弹出窗口状态和行为的视图模型,实际使用可以根据需要选择合适的组件来创建交互式的地图应用。
3.6. FeatureLayer 要素图层
FeatureLayer 要素图层的操作在 3.x 和 4.x 版本中有很大的不同,一个是 token 的传递,一个是事件的监听。
3.6.1. 创建
首先来看一下 FeatureLayer 在创建上的不同。
-
3.x
this.projectLayer = new FeatureLayer(`http://xxx.arcgis/rest/services/sde/MapServer/0?token=${mapToken}`)
-
4.x
this.projectLayer = new FeatureLayer({ url: "http://xxx.arcgis/rest/services/sde/MapServer/0", apiKey: mapToken })
可以看到,在对 FeatureLayer 要素图层的创建上,3.x 可以直接把 token 凭证直接放在 url 地址的后面进行传递,而在 4.x 中不允许这么使用。如果将 token 挂在 url 后面会报 token 无效不允许这么传递的异常。
在 4.x 中给出了一个 apiKey 的参数专门用于传递 token。这么设计其实更加清晰,地址是地址,token 是 token。
3.6.2. 事件监听
对于图层的事件监听也有很大的改变。先来看看具体的代码。
-
3.x
this.featureLayer.on('click', evt => { ... } this.featureLayer.on('mouse-over', evt => { ... } this.featureLayer.on('mouse-out', evt => { ... }
-
4.x
this.handleEvent('click', this.featureLayer, result => { ... }, () => {}) this.handleEvent('mouse-over', this.featureLayer, result => { ... }, () => {}) this.handleEvent('mouse-out', this.featureLayer, result => { ... }, () => {}) handleEvent(event, featureLayer, activeFun, nonActiveFun) { return this.mapItemView.on(event, evt => { this.mapItemView.hitTest(evt).then(response => { if (response.results.length === 0) { nonActiveFun() return } const clickLayer = response.results.filter((result) => { return result.graphic.layer === featureLayer }) if (clickLayer.length > 0) { activeFun(clickLayer[0]) } else { activeFun(null) } }) }) },
可以看到在事件的监听上,4.x 比 3.x 复杂了很多。在 3.x 中,对于 FeatureLayer 的事件监听可以直接挂在图层上,而在 4.x 中 FeatureLayer 失去了这个功能,没有了监听功能,而是将事件监听都在 MapView 上进行了统一的管理。
首先在 MapView 上监听所有的事件,之后使用 hitTest 方法来获取所有结果。在官方文档中,对于 hitTest 的解释是这样的:
返回从每个图层中检索到的与指定屏幕坐标相交的碰撞检测结果。这些结果被组织为一个数组,其中包含不同类型的结果对象。
以下类型的图层在遇到相交特征时会返回所有特征:FeatureLayer、CSVLayer、GeoJSONLayer、GeoRSSLayer、GraphicsLayer、KMLLayer、MapNotesLayer、OGCFeatureLayer、StreamLayer、SubtypeSublayer、VectorTileLayer和WFSLayer。
也就是说 hitTest 会根据 MapView 中的点击结果去对当前加载的每个图层进行检索,并返回所有与指定屏幕坐标相交的结果。
所以如果对于 FeatureLayer 没有监听事件的解决方案我的做法是,通过 hitTest 来获取所有被点击的图层,找到当前需要 FeatureLayer 的要素返回。
4.x 使用这种方式来管理虽然增加了一些代码上的复杂度但也有好处,将所有的事件统一在了一起,当需要获取多个图层的点击要素也会变得很方便。
3.7. Legend 图例
图例在使用上其实 3.x 和 4.x 的区别不大。
-
3.x
this.legned = new Legned({ map: this.mapItem, layerInfos: layerInfo }, 'LegendDiv') // 启动图例 this.legned.startup() // 控制图层显隐并刷新图例 this.legned.refresh(layerInfo)
-
4.x
// 捞出所有的要素图层 const lns = [] this.mapItemView.map.layers.forEach(l => { if (l.type === 'feature' || l.type === 'map-image') { lns.push({ layer: l, title: l.title }) } }) // 图例 this.legned = new Legned({ view: this.mapItemView, layerInfos: lns, container: 'LegendDiv' })
4.x 更加简洁了,没有多余的开始刷新方法,内部会自动的去获取当前显示图层,动态显示。
3.8. Map事件监听
这也是一个很有意思的改动。
在之前 3.6 节中有提到过,FeatureLayer 的事件监听必须通过 MapView 来实现,实际上 MapView 直接提供的事件并不多。在原先 3.x 中,比如 zoom、extent 之类的属性发生了变化,可以直接监听 zoom-change 或者 extent-change 事件,而在 4.x 中并没有提供这些方法。在新的 API 中引入了 reactiveUtils 这么一个类。
先来看看两种版本的用法对比。
-
3.x
this._mainMap.on("extent-change", () => { ... })
-
4.x
reactiveUtils.watch(() => mainMapView.extent, e => { ... }) reactiveUtils.watch(() => mainMapView.zoom, e => { ... })
官方文档中对 reactiveUtils 的描述如下:
reactiveUtils 提供了观察 API 属性状态变化的功能,是管理应用程序生命周期的重要组成部分。可以在各种不同的数据类型和结构上观察状态,包括字符串、数字、数组、布尔值、集合和对象。
reactiveUtils 提供了五个方法,它们提供了观察状态的不同模式和功能:on()、once()、watch()、when() 和 whenOnce()
结合代码和文档内容,可以知道有了 reactiveUtils 这个类之后不管是监听什么组件属性的变化都变得很简单,只需要在所需要的属性上 watch 一下就好。
以上提到的组件是我在对 API 进行升级时遇到变化比较大的类,当然在 4.x 中几乎所有的类都发生了重大的变化,之后如果有遇到会继续进行补充。其实 4.x 突出的功能是对三维地图的展示,在此也并未体现,希望之后可以有机会继续探索。
欢迎大家一起参与讨论。
本文来自博客园,作者:knqiufan,转载请注明原文链接:https://www.cnblogs.com/knqiufan/p/18348731