【HarmonyOS实战】 MapUtil工具类:地图操作的封装哲学

【HarmonyOS实战】 MapUtil工具类:地图操作的封装哲学 文章目录前言一、MapUtil 的完整代码二、为什么要封装2.1 没有封装时的情况2.2 封装后的优势三、Observed为什么加这个装饰器四、export const mapUtil 单例模式五、MapUtil 的方法职责划分六、这种封装的局限性6.1 解耦 mapController 依赖6.2 添加错误处理总结前言GasStationPage.ets是项目里最复杂的文件但你打开它会发现地图相关的具体操作都被抽走了——移动镜头、添加标记、播放动画这些都在MapUtil里。这篇文章不只是讲MapUtil里有哪些方法更重要的是讲为什么要封装、封装的边界在哪里以及这种封装思路在你自己的项目里怎么应用。项目预览一、MapUtil 的完整代码// entry/src/main/ets/utils/MapUtil.etsimport{map,mapCommon}fromkit.MapKit;import{geoLocationManager}fromkit.LocationKit;import{Constants}from../common/Constants;ObservedexportclassMapUtil{// 坐标转换WGS84 → GCJ02publicasyncconvertToGCJ02(latitude:number,longitude:number):PromisemapCommon.LatLng{lettheWGS84Position:mapCommon.LatLng{latitude,longitude};lettheGCJ02Position:mapCommon.LatLngawaitmap.convertCoordinate(mapCommon.CoordinateType.WGS84,mapCommon.CoordinateType.GCJ02,theWGS84Position);returntheGCJ02Position;}// 移动镜头到指定坐标publicmoveToCurrentPosition(latitude:number,longitude:number,mapController:map.MapComponentController):void{letcameraPosition:mapCommon.CameraPosition{target:{latitude,longitude},zoom:15.9};letcameraUpdate:map.CameraUpdatemap.newCameraPosition(cameraPosition);mapController?.animateCamera(cameraUpdate,500);}// 获取当前 GPS 位置asyncgetMyLocation():PromisegeoLocationManager.Location{letlocation:geoLocationManager.LocationawaitgeoLocationManager.getCurrentLocation();returnlocation;}// 移动镜头到当前位置含坐标转换asyncmoveToMyLocation(mapController:map.MapComponentController):Promisevoid{letlocation:geoLocationManager.Locationawaitthis.getMyLocation();mapController?.setMyLocation(location);letgcj02Positionawaitthis.convertToGCJ02(location.latitude,location.longitude);this.moveToCurrentPosition(gcj02Position.latitude,gcj02Position.longitude,mapController);}// 添加 MarkerasyncaddMapMaker(latitude:number,longitude:number,mapController:map.MapComponentController):Promisevoid{letmarkerOptions:mapCommon.MarkerOptions{position:{latitude,longitude},rotation:0,visible:true,zIndex:0,alpha:1,anchorU:0.5,anchorV:1,clickable:true,flat:true,icon:station.svg,collisionRule:mapCommon.CollisionRule.NAME,};awaitmapController.addMarker(markerOptions);}// 播放 Marker 缩放动画asyncimageAnimation(marker:map.Marker,imageScale:number):Promisevoid{letanimationnewmap.ScaleAnimation(Constants.ONE,imageScale,Constants.ONE,imageScale);animation.setDuration(100);animation.setFillMode(map.AnimationFillMode.FORWARDS);marker.setAnimation(animation);marker.startAnimation();}}// 导出单例实例exportconstmapUtil:MapUtilnewMapUtil();二、为什么要封装2.1 没有封装时的情况假设不封装MapUtilGasStationPage.ets里的 callback 大概是这样this.callbackasync(err,mapController):Promisevoid{if(err){return;}this.mapControllermapController;this.mapController.setMyLocationEnabled(true);// 移动到当前位置直接写在这里letlocationawaitgeoLocationManager.getCurrentLocation();mapController?.setMyLocation(location);letwgs84Pos{latitude:location.latitude,longitude:location.longitude};letgcj02Posawaitmap.convertCoordinate(mapCommon.CoordinateType.WGS84,mapCommon.CoordinateType.GCJ02,wgs84Pos);letcamPos{target:gcj02Pos,zoom:15.9};letcamUpdatemap.newCameraPosition(camPos);mapController?.animateCamera(camUpdate,500);this.mapController.on(myLocationButtonClick,(){// 又一遍移动到当前位置...重复代码geoLocationManager.getCurrentLocation().then(async(location){mapController?.setMyLocation(location);letwgs84Pos{latitude:location.latitude,longitude:location.longitude};letgcj02Posawaitmap.convertCoordinate(...);letcamUpdatemap.newCameraPosition({target:gcj02Pos,zoom:15.9});mapController?.animateCamera(camUpdate,500);});});// 添加 Marker重复 4 次...for(letstationofthis.stationInfoList){letmarkerOptions{position:{latitude:station.latitude,longitude:station.longitude},rotation:0,visible:true,zIndex:0,alpha:1,anchorU:0.5,anchorV:1,clickable:true,flat:true,icon:station.svg,collisionRule:mapCommon.CollisionRule.NAME,};awaitmapController.addMarker(markerOptions);}};这段代码的问题GasStationPage文件超长难以阅读移动到当前位置的逻辑重复了两次初始化和按钮点击MarkerOptions 每次都要写一遍地图操作细节和页面业务逻辑混在一起2.2 封装后的优势封装成MapUtil后GasStationPage里的代码变成mapUtil.moveToMyLocation(mapController);// 一行mapUtil.addMapMaker(lat,lon,mapController);// 一行mapUtil.imageAnimation(marker,scale);// 一行关注点分离GasStationPage只关心做什么业务逻辑MapUtil关心怎么做技术实现。三、Observed为什么加这个装饰器ObservedexportclassMapUtil{...}给MapUtil加Observed意味着这个类的实例可以被 ArkUI 框架的状态管理系统观察到。当MapUtil的属性变化时持有它引用的组件可以自动更新。在这个项目里MapUtil没有定义State属性Observed主要是为了潜在的扩展——如果后续给MapUtil加了一些状态属性比如当前地图缩放级别、当前显示的 Marker 数量等这些属性变化时可以触发 UI 更新。四、export const mapUtil 单例模式exportconstmapUtil:MapUtilnewMapUtil();这是模块级单例的写法在模块加载时立刻创建一个MapUtil实例并导出。所有import { mapUtil } from ../utils/MapUtil的地方拿到的是同一个对象。单例的好处不需要每次new MapUtil()再调用如果MapUtil里有缓存状态比如当前位置不同地方操作的是同一份数据使用方式// 在 GasStationPage.ets 顶部import{mapUtil}from../utils/MapUtil;// 直接调用不需要 newmapUtil.moveToMyLocation(mapController);mapUtil.addMapMaker(lat,lon,controller);对比每次new的方式// 不推荐每次创建新实例letutilnewMapUtil();util.moveToMyLocation(mapController);单例模式更简洁且避免了不必要的对象创建。五、MapUtil 的方法职责划分方法职责输入输出convertToGCJ02坐标系转换WGS84坐标GCJ02坐标getMyLocation获取GPS位置无geoLocation对象moveToCurrentPosition移动镜头带坐标经纬度controller无side effectmoveToMyLocation移动镜头到我的位置controller无side effectaddMapMaker添加地图标记经纬度controller无side effectimageAnimation播放缩放动画marker比例无side effect每个方法职责单一易于理解和测试。六、这种封装的局限性项目里的封装已经很好了但如果继续扩展可以考虑6.1 解耦 mapController 依赖现在很多方法都需要传入mapController如果MapUtil维护一个内部 controller 引用会更方便exportclassMapUtil{privatecontroller:map.MapComponentController|nullnull;setController(controller:map.MapComponentController):void{this.controllercontroller;}moveToCurrentPosition(latitude:number,longitude:number):void{if(!this.controller)return;// 直接用 this.controller不需要传参}}6.2 添加错误处理目前方法里的错误处理不够完善实际项目应该给异步方法加 try/catchasyncgetMyLocation():PromisegeoLocationManager.Location|null{try{returnawaitgeoLocationManager.getCurrentLocation();}catch(err){Logger.error(MapUtil,获取位置失败:${err.message});returnnull;}}总结MapUtil的封装体现了两个核心原则单一职责每个方法只做一件事获取位置、移动镜头、添加标记、播放动画关注点分离页面GasStationPage不关心地图操作细节只调用工具类Observed export const的组合实现了一个响应式的模块级单例调用方直接import { mapUtil }就能用不需要实例化。这种工具类 单例导出的模式在 HarmonyOS 项目里很常见适合封装系统 API 调用、网络请求、存储操作等有一定复杂度的逻辑。下一篇讲bindSheet 半屏弹窗——底部加油站列表是怎么实现的bindSheet的所有参数详解。