高德地图的内网代理

背景

部署的场景内为内网环境,不能直接访问外网,所以前端对高德地图的接口调用要通过nginx转发到可以访问外网的接口去调用对应的接口;

实现

本地测试时nginx配置

worker_processes  1;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    keepalive_timeout  65;

    server {
        listen  80;
        server_name  localhost;

        # 代理Vue3前端项目
        location / {
        	# 前端项目的地址
            proxy_pass http://xxx.xxx.xxx.xx:xxxx;
	        proxy_ssl_verify off;
            
            # 处理WebSocket和热更新
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection $http_connection;
            
            # 处理前端路由(如Vue Router的history模式)
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            
            # 解决Vite开发服务器热更新问题
            proxy_redirect off;
        }

        location /map/amap-proxy/ {
            # 转发到 webapi.amap.com
            proxy_pass https://webapi.amap.com/;

            # 禁用压缩,确保 sub_filter 生效
            proxy_set_header Accept-Encoding "";
            # 替换 JS 内容中的域名引用,使其走本地代理地址
            sub_filter_types *;
            sub_filter_once off;
            sub_filter 'webapi.amap.com' '$host:$server_port/amap-webapi';
            sub_filter 'https' 'http';
            sub_filter 'c-webapi.amap.com' '$host:$server_port/amap-c-webapi';
            sub_filter 'http://webapi.amap.com' '$host:$server_port/amap-webapi';
            sub_filter 'https://webapi.amap.com' '$host:$server_port/amap-webapi';
            sub_filter 'restapi.amap.com' '$host:$server_port/amap-restapi';
            sub_filter 'https://restapi.amap.com' '$host:$server_port/amap-restapi';
            sub_filter 'https://a.amap.com' '$host:$server_port/amap-a';
            sub_filter 'vdata.amap.com' '$host:$server_port/amap-vdata';
            sub_filter '{vdata,vdata01,vdata02,vdata03,vdata04}.amap.com' '$host:$server_port/amap-{vdata,vdata01,vdata02,vdata03,vdata04}';
            sub_filter 'webst0{1,2,3,4}.is.autonavi.com' '$host:$server_port/amap-webst0{1,2,3,4}';
            sub_filter 'wprd0{1,2,3,4}.is.autonavi.com' '$host:$server_port/amap-wprd0{1,2,3,4}';

            # 添加模拟浏览器来源的头
            proxy_set_header Host webapi.amap.com;
            proxy_set_header Referer https://webapi.amap.com;
            proxy_set_header Origin https://webapi.amap.com;
            proxy_set_header User-Agent Mozilla/5.0;

            # WebSocket兼容设置(其实高德不需要,也可以保留)
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            # 防止缓存
            proxy_buffering off;

            # 关闭 SSL 验证(高德是 HTTPS)
            proxy_ssl_verify off;
        }
        
        location /amap-webapi/ {
            proxy_pass https://webapi.amap.com/;
        }
        location /amap-c-webapi/ {
            proxy_pass https://c-webapi.amap.com/;
        }
        location /amap-restapi/ {
            proxy_pass https://restapi.amap.com/;

            proxy_set_header Host restapi.amap.com;
            proxy_set_header Referer https://restapi.amap.com;
            proxy_set_header Origin https://restapi.amap.com;
            proxy_set_header User-Agent Mozilla/5.0;

            proxy_buffering off;
        }
        location /amap-a/ {
            proxy_pass https://a.amap.com/;
        }
        location /amap-vdata/ {
            proxy_pass https://vdata.amap.com/;
        }
        location /amap-vdata01/ {
            proxy_pass https://vdata01.amap.com/;
        }
        location /amap-vdata02/ {
            proxy_pass https://vdata02.amap.com/;
        }
        location /amap-vdata03/ {
            proxy_pass https://vdata03.amap.com/;
        }
        location /amap-vdata04/ {
            proxy_pass https://vdata04.amap.com/;
        }
        location /amap-webst01/ {
            proxy_pass https://webst01.is.autonavi.com/;
        }
        location /amap-webst02/ {
            proxy_pass https://webst02.is.autonavi.com/;
        }
        location /amap-webst03/ {
            proxy_pass https://webst03.is.autonavi.com/;
        }
        location /amap-webst04/ {
            proxy_pass https://webst04.is.autonavi.com/;
        }
        location /amap-wprd01/ {
            proxy_pass https://wprd01.is.autonavi.com/;
        }
        location /amap-wprd02/ {
            proxy_pass https://wprd02.is.autonavi.com/;
        }
        location /amap-wprd03/ {
            proxy_pass https://wprd03.is.autonavi.com/;
        }
        location /amap-wprd04/ {
            proxy_pass https://wprd04.is.autonavi.com/;
        }
        location /vector/ {
            proxy_pass http://vector.amap.com/;
        }
        location /lbs/ {
        proxy_pass  https://lbs.amap.com/;
        }

        # 处理静态资源缓存
        location /assets/ {
            proxy_pass http://xxx.xxx.xxx.xx:xxxx/assets/;
            expires 1d;
        }
    }
}

vue前端按需加载

let amapLoaded = false

// eslint-disable-next-line import/prefer-default-export
export function loadAMapScript(): Promise<void> {
  if (amapLoaded) return Promise.resolve()

  return new Promise((resolve, reject) => {
    // 设置安全配置
    // eslint-disable-next-line no-underscore-dangle
    ;(window as any)._AMapSecurityConfig = {
      securityJsCode: import.meta.env.VITE_AMAP_SECURITY_JS_CODE,
    }

    // 加载脚本
    const script = document.createElement('script')
    script.type = 'text/javascript'
    script.defer = true
    script.src =
      `/map/amap-proxy/maps` +
      `?v=${import.meta.env.VITE_AMAP_VERSION}` +
      `&key=${import.meta.env.VITE_AMAP_KEY}` +
      `&plugin=${import.meta.env.VITE_AMAP_PLUGINS}` +
      `&callback=___onAPILoaded`

    // 定义回调
    // eslint-disable-next-line no-underscore-dangle
    ;(window as any).___onAPILoaded = () => {
      amapLoaded = true
      resolve()
    }

    script.onerror = reject
    document.head.appendChild(script)
  })
}

高德地图相关函数 

// useMap.ts
import { createApp, h, ref } from 'vue'
import { renderToString } from '@vue/server-renderer'
import iconStart from '@/assets/images/transport_start.png'
import iconEnd from '@/assets/images/transport-end.png'
import InfoCard from '../components/hover-info-card.vue'

// 图标URL常量
const MARKER_ICON = {
  start: iconStart,
  end: iconEnd,
  middle: iconEnd,
}

// eslint-disable-next-line no-shadow
enum MarkerType {
  START = 'start',
  END = 'end',
  MIDDLE = 'middle',
}

// 声明全局AMap类型(避免TS报错)
declare global {
  interface Window {
    AMap: any
    ___onAPILoaded?: () => void
  }
}

/**
 * 高德地图初始化和轨迹绘制封装
 * 提供 initMap、drawRoute、drawPath、addMarker、clearMap 等通用接口
 *
 * @param containerId 地图容器 DOM 元素的 ID
 */
export default function useAMap(containerId: string) {
  const map = ref<any>(null)
  const amap = ref<any>()
  let currentOverlays: any[] = []
  let drivingInstance: any = null
  let currentMarker: any = null
  let pathLine: any = null
  let currentInfoWindow: any = null
  let currentMarkerPosition: [number, number] = [0, 0]

  /**
   * 初始化高德地图
   * */
  const initMap = async (
    center: [number, number] = [114.404404, 22.736404],
    zoom = 14
  ): Promise<void> => {
    return new Promise((resolve, reject) => {
      // 情况1:全局AMap已存在(SDK已加载完成)
      if (window?.AMap) {
        try {
          const AMap = new window.AMap.Map(containerId, {
            viewMode: '3D',
            center,
            zoom,
          })
          amap.value = window.AMap
          map.value = AMap
          resolve()
        } catch (error) {
          reject(error)
        }
        return
      }

      // 情况2:SDK正在加载中(监听回调)
      // eslint-disable-next-line no-underscore-dangle
      if (typeof window.___onAPILoaded === 'function') {
        // eslint-disable-next-line no-underscore-dangle
        const originalCallback = window.___onAPILoaded
        // eslint-disable-next-line no-underscore-dangle
        window.___onAPILoaded = () => {
          originalCallback()
          initMap(center, zoom).then(resolve).catch(reject)
        }
        return
      }

      // 情况3:SDK未加载(降级处理)
      reject(new Error('高德地图SDK未加载,请检查HTML入口文件'))
    })
  }

  // 封装打开弹窗方法
  const openInfoWindow = () => {
    if (currentInfoWindow && map.value) {
      currentInfoWindow.open(map.value, currentMarkerPosition)
    }
  }

  // 封装关闭弹窗方法
  const closeInfoWindow = () => {
    currentInfoWindow?.close()
  }

  /**
   * 清除当前绘制的路线、轨迹和标记
   */
  const clearMap = () => {
    if (!map.value) return
    currentOverlays.forEach((overlay) => map.value?.remove(overlay))
    if (currentMarker) map.value?.remove(currentMarker)
    currentOverlays = []
    currentMarker = null
    pathLine = null
    if (drivingInstance) {
      drivingInstance.clearResults()
      drivingInstance = null
    }
    closeInfoWindow()
  }

  /**
   * 设置地图视角以适应所有当前的轨迹/标记点
   */
  const setViewFit = () => {
    if (map.value && currentOverlays.length) {
      map.value.setFitView(currentOverlays, {
        padding: [50, 50, 50, 50],
        animate: true,
      })
    }
  }

  /**
   * 添加一个地图标记
   * @param position 经纬度 [lng, lat]
   * @param type 标记类型:start | end | middle,默认 middle
   */
  const addMarker = (
    position: [number, number],
    type: MarkerType = MarkerType.MIDDLE
  ) => {
    if (!amap.value) return null
    // 定义icon
    const iconWidth = type === MarkerType.START ? '19' : '40'
    const iconHeight = type === MarkerType.START ? '30' : '40'
    const icon = new amap.value.Icon({
      image: MARKER_ICON[type],
      size: new amap.value.Size(iconWidth, iconHeight),
      imageSize: new amap.value.Size(iconWidth, iconHeight),
    })
    // 设置标记
    const marker = new amap.value.Marker({
      position,
      icon,
      offset: new amap.value.Pixel(-20, -30),
      zIndex: 100,
    })
    map.value?.add(marker)
    currentOverlays.push(marker)
    return marker
  }

  /**
   * 路径规划并绘制路线图(自动添加起点、终点图标)
   * 最优路线
   * @param start 起点坐标 [经度, 纬度] [lng, lat]
   * @param end 终点坐标 [经度, 纬度] [lng, lat]
   */
  const drawSearchRoute = async (
    start: [number, number],
    end: [number, number]
  ): Promise<void> => {
    if (!amap.value) {
      // 自动初始化
      await initMap(start)
    }
    // 清除旧路线
    clearMap()
    const AMap = amap.value
    drivingInstance = new AMap.Driving({
      // 最快路线
      policy: AMap.DrivingPolicy.LEAST_TIME,
      // 不自动绘制到地图
      map: null,
    })

    drivingInstance.search(start, end, (status: string, result: any) => {
      if (status !== 'complete' || !result.routes?.length) {
        throw new Error(`路径规划失败: ${status}`)
      }

      const path = result.routes[0].steps.flatMap((step: any) => step.path)

      // 添加起点和终点标记
      addMarker(path[0], MarkerType.START)
      addMarker(path[path.length - 1], MarkerType.END)

      // 创建路线 polyline
      const routeLine = new AMap.Polyline({
        path,
        isOutline: true,
        outlineColor: '#ffeeee',
        borderWeight: 2,
        strokeWeight: 5,
        strokeOpacity: 0.9,
        strokeColor: '#0091ff',
        lineJoin: 'round',
      })

      map.value?.add(routeLine)
      currentOverlays.push(routeLine)

      setViewFit()
    })
  }

  /**
   * 渲染终点的卡片
   * @param data
   * @returns
   */
  const renderInfoCard = async (data: any): Promise<string> => {
    const app = createApp({
      render: () => h(InfoCard, { data }),
    })
    const html = await renderToString(app)
    return html
  }

  /**
   * 根据经纬度获取地理信息
   * 逆地理编码
   * @param lngLat [经度, 纬度] [lng, lat]
   * @returns
   */
  const getAddressByLngLat = async (
    lngLat: [number, number]
  ): Promise<string | null> => {
    if (!amap.value) return null

    return new Promise((resolve) => {
      amap.value.plugin('AMap.Geocoder', () => {
        const geocoder = new amap.value.Geocoder({
          radius: 1000,
          extensions: 'all',
        })
        geocoder.getAddress(lngLat, (status: string, result: any) => {
          if (status === 'complete' && result?.regeocode) {
            resolve(result.regeocode.formattedAddress)
          } else {
            console.warn('逆地理编码失败', result)
            resolve(null)
          }
        })
      })
    })
  }

  /**
   * 更新标记点的方法
   * @param position [经度, 纬度] [lng, lat]
   * @param info
   * @returns
   */
  const updateCurrentMarker = async (
    position: [number, number],
    info?: any
  ) => {
    if (!amap.value) return
    currentMarkerPosition = position

    if (!currentMarker) {
      currentMarker = new amap.value.Marker({
        position,
        icon: new amap.value.Icon({
          image: MARKER_ICON.end,
          imageSize: new amap.value.Size(40, 40),
        }),
        offset: new amap.value.Pixel(-10, -30),
      })
      map.value?.add(currentMarker)

      if (info) {
        info.location = await getAddressByLngLat(position)
        const contentHtml = await renderInfoCard(info)

        // 创建弹窗并保存引用
        currentInfoWindow = new amap.value.InfoWindow({
          content: contentHtml,
          offset: new amap.value.Pixel(0, -40),
        })

        // 点击标记点时打开弹窗
        currentMarker.on('click', openInfoWindow)

        // 点击地图其他区域关闭弹窗
        map.value?.on('click', closeInfoWindow)
      }
    } else {
      currentMarker.setPosition(position)
      currentMarkerPosition = position
    }
  }

  /**
   * 动态更新轨迹线
   * @param points
   * @returns
   */
  const updatePathLine = (points: [number, number][]) => {
    if (!amap.value) return
    if (!pathLine) {
      pathLine = new amap.value.Polyline({
        path: points,
        isOutline: true,
        outlineColor: '#ffeeee',
        borderWeight: 2,
        strokeWeight: 5,
        strokeOpacity: 0.9,
        strokeColor: '#0091ff',
        lineJoin: 'round',
      })
      map.value?.add(pathLine)
      currentOverlays.push(pathLine)
    } else {
      pathLine.setPath(points)
    }
  }

  /**
   * 轨迹绘制:支持多点轨迹路径渲染(自动添加起点终点图标)
   * @param points 点数组 [ [lng, lat], ... ]
   * @param end 终点(非必须)
   */
  const drawHavePath = async (
    gpsPoints: {
      point: [number, number]
      [key: string]: any
    }[],
    end?: [number, number]
  ): Promise<void> => {
    if (!gpsPoints?.length) return
    const startPoint = gpsPoints[0].point
    if (!amap.value) {
      await initMap(startPoint)
    }
    // 添加起点(轨迹第一个点就是起点)
    if (gpsPoints?.length > 1) addMarker(startPoint, MarkerType.START)
    // 添加终点
    if (end) addMarker(end, MarkerType.END)
    // 更新轨迹线
    const path: [number, number][] = gpsPoints.map((e) => e.point)
    updatePathLine(path)
    // 更新当前点位置
    await updateCurrentMarker(
      gpsPoints[gpsPoints.length - 1].point,
      gpsPoints[gpsPoints.length - 1]
    )
    openInfoWindow()
    setViewFit()
  }

  return {
    map,
    initMap,
    clearMap,
    drawSearchRoute,
    drawHavePath,
    getAddressByLngLat,
    openInfoWindow,
    closeInfoWindow,
  }
}

遇到的问题

图片的加载

尝试过的加载

// 图标URL常量
const MARKER_ICON = {
  start: 'https://webapi.amap.com/theme/v1.3/markers/n/start.png',
  end: 'https://webapi.amap.com/theme/v1.3/markers/n/end.png',
  middle:
    'https://img.icons8.com/?size=100&id=8VxSIFp8T1iU&format=png&color=000000',
}
如果是网络图片,vite打包之后页面可以渲染

const MARKER_ICON = {
  start: new URL('../assets/icons/svg/send_transport.svg', import.meta.url)
    .href,
  end: new URL(
    '../assets/icons/svg/send_transport_send_box.svg',
    import.meta.url
  ).href,
  middle: new URL(
    '../assets/icons/svg/send_transport_send_box.svg',
    import.meta.url
  ).href,
}
这样得到的地址带上了当前的组件名称,地址不对

const MARKER_ICON = {
  start: 'public/icons/send_transport_start.png',
  end: 'public/icons/send_transport_end.svg',
  // 高德地图的 API 是运行在浏览器环境中的,因此要使用public下的相对路径
  middle: 'public/icons/send_transport_end.svg',
}
这种方式在文件夹中未找到public下的icon的文件夹

逆地理编码的错误情况

常规错误代码
错误代码含义可能原因解决方案
INVALID_USER_KEY无效的keyAPI密钥错误或过期检查并更新正确的key
SERVICE_NOT_AVAILABLE服务不可用服务端故障稍后重试或联系高德技术支持
DAILY_QUERY_OVER_LIMIT每日请求超限超出日调用量限制升级配额或控制调用频率
ACCESS_TOO_FREQUENT访问频次超限请求过于频繁降低请求频率(建议≤10次/秒)
INVALID_USER_IPIP白名单验证失败请求IP不在白名单中添加IP到控制台白名单
INVALID_USER_DOMAIN域名白名单验证失败请求域名未备案在控制台添加合法域名
逆地理编码特有错误
错误代码含义可能原因解决方案
MISSING_REQUIRED_PARAMS缺少必要参数未传坐标参数检查lngLat参数是否有效
ILLEGAL_REQUEST请求违法参数格式错误验证坐标格式[经度,纬度]
UNKNOWN_ERROR未知错误服务端异常记录错误并重试
ENGINE_RESPONSE_ERROR引擎返回数据错误内部服务错误稍后重试
NO_JAVASCRIPTAPI_AUTHJSAPI安全密钥缺失未配置安全密钥添加_AMapSecurityConfig
业务逻辑错误
错误代码含义可能原因解决方案
NO_DATA无数据坐标位置无对应地址尝试扩大搜索半径
LOCATION_OUT_OF_CHINA中国境外坐标坐标不在国内确认坐标范围
INVALID_COORDINATE非法坐标坐标值超出有效范围验证经度(-180~180)、纬度(-90~90)
### 高德地图内网访问配置使用说明 为了实现内网环境下对高德地图服务的访问,通常需要通过反向代理的方式解决跨网络通信问题。以下是具体的配置方法和技术细节: #### 一、Nginx 反向代理配置 可以通过 Nginx 设置反向代理来实现内网环境下的高德地图资源加载。以下是一个典型的 Nginx 配置示例[^2]: ```nginx server { listen 80; server_name localhost; location /maps/ { proxy_pass https://webapi.amap.com/; proxy_set_header Host webapi.amap.com; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /geocoder/ { proxy_pass http://restapi.amap.com/v3/geocode/regeo; proxy_set_header Host restapi.amap.com; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } ``` 上述配置中,`location /maps/` 将请求转发至 `https://webapi.amap.com/`,而 `location /geocoder/` 则用于地理编码接口。 #### 二、JavaScript 调用调整 由于内网无法直接访问外部域名,需修改 JavaScript 文件中的 API 地址指向本地代理路径。例如,将原始脚本标签替换为如下形式[^1]: ```html <script type="text/javascript" src="/maps/maps?v=1.4.15&key=YOUR_API_KEY&plugin=AMap.MarkerClusterer,AMap.Geocoder"></script> ``` 这里的 `/maps/` 是之前设置的 Nginx 反向代理路径。 #### 三、API 密钥管理 确保使用的 API Key 已经申请并绑定到允许访问的服务 IP 或域名下。如果是在完全隔离的内网环境中部署,则可能需要联系高德地图技术支持团队获取专属解决方案。 #### 四、其他注意事项 除了基础的地图显示功能之外,还需要考虑插件支持情况以及静态资源文件下载等问题。对于某些特定需求(比如矢量瓦片图层),可以额外增加对应的代理规则或者缓存机制以提高性能和稳定性。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值