打造微信风格位置分享功能:Web端高德地图集成实战指南

346 阅读22分钟

在现代聊天应用中,位置分享已经成为标配功能。本文将详细介绍如何在React项目中集成高德地图API,实现类似微信的"发送位置"功能,包括位置获取、地址解析、地图展示等全流程实现。

一、项目需求与效果概览

我们的目标是在Web聊天应用中实现:

  1. 点击位置图标获取当前位置
  1. 将位置信息(经纬度)转换为可读地址
  1. 以卡片形式展示位置信息(地址、商圈、地图快照)
  1. 点击位置卡片可跳转到高德地图详情

最终效果与微信类似:聊天窗口中能够展示精美的位置卡片,包含地址、商圈信息和地图快照。

二、开发环境与依赖准备

本项目基于React + TypeScript + Material UI技术栈,使用axios进行网络请求。

2.1 高德地图开发准备

首先需要申请高德地图开发者账号并获取API Key:

  1. 访问高德开放平台注册账号
  1. 完成实名认证
  1. 创建应用并获取Key(本案例中使用的Key为xxxxx)

高德地图为个人开发者提供了丰富的免费配额:

  • 每日API调用:30,000次(足够大多数个人项目使用)

三、核心功能实现

3.1 封装高德地图API服务

首先,我们创建一个专门的服务文件来封装高德地图API请求,实现关注点分离和代码复用:

image.png

这种封装方式的优点是:

  • 集中管理API Key,便于后期更换
  • 统一处理请求参数和URL构建
  • 提高代码可读性和可维护性

3.2 位置获取与发送功能

在聊天输入组件中,我们添加一个位置按钮,点击时获取用户位置并发送位置信息:

image.png

image.png 关键技术点解析:

  1. Geolocation API:使用浏览器原生的地理位置API获取经纬度
  1. Promise封装:将回调式API转换为Promise,使代码更简洁
  1. 错误处理:完善的错误捕获和用户提示

3.3 位置消息UI组件

接下来,我们需要设计一个美观的位置消息卡片,展示在聊天界面中:

image.png

image.png 设计要点:

  1. 卡片式布局:采用Material UI的Box和Typography组件构建卡片
  1. 多层级信息:地址+商圈+地图快照的三层结构
  1. 交互设计:添加hover效果,点击跳转到地图
  1. 静态地图:使用高德静态地图API生成地图快照

3.4 消息数据结构设计

为了支持位置消息,我们需要扩展现有的消息数据结构:

image.png 这样设计的好处是:

  • 保持与现有消息类型的兼容性
  • 灵活支持多种消息类型(文本、图片、位置等)
  • 为未来扩展其他消息类型预留空间

四、关键技术点详解

4.1 高德地图逆地理编码API详解

逆地理编码API是本功能的核心,它返回的数据结构如下:

image.png 关键数据提取:

  • formatted_address:完整的地址字符串
  • addressComponent.businessAreas:商圈信息数组
  • addressComponent.city/province/district:城市/省份/区县信息

4.2 浏览器Geolocation API使用技巧

浏览器的地理位置API有几个需要注意的点:

  1. 权限请求:首次使用时浏览器会向用户请求权限
  1. HTTPS限制:现代浏览器要求Geolocation API只能在HTTPS环境使用
  1. 精度控制:可以通过参数调整定位精度

image.png

4.3 高德地图URI API跳转

当用户点击位置卡片时,我们使用高德地图的URI API进行跳转:

image.png 这个URI可以在多种环境下使用:

  • 移动端自动打开高德地图App(如已安装)
  • PC端打开高德地图网页版
  • 提供了丰富的参数支持不同场景

五、用户体验优化

5.1 加载状态处理

位置获取是异步操作,添加加载状态提升用户体验:

image.png ⌘K to generate a command

Load older messages

直接帮我把代码运用到项目中

Restore checkpoint

好的,我来帮你把代码实际应用到项目中:

1. 首先创建高德地图服务封装文件

amap.ts+25-1

 

 

import axios from 'axios';

const AMAP_KEY = 'e924a4f20bcb4cf72015d066ab5b1c5e'; // 你的高德key

const amapAxios = axios.create({

  baseURL: 'restapi.amap.com/v3',

  timeout: 10000,

});

// 逆地理编码

export async function getRegeo({ longitude, latitude }: { longitude: number; latitude: number }) {

  const res = await amapAxios.get('/geocode/regeo', {

    params: {

      key: AMAP_KEY,

      location: ${longitude},${latitude},

      extensions: 'base',

    },

  });

  return res.data;

}

// 静态地图

export function getStaticMapUrl(longitude: number, latitude: number) {

  return https://restapi.amap.com/v3/staticmap?location=${longitude},${latitude}&zoom=17&size=260*120&markers=mid,,A:${longitude},${latitude}&key=${AMAP_KEY};

2. 修改 ChatInput.tsx 使用封装的高德服务

ChatInput.tsx+6-7

import { useState, useRef } from 'react';

import { EmojiPickerComponent } from './EmojiPicker';

import { ImageUpload } from './ImageUpload';

interface ChatInputProps {

  onSend: (content: string, image?: File, location?: { latitude: number; longitude: number; address: string }) => void;

  disabled?: boolean;

  quotedMessage?: string | null;

  onClearQuote?: () => void;

        navigator.geolocation.getCurrentPosition(resolve, reject);

      });

      // 使用高德地图 API 进行地理编码

      const { latitude, longitude } = position.coords;

      const response = await fetch(

        https://restapi.amap.com/v3/geocode/regeo?key=e924a4f20bcb4cf72015d066ab5b1c5e&location=${longitude},${latitude}&extensions=base

      );

      const data = await response.json();

      if (data.status === '1' && data.regeocode) {

        const comp = data.regeocode.addressComponent;

        // businessAreas 拼接

        onSend(displayContent, undefined, {

          latitude,

          longitude,

          address: formatted_address

        });

      } else {

        throw new Error('获取地址信息失败');

  onSend: (content: string, image?: File, location?: { latitude: number; longitude: number; address: string }) => void;

      // 使用高德地图 API 进行地理编码

      const response = await fetch(

        https://restapi.amap.com/v3/geocode/regeo?key=e924a4f20bcb4cf72015d066ab5b1c5e&location=${longitude},${latitude}&extensions=base

      );

      const data = await response.json();

          address: formatted_address

import { useState, useRef } from 'react';

import { EmojiPickerComponent } from './EmojiPicker';

import { ImageUpload } from './ImageUpload';

import { getRegeo } from '../services/amap';

interface ChatInputProps {

  onSend: (content: string, image?: File, location?: { latitude: number; longitude: number; address: string; businessAreas?: string }) => void;

  disabled?: boolean;

  quotedMessage?: string | null;

  onClearQuote?: () => void;

        navigator.geolocation.getCurrentPosition(resolve, reject);

      });

      const { latitude, longitude } = position.coords;

      const data = await getRegeo({ longitude, latitude });

      

      if (data.status === '1' && data.regeocode) {

        const comp = data.regeocode.addressComponent;

        // businessAreas 拼接

        onSend(displayContent, undefined, {

          latitude,

          longitude,

          address: formatted_address,

          businessAreas: business

        });

      } else {

        throw new Error('获取地址信息失败');

53 个隐藏的行

10 个隐藏的行

3. 修改 Message.tsx 完善显示效果:

Message.tsx+7-3

import { Face as UserIcon, SmartToy as BotIcon, LocationOn } from '@mui/icons-material';

import { useState } from 'react';

import { ImagePreview } from './ImagePreview';

interface MessageProps {

  content: string;

              maxWidth: 260,

              minWidth: 200,

              boxShadow: 1,

              '&:hover': { boxShadow: 3 }

            }}

            onClick={handleLocationClick}

          >

            {/* 地图快照 */}

            <Box

              component="img"

              src={https://restapi.amap.com/v3/staticmap?location=${location.longitude},${location.latitude}&zoom=17&size=260*120&markers=mid,,A:${location.longitude},${location.latitude}&key=你的key}

              alt="地图"

              sx={{ width: '100%', borderRadius: 1 }}

            />

          

        )}

        <Typography sx={{ wordBreak: 'break-word' }}>{content}

        <Typography

          variant="caption"

          sx={{

              '&:hover': { boxShadow: 3 }

              src={https://restapi.amap.com/v3/staticmap?location=${location.longitude},${location.latitude}&zoom=17&size=260*120&markers=mid,,A:${location.longitude},${location.latitude}&key=你的key}

        <Typography sx={{ wordBreak: 'break-word' }}>{content}

import { Face as UserIcon, SmartToy as BotIcon, LocationOn } from '@mui/icons-material';

import { useState } from 'react';

import { ImagePreview } from './ImagePreview';

import { getStaticMapUrl } from '../services/amap';

interface MessageProps {

  content: string;

              maxWidth: 260,

              minWidth: 200,

              boxShadow: 1,

              '&:hover': { boxShadow: 3 },

              color: 'black'

            }}

            onClick={handleLocationClick}

          >

            {/* 地图快照 */}

            <Box

              component="img"

              src={getStaticMapUrl(location.longitude, location.latitude)}

              alt="地图"

              sx={{ width: '100%', borderRadius: 1 }}

            />

          

        )}

        {!location && (

          <Typography sx={{ wordBreak: 'break-word' }}>{content}

        )}

        <Typography

          variant="caption"

          sx={{

131 个隐藏的行

10 个隐藏的行

4. 更新 ChatPage.tsx 中的类型定义

ChatPage.tsx+1

    latitude: number;

    longitude: number;

    address: string;

  };

}

    latitude: number;

    longitude: number;

    address: string;

    businessAreas?: string;

  };

}

完成!

现在,你的代码已经优化完毕,特性包括:

  1. 封装了高德地图服务:
  • getRegeo 方法用于获取地理位置信息
  • getStaticMapUrl 方法用于获取静态地图URL
  1. 改进了位置消息展示:
  • 显示详细地址
  • 显示商圈信息
  • 显示地图快照
  • 点击可跳转到高德地图
  1. 优化了代码结构:
  • 网络请求与业务逻辑分离
  • 统一管理了API密钥
  • 样式更加美观

使用指南:

  1. 点击位置图标发送位置
  1. 聊天记录中会显示地址、商圈和地图卡片
  1. 点击卡片可在高德地图中查看位置

现在你可以测试发送位置功能,界面应该和你预期的一样了。如有其他需求请告知!

帮我写一篇技术文章,如何实现在 web 端接入高德地图的操作,字数不限,附上详情的代码解释

Web 端接入高德地图完全指南

在现代 Web 应用中,地图功能已成为提升用户体验的重要组成部分。无论是显示位置信息、导航路线,还是地理位置分享,高德地图都提供了丰富的 API 支持。本文将详细介绍如何在 Web 应用中接入高德地图,从基础配置到实现聊天应用中的位置分享功能。

目录

  1. 高德地图开发准备工作
  1. 基础地图接入
  1. 封装高德地图 API 请求
  1. 实现位置获取与分享功能
  1. 位置消息的展示优化
  1. 高德地图进阶功能
  1. 性能优化与最佳实践

一、高德地图开发准备工作

1.1 申请开发者账号与 Key

在使用高德地图 API 前,需要先申请开发者账号并创建应用获取 Key:

  1. 访问高德开放平台注册账号
  1. 创建应用并选择"Web端(JS API)"类型
  1. 完成实名认证(个人开发者也需要)
  1. 获取 Key(应用管理中查看)

1.2 配额与计费说明

高德地图 API 对个人开发者提供了慷慨的免费配额:

  • Web API 服务:每天30,000次调用
  • 静态地图 API:每天100,000次调用

超出配额后需按量付费,但对于大多数个人项目和中小型应用完全够用。

1.3 API 密钥安全性

API 密钥应妥善保管,建议的做法:

typescript

Apply to ChatPage.tsx

// 前端项目中使用环境变量

const AMAP_KEY = process.env.REACT_APP_AMAP_KEY;

// 更安全的做法是通过后端代理请求

// 前端代码不直接暴露 Key

二、基础地图接入

2.1 引入高德地图 JS SDK

最简单的方式是在 HTML 中引入:

html

Apply to ChatPage.tsx

<script type="text/javascript">

  window._AMapSecurityConfig = {

    securityJsCode: '您的安全密钥',

  }

<script type="text/javascript" src="webapi.amap.com/maps?v=2.0&…

在 React 项目中,可以这样动态加载:

typescript

Apply to ChatPage.tsx

// 动态加载高德地图脚本

export function loadAMap(key: string): Promise {

  return new Promise((resolve, reject) => {

    if (window.AMap) {

      resolve();

      return;

    }

    

    const script = document.createElement('script');

    script.type = 'text/javascript';

    script.async = true;

    script.src = https://webapi.amap.com/maps?v=2.0&key=${key}&callback=initAMap;

    script.onerror = reject;

    

    window.initAMap = () => {

      resolve();

    };

    

    document.head.appendChild(script);

  });

}

2.2 创建基础地图组件

以 React 为例,创建一个简单的地图组件:

tsx

Apply to ChatPage.tsx

import React, { useEffect, useRef } from 'react';

import { loadAMap } from '../utils/mapLoader';

const AMAP_KEY = 'e924a4f20bcb4cf72015d066ab5b1c5e';

interface MapProps {

  center?: [number, number];

  zoom?: number;

  height?: string | number;

}

const MapComponent: React.FC = ({ 

  center = [116.397428, 39.90923], 

  zoom = 13,

  height = '400px'

}) => {

  const mapRef = useRef(null);

  const mapInstance = useRef(null);

  

  useEffect(() => {

    let isMounted = true;

    

    const initMap = async () => {

      try {

        await loadAMap(AMAP_KEY);

        

        if (isMounted && mapRef.current && window.AMap) {

          mapInstance.current = new window.AMap.Map(mapRef.current, {

            center,

            zoom,

            resizeEnable: true

          });

        }

      } catch (error) {

        console.error('地图加载失败:', error);

      }

    };

    

    initMap();

    

    return () => {

      isMounted = false;

      if (mapInstance.current) {

        mapInstance.current.destroy();

      }

    };

  }, []);

  

  return <div ref={mapRef} style={{ height, width: '100%' }}>;

};

export default MapComponent;

三、封装高德地图 API 请求

为了提高代码复用性和可维护性,建议将高德地图 API 请求单独封装:

typescript

Apply to ChatPage.tsx

// src/services/amap.ts

import axios from 'axios';

const AMAP_KEY = 'e924a4f20bcb4cf72015d066ab5b1c5e';

const amapAxios = axios.create({

  baseURL: 'restapi.amap.com/v3',

  timeout: 10000,

});

// 逆地理编码

export async function getRegeo({ longitude, latitude }: { longitude: number; latitude: number }) {

  const res = await amapAxios.get('/geocode/regeo', {

    params: {

      key: AMAP_KEY,

      location: ${longitude},${latitude},

      extensions: 'base',

    },

  });

  return res.data;

}

// 静态地图

export function getStaticMapUrl(longitude: number, latitude: number) {

  return https://restapi.amap.com/v3/staticmap?location=${longitude},${latitude}&zoom=17&size=260*120&markers=mid,,A:${longitude},${latitude}&key=${AMAP_KEY};

}

// 地点搜索

export async function searchPOI(keywords: string, city?: string) {

  const res = await amapAxios.get('/place/text', {

    params: {

      key: AMAP_KEY,

      keywords,

      city: city || '',

      offset: 20,

      page: 1,

      extensions: 'all'

    }

  });

  return res.data;

}

// 行驶路线规划

export async function getRoute(origin: string, destination: string, type = 'driving') {

  const res = await amapAxios.get(/direction/${type}, {

    params: {

      key: AMAP_KEY,

      origin,

      destination,

      extensions: 'base'

    }

  });

  return res.data;

}

这样封装的好处是:

  1. 集中管理 API Key
  1. 统一处理请求参数和响应
  1. 便于后续扩展和维护
  1. 业务代码更加简洁

四、实现位置获取与分享功能

在聊天应用中实现位置分享,需要几个关键步骤:

4.1 获取用户当前位置

使用浏览器的 Geolocation API:

typescript

Apply to ChatPage.tsx

const handleLocationClick = async () => {

  if (!navigator.geolocation) {

    alert('您的浏览器不支持地理位置功能');

    return;

  }

  try {

    const position = await new Promise((resolve, reject) => {

      navigator.geolocation.getCurrentPosition(resolve, reject, {

        enableHighAccuracy: true, // 高精度定位

        timeout: 5000,           // 超时时间

        maximumAge: 0            // 不使用缓存

      });

    });

    const { latitude, longitude } = position.coords;

    // 后续处理...

  } catch (error) {

    alert('获取位置信息失败,请检查位置权限设置');

  }

};

4.2 逆地理编码获取地址

获取到经纬度后,需要转换为用户可读的地址:

typescript

Apply to ChatPage.tsx

const { latitude, longitude } = position.coords;

const data = await getRegeo({ longitude, latitude });

  

if (data.status === '1' && data.regeocode) {

  const comp = data.regeocode.addressComponent;

  // businessAreas 拼接

  let business = '';

  if (comp.businessAreas && comp.businessAreas.length > 0) {

    business = comp.businessAreas.map((b: any) => b.name).filter(Boolean).join('、');

  }

  // 展示内容

  const formatted_address = data.regeocode.formatted_address;

  let displayContent = formatted_address;

  if (business) {

    displayContent += (商圈:${business});

  }

  

  // 发送位置信息

  onSend(displayContent, undefined, {

    latitude,

    longitude,

    address: formatted_address,

    businessAreas: business

  });

}

4.3 完整的位置发送功能

在聊天输入组件中的实现:

tsx

Apply to ChatPage.tsx

// src/components/ChatInput.tsx

import { LocationOn } from '@mui/icons-material';

import { getRegeo } from '../services/amap';

// 组件内部...

const handleLocationClick = async () => {

  if (!navigator.geolocation) {

    alert('您的浏览器不支持地理位置功能');

    return;

  }

  try {

    const position = await new Promise((resolve, reject) => {

      navigator.geolocation.getCurrentPosition(resolve, reject);

    });

    const { latitude, longitude } = position.coords;

    const data = await getRegeo({ longitude, latitude });

    

    if (data.status === '1' && data.regeocode) {

      const comp = data.regeocode.addressComponent;

      // businessAreas 拼接

      let business = '';

      if (comp.businessAreas && comp.businessAreas.length > 0) {

        business = comp.businessAreas.map((b: any) => b.name).filter(Boolean).join('、');

      }

      // 展示内容

      const formatted_address = data.regeocode.formatted_address;

      let displayContent = formatted_address;

      if (business) {

        displayContent += (商圈:${business});

      }

      onSend(displayContent, undefined, {

        latitude,

        longitude,

        address: formatted_address,

        businessAreas: business

      });

    } else {

      throw new Error('获取地址信息失败');

    }

  } catch (error) {

    alert('获取位置信息失败,请检查位置权限设置');

  }

};

// 在渲染部分添加位置按钮

<IconButton 

  color="primary" 

  onClick={handleLocationClick}

  disabled={disabled}

  <LocationOn />

五、位置消息的展示优化

5.1 位置消息卡片设计

对于位置信息,可以设计精美的卡片式展示:

tsx

Apply to ChatPage.tsx

// src/components/Message.tsx

import { getStaticMapUrl } from '../services/amap';

// 位置消息类型

interface LocationData {

  latitude: number;

  longitude: number;

  address: string;

  businessAreas?: string;

}

// 在消息组件中渲染位置信息

{location && (

  <Box 

    sx={{ 

      mb: 1,

      p: 1,

      border: '1px solid #eee',

      borderRadius: 2,

      background: '#f8f8f8',

      cursor: 'pointer',

      maxWidth: 260,

      minWidth: 200,

      boxShadow: 1,

      '&:hover': { boxShadow: 3 },

      color: 'black'

    }}

    onClick={handleLocationClick}

  >

    {/* 详细地址 */}

    <Typography variant="subtitle1" sx={{ fontWeight: 700, fontSize: 16 }}>

      {location.address}

    

    {/* 商圈 */}

    {location.businessAreas && (

      <Typography variant="body2" sx={{ color: 'text.secondary', mb: 1 }}>

        商圈:{location.businessAreas}

      

    )}

    {/* 地图快照 */}

    <Box

      component="img"

      src={getStaticMapUrl(location.longitude, location.latitude)}

      alt="地图"

      sx={{ width: '100%', borderRadius: 1 }}

    />

  

)}

5.2 点击跳转到详细地图

typescript

Apply to ChatPage.tsx

const handleLocationClick = () => {

  if (location) {

    window.open(https://uri.amap.com/marker?position=${location.longitude},${location.latitude}&name=${encodeURIComponent(location.address)}, '_blank');

  }

};

这里使用了高德地图的 URI API,可以直接在地图中打开特定位置。更多参数可查看高德地图 URI API 文档。

六、高德地图进阶功能

除了基础的位置获取和分享,高德地图还提供了多种进阶功能:

6.1 POI(兴趣点)搜索

可以实现地点搜索功能:

typescript

Apply to ChatPage.tsx

export async function searchPOI(keywords: string, city?: string) {

  const res = await amapAxios.get('/place/text', {

    params: {

      key: AMAP_KEY,

      keywords,

      city: city || '',

      offset: 20,

      page: 1,

      extensions: 'all'

    }

  });

  return res.data;

}

// 使用示例

const searchResult = await searchPOI('咖啡厅', '北京市');

const places = searchResult.pois.map(poi => ({

  name: poi.name,

  address: poi.address,

  location: poi.location

}));

6.2 路线规划

提供导航路线:

typescript

Apply to ChatPage.tsx

// 获取驾车路线

const routeData = await getRoute('116.397428,39.90923', '116.481499,39.990475');

const route = routeData.route;

const distance = route.paths[0].distance; // 单位:米

const duration = route.paths[0].duration; // 单位:秒

6.3 地理围栏

监测用户是否进入特定区域:

typescript

Apply to ChatPage.tsx

// 在地图组件中设置围栏

const createGeoFence = (map) => {

  const polygon = new window.AMap.Polygon({

    path: [

      [116.39, 39.91],

      [116.40, 39.91],

      [116.40, 39.92],

      [116.39, 39.92]

    ],

    strokeColor: '#FF33FF',

    strokeWeight: 2,

    strokeOpacity: 0.2,

    fillOpacity: 0.4,

    fillColor: '#1791fc',

    zIndex: 50,

  });

  

  map.add(polygon);

  

  // 判断点是否在围栏内

  const isPointInPolygon = (point) => {

    return window.AMap.GeometryUtil.isPointInRing(point, polygon.getPath());

  };

  

  return { polygon, isPointInPolygon };

};

七、性能优化与最佳实践

7.1 按需加载

不要一次性加载所有高德地图插件,按需加载可以显著提升性能:

typescript

Apply to ChatPage.tsx

// 按需加载插件

AMap.plugin(['AMap.ToolBar', 'AMap.Scale'], function(){

  map.addControl(new AMap.ToolBar());

  map.addControl(new AMap.Scale());

});

7.2 静态图片缓存

对于静态地图,可以实现缓存机制:

typescript

Apply to ChatPage.tsx

const staticMapCache = new Map();

export function getStaticMapUrl(longitude: number, latitude: number) {

  const key = ${longitude},${latitude};

  if (staticMapCache.has(key)) {

    return staticMapCache.get(key);

  }

  

  const url = https://restapi.amap.com/v3/staticmap?location=${longitude},${latitude}&zoom=17&size=260*120&markers=mid,,A:${longitude},${latitude}&key=${AMAP_KEY};

  staticMapCache.set(key, url);

  return url;

}

7.3 错误处理与重试机制

typescript

Apply to ChatPage.tsx

export async function getRegeoWithRetry({ longitude, latitude }: { longitude: number; latitude: number }, retries = 3) {

  let lastError;

  

  for (let i = 0; i < retries; i++) {

    try {

      const res = await amapAxios.get('/geocode/regeo', {

        params: {

          key: AMAP_KEY,

          location: ${longitude},${latitude},

          extensions: 'base',

        },

      });

      

      if (res.data.status === '1') {

        return res.data;

      }

      

      throw new Error(API错误: ${res.data.info});

    } catch (error) {

      lastError = error;

      await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));

    }

  }

  

  throw lastError;

}

7.4 安全性考虑

对于敏感操作,建议通过后端代理请求高德地图 API:

typescript

Apply to ChatPage.tsx

// 前端代码

async function getLocationInfo(longitude, latitude) {

  const res = await fetch('/api/location', {

    method: 'POST',

    headers: { 'Content-Type': 'application/json' },

    body: JSON.stringify({ longitude, latitude })

  });

  return res.json();

}

// 后端代码 (Node.js)

app.post('/api/location', async (req, res) => {

  const { longitude, latitude } = req.body;

  

  try {

    const response = await axios.get('restapi.amap.com/v3/geocode/…

      params: {

        key: process.env.AMAP_KEY, // 服务器端存储 Key

        location: ${longitude},${latitude},

        extensions: 'base'

      }

    });

    

    res.json(response.data);

  } catch (error) {

    res.status(500).json({ error: '获取位置信息失败' });

  }

});

总结

高德地图提供了丰富的 API,能满足 Web 应用中各种地图相关需求。通过合理封装和使用,可以轻松实现位置分享、地点搜索、路线规划等功能。本文详细介绍了从基础接入到高级应用的完整流程,希望能帮助开发者在自己的应用中高效集成高德地图功能。在实际开发中,建议阅读高德开放平台官方文档,了解更多功能和最新特性。同时,注意控制 API 调用频率,避免超出免费配额而产生不必要的费用。


通过本文的介绍,相信你已经掌握了在 Web 应用中接入高德地图的主要方法和技巧。无论是简单的位置显示,还是复杂的地图交互功能,都可以基于这些基础知识进行扩展和定制,打造出符合用户需求的优质地图体验。

基于这个项目的实现写这篇文章,重新写一篇

打造微信风格位置分享功能:Web端高德地图集成实战指南

在现代聊天应用中,位置分享已经成为标配功能。本文将详细介绍如何在React项目中集成高德地图API,实现类似微信的"发送位置"功能,包括位置获取、地址解析、地图展示等全流程实现。

一、项目需求与效果概览

我们的目标是在Web聊天应用中实现:

  1. 点击位置图标获取当前位置
  1. 将位置信息(经纬度)转换为可读地址
  1. 以卡片形式展示位置信息(地址、商圈、地图快照)
  1. 点击位置卡片可跳转到高德地图详情

最终效果与微信类似:聊天窗口中能够展示精美的位置卡片,包含地址、商圈信息和地图快照。

二、开发环境与依赖准备

本项目基于React + TypeScript + Material UI技术栈,使用axios进行网络请求。

2.1 高德地图开发准备

首先需要申请高德地图开发者账号并获取API Key:

  1. 访问高德开放平台注册账号
  1. 完成实名认证
  1. 创建应用并获取Key(本案例中使用的Key为e924a4f20bcb4cf72015d066ab5b1c5e)

高德地图为个人开发者提供了丰富的免费配额:

  • 每日API调用:30,000次(足够大多数个人项目使用)

三、核心功能实现

3.1 封装高德地图API服务

首先,我们创建一个专门的服务文件来封装高德地图API请求,实现关注点分离和代码复用:

typescript

Apply to ChatPage.tsx

// src/services/amap.ts

import axios from 'axios';

const AMAP_KEY = 'e924a4f20bcb4cf72015d066ab5b1c5e'; // API Key

const amapAxios = axios.create({

  baseURL: 'restapi.amap.com/v3',

  timeout: 10000,

});

// 逆地理编码:将经纬度转换为详细地址

export async function getRegeo({ longitude, latitude }: { longitude: number; latitude: number }) {

  const res = await amapAxios.get('/geocode/regeo', {

    params: {

      key: AMAP_KEY,

      location: ${longitude},${latitude},

      extensions: 'base',

    },

  });

  return res.data;

}

// 获取静态地图URL

export function getStaticMapUrl(longitude: number, latitude: number) {

  return https://restapi.amap.com/v3/staticmap?location=${longitude},${latitude}&zoom=17&size=260*120&markers=mid,,A:${longitude},${latitude}&key=${AMAP_KEY};

}

这种封装方式的优点是:

  • 集中管理API Key,便于后期更换
  • 统一处理请求参数和URL构建
  • 提高代码可读性和可维护性

3.2 位置获取与发送功能

在聊天输入组件中,我们添加一个位置按钮,点击时获取用户位置并发送位置信息:

typescript

Apply to ChatPage.tsx

// 在ChatInput.tsx中添加位置发送功能

import { LocationOn } from '@mui/icons-material';

import { getRegeo } from '../services/amap';

// ...组件定义...

const handleLocationClick = async () => {

  if (!navigator.geolocation) {

    alert('您的浏览器不支持地理位置功能');

    return;

  }

  try {

    // 获取当前位置

    const position = await new Promise((resolve, reject) => {

      navigator.geolocation.getCurrentPosition(resolve, reject);

    });

    const { latitude, longitude } = position.coords;

    

    // 通过高德API获取地址信息

    const data = await getRegeo({ longitude, latitude });

    

    if (data.status === '1' && data.regeocode) {

      const comp = data.regeocode.addressComponent;

      

      // 提取商圈信息

      let business = '';

      if (comp.businessAreas && comp.businessAreas.length > 0) {

        business = comp.businessAreas.map((b: any) => b.name).filter(Boolean).join('、');

      }

      

      // 获取格式化地址

      const formatted_address = data.regeocode.formatted_address;

      let displayContent = formatted_address;

      if (business) {

        displayContent += (商圈:${business});

      }

      

      // 发送位置消息

      onSend(displayContent, undefined, {

        latitude,

        longitude,

        address: formatted_address,

        businessAreas: business

      });

    } else {

      throw new Error('获取地址信息失败');

    }

  } catch (error) {

    alert('获取位置信息失败,请检查位置权限设置');

  }

};

// 在聊天输入框组件中添加位置按钮

<IconButton 

  color="primary" 

  onClick={handleLocationClick}

  disabled={disabled}

  <LocationOn />

关键技术点解析:

  1. Geolocation API:使用浏览器原生的地理位置API获取经纬度
  1. Promise封装:将回调式API转换为Promise,使代码更简洁
  1. 错误处理:完善的错误捕获和用户提示

3.3 位置消息UI组件

接下来,我们需要设计一个美观的位置消息卡片,展示在聊天界面中:

tsx

Apply to ChatPage.tsx

// 在Message.tsx组件中添加位置消息展示

import { getStaticMapUrl } from '../services/amap';

// ...组件参数定义...

interface MessageProps {

  // ...其他属性...

  location?: {

    latitude: number;

    longitude: number;

    address: string;

    businessAreas?: string;

  };

}

// 位置消息点击处理

const handleLocationClick = () => {

  if (location) {

    window.open(https://uri.amap.com/marker?position=${location.longitude},${location.latitude}&name=${encodeURIComponent(location.address)}, '_blank');

  }

};

// 在组件渲染部分添加位置消息卡片

{location && (

  <Box 

    sx={{ 

      mb: 1,

      p: 1,

      border: '1px solid #eee',

      borderRadius: 2,

      background: '#f8f8f8',

      cursor: 'pointer',

      maxWidth: 260,

      minWidth: 200,

      boxShadow: 1,

      '&:hover': { boxShadow: 3 },

      color: 'black'

    }}

    onClick={handleLocationClick}

  >

    {/* 详细地址 */}

    <Typography variant="subtitle1" sx={{ fontWeight: 700, fontSize: 16 }}>

      {location.address}

    

    

    {/* 商圈信息 */}

    {location.businessAreas && (

      <Typography variant="body2" sx={{ color: 'text.secondary', mb: 1 }}>

        商圈:{location.businessAreas}

      

    )}

    

    {/* 地图快照 */}

    <Box

      component="img"

      src={getStaticMapUrl(location.longitude, location.latitude)}

      alt="地图"

      sx={{ width: '100%', borderRadius: 1 }}

    />

  

)}

设计要点:

  1. 卡片式布局:采用Material UI的Box和Typography组件构建卡片
  1. 多层级信息:地址+商圈+地图快照的三层结构
  1. 交互设计:添加hover效果,点击跳转到地图
  1. 静态地图:使用高德静态地图API生成地图快照

3.4 消息数据结构设计

为了支持位置消息,我们需要扩展现有的消息数据结构:

typescript

Apply to ChatPage.tsx

// 在ChatPage.tsx中定义消息接口

interface ChatMessage {

  role: 'user' | 'assistant';

  content: string;

  timestamp: Date;

  isDeleted?: boolean;

  quotedMessage?: {

    content: string;

    timestamp: Date;

  };

  image?: {

    url: string;

    filename: string;

  };

  location?: {

    latitude: number;

    longitude: number;

    address: string;

    businessAreas?: string;

  };

}

这样设计的好处是:

  • 保持与现有消息类型的兼容性
  • 灵活支持多种消息类型(文本、图片、位置等)
  • 为未来扩展其他消息类型预留空间

四、关键技术点详解

4.1 高德地图逆地理编码API详解

逆地理编码API是本功能的核心,它返回的数据结构如下:

json

Apply to ChatPage.tsx

{

  "status": "1",

  "regeocode": {

    "addressComponent": {

      "city": "深圳市",

      "province": "广东省",

      "adcode": "440305",

      "district": "南山区",

      "towncode": "440305007000",

      "streetNumber": {

        "number": "9116号",

        "location": "113.940312,22.552990",

        "direction": "东北",

        "distance": "172.361",

        "street": "北环大道"

      },

      "country": "中国",

      "township": "粤海街道",

      "businessAreas": [

        {

          "location": "113.943834,22.545057",

          "name": "科技园",

          "id": "440305"

        }

      ],

      "building": {

        "name": [],

        "type": []

      },

      "neighborhood": {

        "name": [],

        "type": []

      },

      "citycode": "0755"

    },

    "formatted_address": "广东省深圳市南山区粤海街道北环大道"

  },

  "info": "OK",

  "infocode": "10000"

}

关键数据提取:

  • formatted_address:完整的地址字符串
  • addressComponent.businessAreas:商圈信息数组
  • addressComponent.city/province/district:城市/省份/区县信息

4.2 浏览器Geolocation API使用技巧

浏览器的地理位置API有几个需要注意的点:

  1. 权限请求:首次使用时浏览器会向用户请求权限
  1. HTTPS限制:现代浏览器要求Geolocation API只能在HTTPS环境使用
  1. 精度控制:可以通过参数调整定位精度

javascript

Apply to ChatPage.tsx

navigator.geolocation.getCurrentPosition(

  successCallback,

  errorCallback,

  {

    enableHighAccuracy: true,  // 高精度模式

    timeout: 5000,             // 超时时间(ms)

    maximumAge: 0              // 缓存时间(ms),0表示不使用缓存

  }

);

4.3 高德地图URI API跳转

当用户点击位置卡片时,我们使用高德地图的URI API进行跳转:

javascript

Apply to ChatPage.tsx

window.open(https://uri.amap.com/marker?position=${longitude},${latitude}&name=${encodeURIComponent(address)}, '_blank');

这个URI可以在多种环境下使用:

  • 移动端自动打开高德地图App(如已安装)
  • PC端打开高德地图网页版
  • 提供了丰富的参数支持不同场景

五、用户体验优化

5.1 加载状态处理

位置获取是异步操作,添加加载状态提升用户体验:

typescript

Apply to ChatPage.tsx

const [isLocationLoading, setIsLocationLoading] = useState(false);

const handleLocationClick = async () => {

  setIsLocationLoading(true);

  try {

    // 位置获取逻辑...

  } catch (error) {

    // 错误处理...

  } finally {

    setIsLocationLoading(false);

  }

};

// 在位置按钮上显示加载状态

<IconButton 

  color="primary" 

  onClick={handleLocationClick}

  disabled={disabled || isLocationLoading}

  {isLocationLoading ? <CircularProgress size={24} /> : <LocationOn />}

5.2 错误处理与提示

用户友好的错误提示至关重要:

image.png ### 5.3 位置卡片样式优化

精心设计的UI细节能大幅提升用户体验:

image.png ## 六、性能与安全优化

6.1 API Key安全保护

在前端代码中直接嵌入API Key存在安全风险,更安全的做法:

image.png ### 6.2 缓存优化

对于静态地图URL,可以实现缓存机制:

typescript

image.png ⌘K to generate a command

Load older messages

直接帮我把代码运用到项目中

Restore checkpoint

好的,我来帮你把代码实际应用到项目中:

1. 首先创建高德地图服务封装文件

amap.ts+25-1

 

 

import axios from 'axios';

const AMAP_KEY = 'e924a4f20bcb4cf72015d066ab5b1c5e'; // 你的高德key

const amapAxios = axios.create({

  baseURL: 'restapi.amap.com/v3',

  timeout: 10000,

});

// 逆地理编码

export async function getRegeo({ longitude, latitude }: { longitude: number; latitude: number }) {

  const res = await amapAxios.get('/geocode/regeo', {

    params: {

      key: AMAP_KEY,

      location: ${longitude},${latitude},

      extensions: 'base',

    },

  });

  return res.data;

}

// 静态地图

export function getStaticMapUrl(longitude: number, latitude: number) {

  return https://restapi.amap.com/v3/staticmap?location=${longitude},${latitude}&zoom=17&size=260*120&markers=mid,,A:${longitude},${latitude}&key=${AMAP_KEY};

2. 修改 ChatInput.tsx 使用封装的高德服务

ChatInput.tsx+6-7

import { useState, useRef } from 'react';

import { EmojiPickerComponent } from './EmojiPicker';

import { ImageUpload } from './ImageUpload';

interface ChatInputProps {

  onSend: (content: string, image?: File, location?: { latitude: number; longitude: number; address: string }) => void;

  disabled?: boolean;

  quotedMessage?: string | null;

  onClearQuote?: () => void;

        navigator.geolocation.getCurrentPosition(resolve, reject);

      });

      // 使用高德地图 API 进行地理编码

      const { latitude, longitude } = position.coords;

      const response = await fetch(

        https://restapi.amap.com/v3/geocode/regeo?key=e924a4f20bcb4cf72015d066ab5b1c5e&location=${longitude},${latitude}&extensions=base

      );

      const data = await response.json();

      if (data.status === '1' && data.regeocode) {

        const comp = data.regeocode.addressComponent;

        // businessAreas 拼接

        onSend(displayContent, undefined, {

          latitude,

          longitude,

          address: formatted_address

        });

      } else {

        throw new Error('获取地址信息失败');

  onSend: (content: string, image?: File, location?: { latitude: number; longitude: number; address: string }) => void;

      // 使用高德地图 API 进行地理编码

      const response = await fetch(

        https://restapi.amap.com/v3/geocode/regeo?key=e924a4f20bcb4cf72015d066ab5b1c5e&location=${longitude},${latitude}&extensions=base

      );

      const data = await response.json();

          address: formatted_address

import { useState, useRef } from 'react';

import { EmojiPickerComponent } from './EmojiPicker';

import { ImageUpload } from './ImageUpload';

import { getRegeo } from '../services/amap';

interface ChatInputProps {

  onSend: (content: string, image?: File, location?: { latitude: number; longitude: number; address: string; businessAreas?: string }) => void;

  disabled?: boolean;

  quotedMessage?: string | null;

  onClearQuote?: () => void;

        navigator.geolocation.getCurrentPosition(resolve, reject);

      });

      const { latitude, longitude } = position.coords;

      const data = await getRegeo({ longitude, latitude });

      

      if (data.status === '1' && data.regeocode) {

        const comp = data.regeocode.addressComponent;

        // businessAreas 拼接

        onSend(displayContent, undefined, {

          latitude,

          longitude,

          address: formatted_address,

          businessAreas: business

        });

      } else {

        throw new Error('获取地址信息失败');

53 个隐藏的行

10 个隐藏的行

3. 修改 Message.tsx 完善显示效果:

Message.tsx+7-3

import { Face as UserIcon, SmartToy as BotIcon, LocationOn } from '@mui/icons-material';

import { useState } from 'react';

import { ImagePreview } from './ImagePreview';

interface MessageProps {

  content: string;

              maxWidth: 260,

              minWidth: 200,

              boxShadow: 1,

              '&:hover': { boxShadow: 3 }

            }}

            onClick={handleLocationClick}

          >

            {/* 地图快照 */}

            <Box

              component="img"

              src={https://restapi.amap.com/v3/staticmap?location=${location.longitude},${location.latitude}&zoom=17&size=260*120&markers=mid,,A:${location.longitude},${location.latitude}&key=你的key}

              alt="地图"

              sx={{ width: '100%', borderRadius: 1 }}

            />

          

        )}

        <Typography sx={{ wordBreak: 'break-word' }}>{content}

        <Typography

          variant="caption"

          sx={{

              '&:hover': { boxShadow: 3 }

              src={https://restapi.amap.com/v3/staticmap?location=${location.longitude},${location.latitude}&zoom=17&size=260*120&markers=mid,,A:${location.longitude},${location.latitude}&key=你的key}

        <Typography sx={{ wordBreak: 'break-word' }}>{content}

import { Face as UserIcon, SmartToy as BotIcon, LocationOn } from '@mui/icons-material';

import { useState } from 'react';

import { ImagePreview } from './ImagePreview';

import { getStaticMapUrl } from '../services/amap';

interface MessageProps {

  content: string;

              maxWidth: 260,

              minWidth: 200,

              boxShadow: 1,

              '&:hover': { boxShadow: 3 },

              color: 'black'

            }}

            onClick={handleLocationClick}

          >

            {/* 地图快照 */}

            <Box

              component="img"

              src={getStaticMapUrl(location.longitude, location.latitude)}

              alt="地图"

              sx={{ width: '100%', borderRadius: 1 }}

            />

          

        )}

        {!location && (

          <Typography sx={{ wordBreak: 'break-word' }}>{content}

        )}

        <Typography

          variant="caption"

          sx={{

131 个隐藏的行

10 个隐藏的行

4. 更新 ChatPage.tsx 中的类型定义

ChatPage.tsx+1

    latitude: number;

    longitude: number;

    address: string;

  };

}

    latitude: number;

    longitude: number;

    address: string;

    businessAreas?: string;

  };

}

完成!

现在,你的代码已经优化完毕,特性包括:

  1. 封装了高德地图服务:
  • getRegeo 方法用于获取地理位置信息
  • getStaticMapUrl 方法用于获取静态地图URL
  1. 改进了位置消息展示:
  • 显示详细地址
  • 显示商圈信息
  • 显示地图快照
  • 点击可跳转到高德地图
  1. 优化了代码结构:
  • 网络请求与业务逻辑分离
  • 统一管理了API密钥
  • 样式更加美观

使用指南:

  1. 点击位置图标发送位置
  1. 聊天记录中会显示地址、商圈和地图卡片
  1. 点击卡片可在高德地图中查看位置

现在你可以测试发送位置功能,界面应该和你预期的一样了。如有其他需求请告知!

帮我写一篇技术文章,如何实现在 web 端接入高德地图的操作,字数不限,附上详情的代码解释

Web 端接入高德地图完全指南

在现代 Web 应用中,地图功能已成为提升用户体验的重要组成部分。无论是显示位置信息、导航路线,还是地理位置分享,高德地图都提供了丰富的 API 支持。本文将详细介绍如何在 Web 应用中接入高德地图,从基础配置到实现聊天应用中的位置分享功能。

目录

  1. 高德地图开发准备工作
  1. 基础地图接入
  1. 封装高德地图 API 请求
  1. 实现位置获取与分享功能
  1. 位置消息的展示优化
  1. 高德地图进阶功能
  1. 性能优化与最佳实践

一、高德地图开发准备工作

1.1 申请开发者账号与 Key

在使用高德地图 API 前,需要先申请开发者账号并创建应用获取 Key:

  1. 访问高德开放平台注册账号
  1. 创建应用并选择"Web端(JS API)"类型
  1. 完成实名认证(个人开发者也需要)
  1. 获取 Key(应用管理中查看)

1.2 配额与计费说明

高德地图 API 对个人开发者提供了慷慨的免费配额:

  • Web API 服务:每天30,000次调用
  • 静态地图 API:每天100,000次调用

超出配额后需按量付费,但对于大多数个人项目和中小型应用完全够用。

1.3 API 密钥安全性

API 密钥应妥善保管,建议的做法:

typescript

Apply to ChatPage.tsx

// 前端项目中使用环境变量

const AMAP_KEY = process.env.REACT_APP_AMAP_KEY;

// 更安全的做法是通过后端代理请求

// 前端代码不直接暴露 Key

二、基础地图接入

2.1 引入高德地图 JS SDK

最简单的方式是在 HTML 中引入:

html

Apply to ChatPage.tsx

<script type="text/javascript">

  window._AMapSecurityConfig = {

    securityJsCode: '您的安全密钥',

  }

<script type="text/javascript" src="webapi.amap.com/maps?v=2.0&…

在 React 项目中,可以这样动态加载:

typescript

Apply to ChatPage.tsx

// 动态加载高德地图脚本

export function loadAMap(key: string): Promise {

  return new Promise((resolve, reject) => {

    if (window.AMap) {

      resolve();

      return;

    }

    

    const script = document.createElement('script');

    script.type = 'text/javascript';

    script.async = true;

    script.src = https://webapi.amap.com/maps?v=2.0&key=${key}&callback=initAMap;

    script.onerror = reject;

    

    window.initAMap = () => {

      resolve();

    };

    

    document.head.appendChild(script);

  });

}

2.2 创建基础地图组件

以 React 为例,创建一个简单的地图组件:

tsx

Apply to ChatPage.tsx

import React, { useEffect, useRef } from 'react';

import { loadAMap } from '../utils/mapLoader';

const AMAP_KEY = 'e924a4f20bcb4cf72015d066ab5b1c5e';

interface MapProps {

  center?: [number, number];

  zoom?: number;

  height?: string | number;

}

const MapComponent: React.FC = ({ 

  center = [116.397428, 39.90923], 

  zoom = 13,

  height = '400px'

}) => {

  const mapRef = useRef(null);

  const mapInstance = useRef(null);

  

  useEffect(() => {

    let isMounted = true;

    

    const initMap = async () => {

      try {

        await loadAMap(AMAP_KEY);

        

        if (isMounted && mapRef.current && window.AMap) {

          mapInstance.current = new window.AMap.Map(mapRef.current, {

            center,

            zoom,

            resizeEnable: true

          });

        }

      } catch (error) {

        console.error('地图加载失败:', error);

      }

    };

    

    initMap();

    

    return () => {

      isMounted = false;

      if (mapInstance.current) {

        mapInstance.current.destroy();

      }

    };

  }, []);

  

  return <div ref={mapRef} style={{ height, width: '100%' }}>;

};

export default MapComponent;

三、封装高德地图 API 请求

为了提高代码复用性和可维护性,建议将高德地图 API 请求单独封装:

typescript

Apply to ChatPage.tsx

// src/services/amap.ts

import axios from 'axios';

const AMAP_KEY = 'e924a4f20bcb4cf72015d066ab5b1c5e';

const amapAxios = axios.create({

  baseURL: 'restapi.amap.com/v3',

  timeout: 10000,

});

// 逆地理编码

export async function getRegeo({ longitude, latitude }: { longitude: number; latitude: number }) {

  const res = await amapAxios.get('/geocode/regeo', {

    params: {

      key: AMAP_KEY,

      location: ${longitude},${latitude},

      extensions: 'base',

    },

  });

  return res.data;

}

// 静态地图

export function getStaticMapUrl(longitude: number, latitude: number) {

  return https://restapi.amap.com/v3/staticmap?location=${longitude},${latitude}&zoom=17&size=260*120&markers=mid,,A:${longitude},${latitude}&key=${AMAP_KEY};

}

// 地点搜索

export async function searchPOI(keywords: string, city?: string) {

  const res = await amapAxios.get('/place/text', {

    params: {

      key: AMAP_KEY,

      keywords,

      city: city || '',

      offset: 20,

      page: 1,

      extensions: 'all'

    }

  });

  return res.data;

}

// 行驶路线规划

export async function getRoute(origin: string, destination: string, type = 'driving') {

  const res = await amapAxios.get(/direction/${type}, {

    params: {

      key: AMAP_KEY,

      origin,

      destination,

      extensions: 'base'

    }

  });

  return res.data;

}

这样封装的好处是:

  1. 集中管理 API Key
  1. 统一处理请求参数和响应
  1. 便于后续扩展和维护
  1. 业务代码更加简洁

四、实现位置获取与分享功能

在聊天应用中实现位置分享,需要几个关键步骤:

4.1 获取用户当前位置

使用浏览器的 Geolocation API:

typescript

Apply to ChatPage.tsx

const handleLocationClick = async () => {

  if (!navigator.geolocation) {

    alert('您的浏览器不支持地理位置功能');

    return;

  }

  try {

    const position = await new Promise((resolve, reject) => {

      navigator.geolocation.getCurrentPosition(resolve, reject, {

        enableHighAccuracy: true, // 高精度定位

        timeout: 5000,           // 超时时间

        maximumAge: 0            // 不使用缓存

      });

    });

    const { latitude, longitude } = position.coords;

    // 后续处理...

  } catch (error) {

    alert('获取位置信息失败,请检查位置权限设置');

  }

};

4.2 逆地理编码获取地址

获取到经纬度后,需要转换为用户可读的地址:

typescript

Apply to ChatPage.tsx

const { latitude, longitude } = position.coords;

const data = await getRegeo({ longitude, latitude });

  

if (data.status === '1' && data.regeocode) {

  const comp = data.regeocode.addressComponent;

  // businessAreas 拼接

  let business = '';

  if (comp.businessAreas && comp.businessAreas.length > 0) {

    business = comp.businessAreas.map((b: any) => b.name).filter(Boolean).join('、');

  }

  // 展示内容

  const formatted_address = data.regeocode.formatted_address;

  let displayContent = formatted_address;

  if (business) {

    displayContent += (商圈:${business});

  }

  

  // 发送位置信息

  onSend(displayContent, undefined, {

    latitude,

    longitude,

    address: formatted_address,

    businessAreas: business

  });

}

4.3 完整的位置发送功能

在聊天输入组件中的实现:

tsx

Apply to ChatPage.tsx

// src/components/ChatInput.tsx

import { LocationOn } from '@mui/icons-material';

import { getRegeo } from '../services/amap';

// 组件内部...

const handleLocationClick = async () => {

  if (!navigator.geolocation) {

    alert('您的浏览器不支持地理位置功能');

    return;

  }

  try {

    const position = await new Promise((resolve, reject) => {

      navigator.geolocation.getCurrentPosition(resolve, reject);

    });

    const { latitude, longitude } = position.coords;

    const data = await getRegeo({ longitude, latitude });

    

    if (data.status === '1' && data.regeocode) {

      const comp = data.regeocode.addressComponent;

      // businessAreas 拼接

      let business = '';

      if (comp.businessAreas && comp.businessAreas.length > 0) {

        business = comp.businessAreas.map((b: any) => b.name).filter(Boolean).join('、');

      }

      // 展示内容

      const formatted_address = data.regeocode.formatted_address;

      let displayContent = formatted_address;

      if (business) {

        displayContent += (商圈:${business});

      }

      onSend(displayContent, undefined, {

        latitude,

        longitude,

        address: formatted_address,

        businessAreas: business

      });

    } else {

      throw new Error('获取地址信息失败');

    }

  } catch (error) {

    alert('获取位置信息失败,请检查位置权限设置');

  }

};

// 在渲染部分添加位置按钮

<IconButton 

  color="primary" 

  onClick={handleLocationClick}

  disabled={disabled}

  <LocationOn />

五、位置消息的展示优化

5.1 位置消息卡片设计

对于位置信息,可以设计精美的卡片式展示:

tsx

Apply to ChatPage.tsx

// src/components/Message.tsx

import { getStaticMapUrl } from '../services/amap';

// 位置消息类型

interface LocationData {

  latitude: number;

  longitude: number;

  address: string;

  businessAreas?: string;

}

// 在消息组件中渲染位置信息

{location && (

  <Box 

    sx={{ 

      mb: 1,

      p: 1,

      border: '1px solid #eee',

      borderRadius: 2,

      background: '#f8f8f8',

      cursor: 'pointer',

      maxWidth: 260,

      minWidth: 200,

      boxShadow: 1,

      '&:hover': { boxShadow: 3 },

      color: 'black'

    }}

    onClick={handleLocationClick}

  >

    {/* 详细地址 */}

    <Typography variant="subtitle1" sx={{ fontWeight: 700, fontSize: 16 }}>

      {location.address}

    

    {/* 商圈 */}

    {location.businessAreas && (

      <Typography variant="body2" sx={{ color: 'text.secondary', mb: 1 }}>

        商圈:{location.businessAreas}

      

    )}

    {/* 地图快照 */}

    <Box

      component="img"

      src={getStaticMapUrl(location.longitude, location.latitude)}

      alt="地图"

      sx={{ width: '100%', borderRadius: 1 }}

    />

  

)}

5.2 点击跳转到详细地图

typescript

Apply to ChatPage.tsx

const handleLocationClick = () => {

  if (location) {

    window.open(https://uri.amap.com/marker?position=${location.longitude},${location.latitude}&name=${encodeURIComponent(location.address)}, '_blank');

  }

};

这里使用了高德地图的 URI API,可以直接在地图中打开特定位置。更多参数可查看高德地图 URI API 文档。

六、高德地图进阶功能

除了基础的位置获取和分享,高德地图还提供了多种进阶功能:

6.1 POI(兴趣点)搜索

可以实现地点搜索功能:

typescript

Apply to ChatPage.tsx

export async function searchPOI(keywords: string, city?: string) {

  const res = await amapAxios.get('/place/text', {

    params: {

      key: AMAP_KEY,

      keywords,

      city: city || '',

      offset: 20,

      page: 1,

      extensions: 'all'

    }

  });

  return res.data;

}

// 使用示例

const searchResult = await searchPOI('咖啡厅', '北京市');

const places = searchResult.pois.map(poi => ({

  name: poi.name,

  address: poi.address,

  location: poi.location

}));

6.2 路线规划

提供导航路线:

typescript

Apply to ChatPage.tsx

// 获取驾车路线

const routeData = await getRoute('116.397428,39.90923', '116.481499,39.990475');

const route = routeData.route;

const distance = route.paths[0].distance; // 单位:米

const duration = route.paths[0].duration; // 单位:秒

6.3 地理围栏

监测用户是否进入特定区域:

typescript

Apply to ChatPage.tsx

// 在地图组件中设置围栏

const createGeoFence = (map) => {

  const polygon = new window.AMap.Polygon({

    path: [

      [116.39, 39.91],

      [116.40, 39.91],

      [116.40, 39.92],

      [116.39, 39.92]

    ],

    strokeColor: '#FF33FF',

    strokeWeight: 2,

    strokeOpacity: 0.2,

    fillOpacity: 0.4,

    fillColor: '#1791fc',

    zIndex: 50,

  });

  

  map.add(polygon);

  

  // 判断点是否在围栏内

  const isPointInPolygon = (point) => {

    return window.AMap.GeometryUtil.isPointInRing(point, polygon.getPath());

  };

  

  return { polygon, isPointInPolygon };

};

七、性能优化与最佳实践

7.1 按需加载

不要一次性加载所有高德地图插件,按需加载可以显著提升性能:

typescript

Apply to ChatPage.tsx

// 按需加载插件

AMap.plugin(['AMap.ToolBar', 'AMap.Scale'], function(){

  map.addControl(new AMap.ToolBar());

  map.addControl(new AMap.Scale());

});

7.2 静态图片缓存

对于静态地图,可以实现缓存机制:

typescript

Apply to ChatPage.tsx

const staticMapCache = new Map();

export function getStaticMapUrl(longitude: number, latitude: number) {

  const key = ${longitude},${latitude};

  if (staticMapCache.has(key)) {

    return staticMapCache.get(key);

  }

  

  const url = https://restapi.amap.com/v3/staticmap?location=${longitude},${latitude}&zoom=17&size=260*120&markers=mid,,A:${longitude},${latitude}&key=${AMAP_KEY};

  staticMapCache.set(key, url);

  return url;

}

7.3 错误处理与重试机制

typescript

Apply to ChatPage.tsx

export async function getRegeoWithRetry({ longitude, latitude }: { longitude: number; latitude: number }, retries = 3) {

  let lastError;

  

  for (let i = 0; i < retries; i++) {

    try {

      const res = await amapAxios.get('/geocode/regeo', {

        params: {

          key: AMAP_KEY,

          location: ${longitude},${latitude},

          extensions: 'base',

        },

      });

      

      if (res.data.status === '1') {

        return res.data;

      }

      

      throw new Error(API错误: ${res.data.info});

    } catch (error) {

      lastError = error;

      await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));

    }

  }

  

  throw lastError;

}

7.4 安全性考虑

对于敏感操作,建议通过后端代理请求高德地图 API:

typescript

Apply to ChatPage.tsx

// 前端代码

async function getLocationInfo(longitude, latitude) {

  const res = await fetch('/api/location', {

    method: 'POST',

    headers: { 'Content-Type': 'application/json' },

    body: JSON.stringify({ longitude, latitude })

  });

  return res.json();

}

// 后端代码 (Node.js)

app.post('/api/location', async (req, res) => {

  const { longitude, latitude } = req.body;

  

  try {

    const response = await axios.get('restapi.amap.com/v3/geocode/…

      params: {

        key: process.env.AMAP_KEY, // 服务器端存储 Key

        location: ${longitude},${latitude},

        extensions: 'base'

      }

    });

    

    res.json(response.data);

  } catch (error) {

    res.status(500).json({ error: '获取位置信息失败' });

  }

});

总结

高德地图提供了丰富的 API,能满足 Web 应用中各种地图相关需求。通过合理封装和使用,可以轻松实现位置分享、地点搜索、路线规划等功能。本文详细介绍了从基础接入到高级应用的完整流程,希望能帮助开发者在自己的应用中高效集成高德地图功能。在实际开发中,建议阅读高德开放平台官方文档,了解更多功能和最新特性。同时,注意控制 API 调用频率,避免超出免费配额而产生不必要的费用。


通过本文的介绍,相信你已经掌握了在 Web 应用中接入高德地图的主要方法和技巧。无论是简单的位置显示,还是复杂的地图交互功能,都可以基于这些基础知识进行扩展和定制,打造出符合用户需求的优质地图体验。

基于这个项目的实现写这篇文章,重新写一篇

打造微信风格位置分享功能:Web端高德地图集成实战指南

在现代聊天应用中,位置分享已经成为标配功能。本文将详细介绍如何在React项目中集成高德地图API,实现类似微信的"发送位置"功能,包括位置获取、地址解析、地图展示等全流程实现。

一、项目需求与效果概览

我们的目标是在Web聊天应用中实现:

  1. 点击位置图标获取当前位置
  1. 将位置信息(经纬度)转换为可读地址
  1. 以卡片形式展示位置信息(地址、商圈、地图快照)
  1. 点击位置卡片可跳转到高德地图详情

最终效果与微信类似:聊天窗口中能够展示精美的位置卡片,包含地址、商圈信息和地图快照。

二、开发环境与依赖准备

本项目基于React + TypeScript + Material UI技术栈,使用axios进行网络请求。

2.1 高德地图开发准备

首先需要申请高德地图开发者账号并获取API Key:

  1. 访问高德开放平台注册账号
  1. 完成实名认证
  1. 创建应用并获取Key(本案例中使用的Key为e924a4f20bcb4cf72015d066ab5b1c5e)

高德地图为个人开发者提供了丰富的免费配额:

  • 每日API调用:30,000次(足够大多数个人项目使用)

三、核心功能实现

3.1 封装高德地图API服务

首先,我们创建一个专门的服务文件来封装高德地图API请求,实现关注点分离和代码复用:

typescript

Apply to ChatPage.tsx

// src/services/amap.ts

import axios from 'axios';

const AMAP_KEY = 'e924a4f20bcb4cf72015d066ab5b1c5e'; // API Key

const amapAxios = axios.create({

  baseURL: 'restapi.amap.com/v3',

  timeout: 10000,

});

// 逆地理编码:将经纬度转换为详细地址

export async function getRegeo({ longitude, latitude }: { longitude: number; latitude: number }) {

  const res = await amapAxios.get('/geocode/regeo', {

    params: {

      key: AMAP_KEY,

      location: ${longitude},${latitude},

      extensions: 'base',

    },

  });

  return res.data;

}

// 获取静态地图URL

export function getStaticMapUrl(longitude: number, latitude: number) {

  return https://restapi.amap.com/v3/staticmap?location=${longitude},${latitude}&zoom=17&size=260*120&markers=mid,,A:${longitude},${latitude}&key=${AMAP_KEY};

}

这种封装方式的优点是:

  • 集中管理API Key,便于后期更换
  • 统一处理请求参数和URL构建
  • 提高代码可读性和可维护性

3.2 位置获取与发送功能

在聊天输入组件中,我们添加一个位置按钮,点击时获取用户位置并发送位置信息:

typescript

Apply to ChatPage.tsx

// 在ChatInput.tsx中添加位置发送功能

import { LocationOn } from '@mui/icons-material';

import { getRegeo } from '../services/amap';

// ...组件定义...

const handleLocationClick = async () => {

  if (!navigator.geolocation) {

    alert('您的浏览器不支持地理位置功能');

    return;

  }

  try {

    // 获取当前位置

    const position = await new Promise((resolve, reject) => {

      navigator.geolocation.getCurrentPosition(resolve, reject);

    });

    const { latitude, longitude } = position.coords;

    

    // 通过高德API获取地址信息

    const data = await getRegeo({ longitude, latitude });

    

    if (data.status === '1' && data.regeocode) {

      const comp = data.regeocode.addressComponent;

      

      // 提取商圈信息

      let business = '';

      if (comp.businessAreas && comp.businessAreas.length > 0) {

        business = comp.businessAreas.map((b: any) => b.name).filter(Boolean).join('、');

      }

      

      // 获取格式化地址

      const formatted_address = data.regeocode.formatted_address;

      let displayContent = formatted_address;

      if (business) {

        displayContent += (商圈:${business});

      }

      

      // 发送位置消息

      onSend(displayContent, undefined, {

        latitude,

        longitude,

        address: formatted_address,

        businessAreas: business

      });

    } else {

      throw new Error('获取地址信息失败');

    }

  } catch (error) {

    alert('获取位置信息失败,请检查位置权限设置');

  }

};

// 在聊天输入框组件中添加位置按钮

<IconButton 

  color="primary" 

  onClick={handleLocationClick}

  disabled={disabled}

  <LocationOn />

关键技术点解析:

  1. Geolocation API:使用浏览器原生的地理位置API获取经纬度
  1. Promise封装:将回调式API转换为Promise,使代码更简洁
  1. 错误处理:完善的错误捕获和用户提示

3.3 位置消息UI组件

接下来,我们需要设计一个美观的位置消息卡片,展示在聊天界面中:

tsx

Apply to ChatPage.tsx

// 在Message.tsx组件中添加位置消息展示

import { getStaticMapUrl } from '../services/amap';

// ...组件参数定义...

interface MessageProps {

  // ...其他属性...

  location?: {

    latitude: number;

    longitude: number;

    address: string;

    businessAreas?: string;

  };

}

// 位置消息点击处理

const handleLocationClick = () => {

  if (location) {

    window.open(https://uri.amap.com/marker?position=${location.longitude},${location.latitude}&name=${encodeURIComponent(location.address)}, '_blank');

  }

};

// 在组件渲染部分添加位置消息卡片

{location && (

  <Box 

    sx={{ 

      mb: 1,

      p: 1,

      border: '1px solid #eee',

      borderRadius: 2,

      background: '#f8f8f8',

      cursor: 'pointer',

      maxWidth: 260,

      minWidth: 200,

      boxShadow: 1,

      '&:hover': { boxShadow: 3 },

      color: 'black'

    }}

    onClick={handleLocationClick}

  >

    {/* 详细地址 */}

    <Typography variant="subtitle1" sx={{ fontWeight: 700, fontSize: 16 }}>

      {location.address}

    

    

    {/* 商圈信息 */}

    {location.businessAreas && (

      <Typography variant="body2" sx={{ color: 'text.secondary', mb: 1 }}>

        商圈:{location.businessAreas}

      

    )}

    

    {/* 地图快照 */}

    <Box

      component="img"

      src={getStaticMapUrl(location.longitude, location.latitude)}

      alt="地图"

      sx={{ width: '100%', borderRadius: 1 }}

    />

  

)}

设计要点:

  1. 卡片式布局:采用Material UI的Box和Typography组件构建卡片
  1. 多层级信息:地址+商圈+地图快照的三层结构
  1. 交互设计:添加hover效果,点击跳转到地图
  1. 静态地图:使用高德静态地图API生成地图快照

3.4 消息数据结构设计

为了支持位置消息,我们需要扩展现有的消息数据结构:

typescript

Apply to ChatPage.tsx

// 在ChatPage.tsx中定义消息接口

interface ChatMessage {

  role: 'user' | 'assistant';

  content: string;

  timestamp: Date;

  isDeleted?: boolean;

  quotedMessage?: {

    content: string;

    timestamp: Date;

  };

  image?: {

    url: string;

    filename: string;

  };

  location?: {

    latitude: number;

    longitude: number;

    address: string;

    businessAreas?: string;

  };

}

这样设计的好处是:

  • 保持与现有消息类型的兼容性
  • 灵活支持多种消息类型(文本、图片、位置等)
  • 为未来扩展其他消息类型预留空间

四、关键技术点详解

4.1 高德地图逆地理编码API详解

逆地理编码API是本功能的核心,它返回的数据结构如下:

json

Apply to ChatPage.tsx

{

  "status": "1",

  "regeocode": {

    "addressComponent": {

      "city": "深圳市",

      "province": "广东省",

      "adcode": "440305",

      "district": "南山区",

      "towncode": "440305007000",

      "streetNumber": {

        "number": "9116号",

        "location": "113.940312,22.552990",

        "direction": "东北",

        "distance": "172.361",

        "street": "北环大道"

      },

      "country": "中国",

      "township": "粤海街道",

      "businessAreas": [

        {

          "location": "113.943834,22.545057",

          "name": "科技园",

          "id": "440305"

        }

      ],

      "building": {

        "name": [],

        "type": []

      },

      "neighborhood": {

        "name": [],

        "type": []

      },

      "citycode": "0755"

    },

    "formatted_address": "广东省深圳市南山区粤海街道北环大道"

  },

  "info": "OK",

  "infocode": "10000"

}

关键数据提取:

  • formatted_address:完整的地址字符串
  • addressComponent.businessAreas:商圈信息数组
  • addressComponent.city/province/district:城市/省份/区县信息

4.2 浏览器Geolocation API使用技巧

浏览器的地理位置API有几个需要注意的点:

  1. 权限请求:首次使用时浏览器会向用户请求权限
  1. HTTPS限制:现代浏览器要求Geolocation API只能在HTTPS环境使用
  1. 精度控制:可以通过参数调整定位精度

javascript

Apply to ChatPage.tsx

navigator.geolocation.getCurrentPosition(

  successCallback,

  errorCallback,

  {

    enableHighAccuracy: true,  // 高精度模式

    timeout: 5000,             // 超时时间(ms)

    maximumAge: 0              // 缓存时间(ms),0表示不使用缓存

  }

);

4.3 高德地图URI API跳转

当用户点击位置卡片时,我们使用高德地图的URI API进行跳转:

javascript

Apply to ChatPage.tsx

window.open(https://uri.amap.com/marker?position=${longitude},${latitude}&name=${encodeURIComponent(address)}, '_blank');

这个URI可以在多种环境下使用:

  • 移动端自动打开高德地图App(如已安装)
  • PC端打开高德地图网页版
  • 提供了丰富的参数支持不同场景

五、用户体验优化

5.1 加载状态处理

位置获取是异步操作,添加加载状态提升用户体验:

typescript

Apply to ChatPage.tsx

const [isLocationLoading, setIsLocationLoading] = useState(false);

const handleLocationClick = async () => {

  setIsLocationLoading(true);

  try {

    // 位置获取逻辑...

  } catch (error) {

    // 错误处理...

  } finally {

    setIsLocationLoading(false);

  }

};

// 在位置按钮上显示加载状态

<IconButton 

  color="primary" 

  onClick={handleLocationClick}

  disabled={disabled || isLocationLoading}

  {isLocationLoading ? <CircularProgress size={24} /> : <LocationOn />}

5.2 错误处理与提示

用户友好的错误提示至关重要:

typescript

Apply to ChatPage.tsx

try {

  // 位置获取逻辑...

} catch (error) {

  // 区分不同错误类型

  if (error.code === 1) {

    alert('获取位置失败:位置权限被拒绝,请在浏览器设置中允许位置访问');

  } else if (error.code === 2) {

    alert('获取位置失败:无法获取当前位置');

  } else if (error.code === 3) {

    alert('获取位置超时,请重试');

  } else {

    alert('获取位置信息失败,请检查网络连接');

  }

}

5.3 位置卡片样式优化

精心设计的UI细节能大幅提升用户体验:

tsx

Apply to ChatPage.tsx

<Box 

  sx={{ 

    mb: 1,

    p: 1,

    border: '1px solid #eee',

    borderRadius: 2,

    background: '#f8f8f8',

    cursor: 'pointer',

    maxWidth: 260,

    minWidth: 200,

    boxShadow: '0 2px 4px rgba(0,0,0,0.1)',

    transition: 'all 0.2s ease',

    '&:hover': { 

      boxShadow: '0 4px 8px rgba(0,0,0,0.15)',

      transform: 'translateY(-2px)'

    },

    color: 'black'

  }}

  onClick={handleLocationClick}

  {/* 卡片内容... */}

六、性能与安全优化

6.1 API Key安全保护

在前端代码中直接嵌入API Key存在安全风险,更安全的做法:

typescript

Apply to ChatPage.tsx

// 使用环境变量存储Key

const AMAP_KEY = process.env.REACT_APP_AMAP_KEY;

// 更安全的方案:通过后端代理请求

async function getLocationInfo(longitude, latitude) {

  return fetch('/api/location', {

    method: 'POST',

    body: JSON.stringify({ longitude, latitude })

  }).then(res => res.json());

}

6.2 缓存优化

对于静态地图URL,可以实现缓存机制:

typescript

Apply to ChatPage.tsx

// 使用Map缓存静态地图URL

const staticMapCache = new Map();

export function getStaticMapUrl(longitude: number, latitude: number) {

  const key = ${longitude},${latitude};

  

  if (staticMapCache.has(key)) {

    return staticMapCache.get(key);

  }

  

  const url = https://restapi.amap.com/v3/staticmap?location=${longitude},${latitude}&zoom=17&size=260*120&markers=mid,,A:${longitude},${latitude}&key=${AMAP_KEY};

  

  staticMapCache.set(key, url);

  return url;

}

6.3 按需加载

对于大型项目,可以考虑对地图相关功能进行代码分割:

image.png ## 七、兼容性与适配

7.1 移动端适配

位置功能在移动端使用更为频繁,需要注意适配:

image.png ### 7.2 降级处理

对于不支持地理位置API的浏览器,提供备选方案:

image.png ## 八、项目总结与实践建议

8.1 功能扩展方向

基于当前实现,可以进一步扩展:

  1. 位置选择:允许用户从地图上选择位置而非仅使用当前位置
  1. 位置收藏:提供常用位置收藏功能
  1. 路线分享:分享导航路线而非单点位置

8.2 实践经验总结

在实际开发过程中的一些经验:

  1. API额度控制:合理使用缓存和按需加载,避免超出免费配额
  1. 权限管理:对位置权限拒绝的情况做好引导和提示
  1. 测试策略:地理位置功能需要在不同设备和网络环境下测试
  1. 降级方案:始终准备降级方案,确保核心功能可用性

结语

通过本文的介绍,我们实现了一个具有微信风格的位置分享功能,包含了从获取位置、地址解析到UI展示的完整流程。位置分享作为现代社交和聊天应用的标配功能,能够极大地提升用户体验和交互便捷性。希望这个实战案例能对你的项目开发有所启发,帮助你在自己的应用中轻松集成高德地图功能,打造出色的用户体验。