背景
部署的场景内为内网环境,不能直接访问外网,所以前端对高德地图的接口调用要通过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 | 无效的key | API密钥错误或过期 | 检查并更新正确的key |
SERVICE_NOT_AVAILABLE | 服务不可用 | 服务端故障 | 稍后重试或联系高德技术支持 |
DAILY_QUERY_OVER_LIMIT | 每日请求超限 | 超出日调用量限制 | 升级配额或控制调用频率 |
ACCESS_TOO_FREQUENT | 访问频次超限 | 请求过于频繁 | 降低请求频率(建议≤10次/秒) |
INVALID_USER_IP | IP白名单验证失败 | 请求IP不在白名单中 | 添加IP到控制台白名单 |
INVALID_USER_DOMAIN | 域名白名单验证失败 | 请求域名未备案 | 在控制台添加合法域名 |
逆地理编码特有错误
错误代码 | 含义 | 可能原因 | 解决方案 |
---|
MISSING_REQUIRED_PARAMS | 缺少必要参数 | 未传坐标参数 | 检查lngLat参数是否有效 |
ILLEGAL_REQUEST | 请求违法 | 参数格式错误 | 验证坐标格式[经度,纬度] |
UNKNOWN_ERROR | 未知错误 | 服务端异常 | 记录错误并重试 |
ENGINE_RESPONSE_ERROR | 引擎返回数据错误 | 内部服务错误 | 稍后重试 |
NO_JAVASCRIPTAPI_AUTH | JSAPI安全密钥缺失 | 未配置安全密钥 | 添加_AMapSecurityConfig |
业务逻辑错误
错误代码 | 含义 | 可能原因 | 解决方案 |
---|
NO_DATA | 无数据 | 坐标位置无对应地址 | 尝试扩大搜索半径 |
LOCATION_OUT_OF_CHINA | 中国境外坐标 | 坐标不在国内 | 确认坐标范围 |
INVALID_COORDINATE | 非法坐标 | 坐标值超出有效范围 | 验证经度(-180~180)、纬度(-90~90) |