本文封装的 React 地图组件基于高德地图 JS API 2.0 开发,集成了地址搜索、经纬度定位、地图交互(点击 / 标记拖拽)、地址与经纬度互转等核心功能,同时提供了灵活的属性配置(如自定义地图高度、初始经纬度)和状态回调能力,可直接嵌入 React 项目实现基础的地图选点、地址解析需求。
一、组件开发前置准备
1. 依赖安装
组件开发依赖高德地图 API 加载器、Ant Design 组件库(用于 UI 交互)和 React 核心库,需先安装对应依赖:
# 安装高德地图API加载器
npm install @amap/amap-jsapi-loader --save
# 安装Ant Design组件库
npm install antd --save
# 确保React核心依赖已安装
npm install react react-dom --save
2. 高德地图开发者配置
使用高德地图 API 前,需在高德开放平台注册开发者账号,创建应用并获取:
key:应用唯一标识,用于 API 调用授权;securityJsCode:安全密钥,用于前端 API 调用的安全验证(2.0 版本必填)。
二、组件核心功能拆解与实现
1. 类型定义与基础配置
首先通过 TypeScript 接口定义组件属性,明确入参规范,提升代码可维护性:
// 地图组件属性接口
export interface MapComponentProps {
lng: number; // 初始经度
lat: number; // 初始纬度
address?: string; // 初始地址
onPositionChange: (lng: number, lat: number, address: string) => void; // 位置变化回调
onLoad?: () => void; // 地图加载完成回调
height?: number; // 地图容器高度
style?: React.CSSProperties; // 自定义样式
}
同时封装经纬度验证函数,确保定位数据的合法性:
// 验证经纬度有效性(范围:经度-180~180,纬度-90~90)
const validateLngLat = (lng: number, lat: number) => {
return !isNaN(lng) && !isNaN(lat) && lng >= -180 && lng <= 180 && lat >= -90 && lat <= 90;
};
2. 地图初始化逻辑
通过useEffect和AMapLoader实现地图异步加载,核心步骤包括:
- 配置安全密钥,初始化 API 加载器;
- 创建地图实例,设置 3D 视图、初始缩放级别和中心点;
- 加载地理编码、地址搜索等插件,为后续功能提供支持;
- 绑定地图控件(比例尺、工具栏)和交互事件。
核心代码如下:
const initMap = async () => {
try {
// 配置安全密钥
window._AMapSecurityConfig = {
securityJsCode: '你的安全密钥',
};
// 加载高德地图API
const AMap = await AMapLoader.load({
key: '你的API Key',
version: '2.0',
plugins: ['AMap.Scale', 'AMap.Geocoder', 'AMap.PlaceSearch', 'AMap.ToolBar'],
});
// 创建地图实例
const map = new AMap.Map(mapContainerRef.current!, {
viewMode: '3D',
zoom: 15,
center: validateLngLat(initialLng, initialLat) ? [initialLng, initialLat] : [116.397428, 39.90923], // 默认北京坐标
});
mapRef.current = map; // 存储地图实例到ref,方便后续操作
// 添加地图控件
map.addControl(new AMap.Scale());
map.addControl(new AMap.ToolBar({ position: 'RB' }));
// 创建初始标记点
createMarker(validateLngLat(initialLng, initialLat) ? [initialLng, initialLat] : [116.397428, 39.90923]);
setMapLoading(false);
onLoad?.(); // 触发地图加载完成回调
} catch (error) {
console.error('地图初始化失败:', error);
setMapError('地图加载失败,请刷新页面重试');
}
};
3. 核心交互功能实现
(1)地址搜索:地址转经纬度
通过AMap.PlaceSearch实现地址关键词搜索,将搜索结果的经纬度同步到地图和标记点:
const searchPositionByAddress = async (keyword: string) => {
if (!keyword.trim() || !mapRef.current) return;
setMapLoading(true);
try {
const AMap = await AMapLoader.load({ key: '你的API Key', plugins: ['AMap.PlaceSearch'] });
const placeSearch = new AMap.PlaceSearch({ pageSize: 1, city: '全国' });
placeSearch.search(keyword, (status: string, result: any) => {
setMapLoading(false);
if (status === 'complete' && result.info === 'OK' && result.poiList.pois.length > 0) {
const { lng, lat } = result.poiList.pois[0].location;
updateMapPosition(lng, lat); // 更新地图中心和标记点
onPositionChange(lng, lat, result.poiList.pois[0].name); // 触发位置变化回调
} else {
message.warning('未找到匹配的地址,请尝试其他关键词');
}
});
} catch (error) {
console.error('地址搜索失败:', error);
setMapLoading(false);
}
};
(2)经纬度转地址:逆地理编码
通过AMap.Geocoder实现经纬度到地址的解析,支持地图点击、标记拖拽后自动获取地址:
const searchAddressByLngLat = (lng: number, lat: number) => {
if (!validateLngLat(lng, lat) || !geocoderRef.current) return;
geocoderRef.current.getAddress([lng, lat], (status: string, result: any) => {
if (status === 'complete' && result.info === 'OK') {
const address = result.regeocode.formattedAddress;
setCurrentAddress(address);
onPositionChange(lng, lat, address); // 同步地址到父组件
}
});
};
(3)地图交互:点击 / 标记拖拽
- 地图点击:绑定
click事件,点击位置同步为新的定位点; - 标记拖拽:标记点开启
draggable,拖拽结束后更新经纬度和地址。
核心事件绑定代码:
// 地图点击事件
map.on('click', (e: any) => {
const { lng, lat } = e.lnglat;
updateMapPosition(lng, lat);
searchAddressByLngLat(lng, lat);
});
// 标记点拖拽结束事件
marker.on('dragend', (e: any) => {
const { lng, lat } = e.lnglat;
updateMapPosition(lng, lat);
searchAddressByLngLat(lng, lat);
});
4. 状态管理与 UI 交互
通过 React Hooks 管理地图加载状态、错误信息、当前地址等:
mapLoading:控制加载中动画(Ant Design Spin 组件);mapError:地图加载失败时的错误提示;statusText:实时显示操作状态(如 "地址获取成功""搜索中...");- 搜索框支持回车触发搜索,按钮加载状态与地图加载状态联动。
三、组件使用示例
在业务组件中引入封装好的地图组件,传入初始参数并监听位置变化:
import React from 'react';
import MapComponent from './MapComponent';
const App = () => {
// 监听位置变化
const handlePositionChange = (lng: number, lat: number, address: string) => {
console.log('当前位置:', { lng, lat, address });
};
return (
<div style={{ width: '800px', margin: '50px auto' }}>
<MapComponent
lng={116.397428}
lat={39.90923}
height={400}
onPositionChange={handlePositionChange}
onLoad={() => console.log('地图加载完成')}
/>
</div>
);
};
export default App;
四、组件优化与扩展建议
- 性能优化 :高德地图 API 加载和插件初始化可通过
useMemo缓存,避免重复加载;标记点创建逻辑抽离,减少 DOM 操作。 - 功能扩展:支持多标记点、范围选择、自定义标记样式;增加地址模糊搜索、历史记录功能。
- 容错处理:补充网络异常、API 调用超限的提示;经纬度解析失败时提供默认地址兜底。
- 样式定制:将地图控件位置、提示框样式抽离为 Props,支持全量自定义。
总结
- 该 React 地图组件基于高德地图 API 2.0 封装,核心实现了地址与经纬度互转、地图点击 / 标记拖拽交互、地址搜索三大核心功能,同时提供灵活的属性配置和回调能力。
- 组件开发通过 TypeScript 定义接口保证类型安全,结合 React Hooks 管理状态,借助 Ant Design 实现友好的 UI 交互,可直接复用至需要地图选点的业务场景。
- 实际使用中可基于该基础版本扩展多标记、范围选择等功能,并补充异常处理和性能优化,适配更复杂的业务需求。
源码
import React, { useState, useEffect, useRef } from 'react';
import { Input, Button, message, Spin } from 'antd';
import { SearchOutlined } from '@ant-design/icons';
import AMapLoader from '@amap/amap-jsapi-loader';
// 定义地图组件属性接口
export interface MapComponentProps {
/** 初始经度 */
lng: number;
/** 初始纬度 */
lat: number;
/** 初始地址 */
address?: string;
/** 位置变化回调函数 */
onPositionChange: (lng: number, lat: number, address: string) => void;
/** 地图加载完成回调 */
onLoad?: () => void;
/** 地图容器高度 */
height?: number;
/** 地图容器样式 */
style?: React.CSSProperties;
}
// 验证经纬度有效性
const validateLngLat = (lng: number, lat: number) => {
return (
!isNaN(lng) &&
!isNaN(lat) &&
lng >= -180 &&
lng <= 180 &&
lat >= -90 &&
lat <= 90
);
};
const MapComponent: React.FC<MapComponentProps> = ({
lng: initialLng,
lat: initialLat,
address: initialAddress = '',
onPositionChange,
onLoad,
height = 300,
style = {},
}) => {
// 状态管理
const [mapLoading, setMapLoading] = useState(true);
const [mapError, setMapError] = useState('');
const [searchKeyword, setSearchKeyword] = useState(initialAddress);
const [currentAddress, setCurrentAddress] = useState(initialAddress);
const [statusText, setStatusText] = useState('');
const [isInitialized, setIsInitialized] = useState(false);
// 地图实例引用
const mapRef = useRef<any>(null);
const markerRef = useRef<any>(null);
const geocoderRef = useRef<any>(null); // 新增:地理编码实例引用
const mapContainerRef = useRef<HTMLDivElement>(null);
// 初始化地图
useEffect(() => {
if (!isInitialized && mapContainerRef.current) {
initMap();
setIsInitialized(true);
}
}, [isInitialized]);
// 处理初始经纬度和地址更新
useEffect(() => {
if (mapRef.current) {
const validLng = typeof initialLng === 'string' ? parseFloat(initialLng) : initialLng;
const validLat = typeof initialLat === 'string' ? parseFloat(initialLat) : initialLat;
if (validateLngLat(validLng, validLat)) {
updateMapPosition(validLng, validLat);
searchAddressByLngLat(validLng, validLat);
} else {
setStatusText('经纬度无效,使用默认位置');
updateMapPosition(116.397428, 39.90923); // 默认北京坐标
}
}
}, [initialLng, initialLat, initialAddress]);
// 初始化地图
const initMap = async () => {
try {
// 配置安全密钥
window._AMapSecurityConfig = {
securityJsCode: '你的密钥',
};
const AMap = await AMapLoader.load({
key: '你的key值',
version: '2.0',
plugins: ['AMap.Scale', 'AMap.Geocoder', 'AMap.PlaceSearch', 'AMap.ToolBar'],
});
// 创建地图实例
const map = new AMap.Map(mapContainerRef.current!, {
viewMode: '3D',
zoom: 15,
center: validateLngLat(initialLng, initialLat)
? [initialLng, initialLat]
: [116.397428, 39.90923],
});
// 创建地理编码实例(用于经纬度转地址)
geocoderRef.current = new AMap.Geocoder({
radius: 1000,
extensions: "all"
});
// 存储地图实例
mapRef.current = map;
// 添加控件
map.addControl(new AMap.Scale());
map.addControl(new AMap.ToolBar({ position: 'RB' }));
// 创建标记点
createMarker(
validateLngLat(initialLng, initialLat)
? [initialLng, initialLat]
: [116.397428, 39.90923]
);
// 绑定地图点击事件
map.on('click', (e: any) => {
const { lng, lat } = e.lnglat;
updateMapPosition(lng, lat);
searchAddressByLngLat(lng, lat);
});
setMapLoading(false);
setStatusText('地图加载完成');
onLoad?.();
} catch (error) {
console.error('地图初始化失败:', error);
setMapError('地图加载失败,请刷新页面重试');
setMapLoading(false);
}
};
// 创建标记点
const createMarker = (position: [number, number]) => {
if (!mapRef.current) return;
// 移除现有标记
if (markerRef.current) {
mapRef.current.remove(markerRef.current);
}
// 创建新标记
const marker = new (window as any).AMap.Marker({
position,
draggable: true, // 允许拖拽
title: currentAddress,
});
// 绑定标记拖拽结束事件
marker.on('dragend', (e: any) => {
const { lng, lat } = e.lnglat;
updateMapPosition(lng, lat);
searchAddressByLngLat(lng, lat);
});
mapRef.current.add(marker);
markerRef.current = marker;
};
// 更新地图位置
const updateMapPosition = (lng: number, lat: number) => {
if (!mapRef.current || !validateLngLat(lng, lat)) return;
// 更新地图中心
mapRef.current.setCenter([lng, lat]);
// 更新标记位置
if (markerRef.current) {
markerRef.current.setPosition([lng, lat]);
} else {
createMarker([lng, lat]);
}
};
// 新增:根据经纬度搜索地址(补全缺失的函数)
const searchAddressByLngLat = (lng: number, lat: number) => {
if (!validateLngLat(lng, lat) || !geocoderRef.current) return;
setStatusText('获取地址中...');
geocoderRef.current.getAddress([lng, lat], (status: string, result: any) => {
if (status === 'complete' && result.info === 'OK') {
const address = result.regeocode.formattedAddress;
setCurrentAddress(address);
setStatusText('地址获取成功');
onPositionChange(lng, lat, address);
// 同步更新搜索框显示
setSearchKeyword(address);
} else {
setStatusText('无法获取地址信息');
onPositionChange(lng, lat, '');
}
});
};
// 根据地址搜索位置
const searchPositionByAddress = async (keyword: string) => {
if (!keyword.trim() || !mapRef.current) return;
setMapLoading(true);
setStatusText('搜索中...');
try {
const AMap = await AMapLoader.load({
key: 'key值',
plugins: ['AMap.PlaceSearch'],
});
const placeSearch = new AMap.PlaceSearch({
pageSize: 1,
city: '全国',
autoFitView: true,
});
placeSearch.search(keyword, (status: string, result: any) => {
setMapLoading(false);
if (status === 'complete' && result.info === 'OK' && result.poiList.pois.length > 0) {
const { location, name } = result.poiList.pois[0];
const { lng, lat } = location;
updateMapPosition(lng, lat);
setCurrentAddress(name);
setStatusText('地址搜索成功');
onPositionChange(lng, lat, name);
} else {
setStatusText('未找到匹配地址');
message.warning('未找到匹配的地址,请尝试其他关键词');
}
});
} catch (error) {
console.error('地址搜索失败:', error);
setMapLoading(false);
setStatusText('搜索失败,请重试');
}
};
// 处理搜索提交
const handleSearch = () => {
searchPositionByAddress(searchKeyword);
};
// 处理键盘回车搜索
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
handleSearch();
}
};
return (
<div style={{ width: '100%', ...style }}>
{/* 搜索栏 */}
<div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
<Input
placeholder="输入地址搜索"
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
onKeyPress={handleKeyPress}
disabled={mapLoading || !!mapError}
style={{ flex: 1 }}
/>
<Button
type="primary"
icon={<SearchOutlined />}
onClick={handleSearch}
loading={mapLoading}
disabled={!!mapError}
>
搜索
</Button>
</div>
{/* 地图容器 */}
<div
ref={mapContainerRef}
style={{
height,
border: '1px solid #e8e8e8',
borderRadius: 4,
position: 'relative',
overflow: 'hidden',
}}
>
{/* 加载状态 */}
{mapLoading && (
<div style={overlayStyle}>
<Spin size="large" tip="地图加载中..." />
</div>
)}
{/* 错误状态 */}
{mapError && (
<div style={overlayStyle}>
<p style={{ color: '#f5222d', margin: 0 }}>{mapError}</p>
</div>
)}
{/* 状态提示 */}
{statusText && (
<div style={statusBarStyle}>
{statusText}
</div>
)}
</div>
</div>
);
};
// 样式定义
const overlayStyle: React.CSSProperties = {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(255, 255, 255, 0.8)',
zIndex: 100,
};
const statusBarStyle: React.CSSProperties = {
position: 'absolute',
bottom: 10,
left: 10,
backgroundColor: 'rgba(255, 255, 255, 0.8)',
padding: '4px 8px',
borderRadius: 4,
fontSize: 12,
zIndex: 99,
};
export default MapComponent;