前端JS接入高德地图——自定义 Hook(useAMap)优雅接入

665 阅读4分钟

说明

上次已经讲了怎么简单的接入高德地图到项目中(JS 如何接入高德地图?),这里我将结合真实的业务场景来接入高德地图,并且会封装一个通用hook useAMap(),有类似业务场景的小伙伴可以拿去复用。

业务场景

在一些表单中需要为一些地点选择地址信息 包括省市区详细地址,经纬度等。

image.png

在这里把选择位置分为三个模块,选择省市区详情地址一个模块,地图一个模块,经纬度一个模块。

这里省市区和经纬度都是可以手动输入的,地图模块会有一个可以拖动的标记,可以在拖动选择位置。

场景:是当三个模块任意一个变化时,另外两个模块也要跟着变化到对应位置

例如:

1.选择北京-北京辖区—东城区 天安门,那么地图自动更新定位到天安门,经纬度自动设置天安门经纬度。

2.地图拖动标记到 上海外滩 那么省市区详细地址自动更新到上海外滩的位置,经纬度也会变化到外滩。

3.随便输入一个有效经纬度,地址和地图也随之变化到对应位置

问题分析

首先这三个模块单独看是很好实现的,复杂点在如何实现联动。联动可以拆解下面几个问题:

  • 如何通过一个地址获取经纬度
  • 如何通过地址设置地图位置
  • 如何通过经纬度获取到地址
  • 地图图标拖动的时候如何得到经纬度和地址

这些问题其实都可以通过高德地图的插件实现。这里注意的是地图只能通过经纬度设置位置,如何地址设置地图位置,需要先将地址转化为经纬度。

hook 设计

既然以上几个问题都可以通过高德插件实现,那我们的hook肯定是要把这些功能封装进去的,除此之外hook还要包含地图的初始化等功能。

以下就是hook的大概功能


interface IUseAMapProp {
  // 地图容器id
  eleId: string;
  // 地图标记拖动变化回调
  onChang: () => void;
}

interface IUseAMapRes {
  //通过经纬度获取地址信息
  getAreaByLngLat: (lng: number, lat: number) => Promise<any>;
  //通过地址获取经纬度
  getLngLatByArea: (area: string) => Promise<any>;
  //通过经纬度设置地图位置
  setMapLocation: (lng: number, lat: number) => void;
}

export const useAMap = (props: IUseAMapProp): IUseAMapRes => {
  const { eleId, onChang } = props;

  const getAreaByLngLat = (lng: number, lat: number) => {};

  const getLngLatByArea = (area: string) => {};

  const setMapLocation = (lng: number, lat: number) => {};
  return {
	getAreaByLngLat,
	getLngLatByArea,
	setMapLocation
  };
};

hook实现

里面的注释写的很详细

代码片段

这里省去了省市区select框的选择

code

import { useEffect } from 'react';
import AMapLoader from '@amap/amap-jsapi-loader';
import { Message } from '@alifd/next';

interface IUseAMapProp {
  // 地图容器id
  eleId: string;
  // 地图标记拖动变化回调
  onChang: (val: {
    areaList: any[]; // 区域列表
    address: string; // 详细地址
    lng: number; // 经度
    lat: number; // 纬度
  }) => void;
}

interface IUseAMapRes {
  //通过经纬度获取地址信息
  getAreaByLngLat: ({
    lng,
    lat,
  }: {
    lng: number;
    lat: number;
  }) => Promise<{ areaList: any[]; address: string }>;

  //通过地址获取经纬度
  getLngLatByArea: (area: string) => Promise<{ lng: number; lat: number }>;

  //通过经纬度设置地图位置
  setMapLocation: ({ lng, lat }: { lng: number; lat: number }) => void;
}

let AmapInstance: any;
let geocoderInstance: any;
let poiMarker: any;

export const useAMap = (props: IUseAMapProp): IUseAMapRes => {
  const { eleId, onChang } = props;

  // 通过经纬度获取地址信息
  const getAreaByLngLat = ({
    lng,
    lat,
  }: {
    lng: number;
    lat: number;
  }): Promise<{ areaList: any[]; address: string }> => {
    return new Promise((resolve, reject) => {
      geocoderInstance.getAddress([lng, lat], (status, result) => {
        if (status === 'complete' && result.regeocode) {
          const { addressComponent, formattedAddress } = result.regeocode;
          const {
            adcode,
            province: provinceName,
            city: cityName,
            district: districtName,
            towncode,
            township: townName,
          } = addressComponent;
          const _areaList = [
            {
              areaCode: Number(`${adcode.substring(0, 2)}0000`),
              areaName: provinceName,
            },
            cityName && {
              areaCode: Number(`${adcode.substring(0, 4)}00`),
              areaName: cityName,
            },
            districtName && {
              areaCode: Number(adcode),
              areaName: districtName,
            },
            towncode && {
              areaCode: Number(towncode.substring(0, 9)),
              areaName: townName,
            },
          ].filter(Boolean);

          resolve({
            areaList: _areaList,
            address: formattedAddress,
          });
        } else {
          Message.error('经纬度查询地址失败');
          reject('获取地址失败');
        }
      });
    });
  };

  // 通过地址获取经纬度
  const getLngLatByArea = (
    area: string,
  ): Promise<{ lng: number; lat: number }> => {
    return new Promise((resolve, reject) => {
      geocoderInstance.getLocation(area, (status, result) => {
        if (status === 'complete' && result.info === 'OK') {
          const [item] = result.geocodes || [];
          resolve({
            lng: item.location.lng,
            lat: item.location.lat,
          });
        } else {
          Message.error(`未查询到地址 ${area}`);
          reject('获取经纬度失败');
        }
      });
    });
  };

  // 通过经纬度设置地图位置和标记位置
  const setMapLocation = ({ lng, lat }: { lng: number; lat: number }) => {
    // 设置marker位置
    poiMarker.setPosition([lng, lat]);
    // 设置地图中心点
    AmapInstance.setZoomAndCenter(14, [lng, lat]);
  };

  // 地图标记拖动事件
  const handleMarkerDragEvent = async (res) => {
    const { lng, lat } = res.lnglat;
    // 设置地图中心点
    AmapInstance.setZoomAndCenter(14, [lng, lat]);
    // 获取地址
    const { areaList, address } = await getAreaByLngLat({ lng, lat });
    // 触发回调
    onChang({ areaList, address, lng, lat });
  };

  // 加载高德地图脚本
  const loadAMapScript = async () => {
    // @ts-ignore
    window._AMapSecurityConfig = {
      securityJsCode: 'a054b7f5ad770dc2c3ab8d9736abd7be',
    };
    const AMap = await AMapLoader.load({
      key: '9894b4d2641a4180e60e07419f65cab8',
      version: '2.0',
      plugins: [
        'AMap.Geocoder', // 地址编码与逆地址编码( 通过经纬度获取地址,通过地址获取经纬度的插件)
      ],
    });
    //天安门经纬度
    const initLngLat = [116.397455, 39.909187];

    // 创建地图实例
    AmapInstance = new AMap.Map(eleId, {
      center: initLngLat,
      zoom: 14, // 初始地图缩放比例
    });
    // 创建地理编码实例
    geocoderInstance = new AMap.Geocoder({});

    //设置marker(地图标记)
    poiMarker = new AMap.Marker({
      position: initLngLat, //初始标记在天安门
      draggable: true, // 标记可拖动
    });

    // 监听marker拖动事件
    poiMarker?.on('dragend', handleMarkerDragEvent);
    // 添加marker
    AmapInstance.add(poiMarker);
  };

  useEffect(() => {
    //进入页面加载地图脚本
    loadAMapScript();

    return () => {
      // 离开页面销毁地图
      AmapInstance.destroy();
      poiMarker = null;
      geocoderInstance = null;
    };
  }, []);

  return {
    getAreaByLngLat,
    getLngLatByArea,
    setMapLocation,
  };
};