背景:访问高德地图时由于其组件内嵌了域名请求,客户机无外网环境时会出现底图、相关图片资源无法加载的问题。
解决方案:当部署的环境或者使用系统的用户电脑不能上网,但是有一台代理服务器可以上网,可以使用代理服务器加载高德地图。就需要将高德官方相关请求全部转发至代理服务器。
项目背景:vue2、nginx。
方法一:域名匹配替换 + Service Worker
此方法注意事项:由于浏览器安全策略,Service Worker的使用需要配置证书,有证书条件的可以使用此方案。
瓦片图相关资源请求由高德地图内部动态生成的域名,域名匹配替换一直无法成功需要配合Service Worker,通过Service Worker将瓦片图相关资源请求进行替换转发。
瓦片图相关资源请求如下(可能伴随官方升级进行变化):
采用"两层机制"完成地图域名转发:
第一层是在应用初始化阶段挂载一个代理控制钩子,负责统一管理代理配置注入(如转发目标、匹配规则、调试参数)和环境兼容;
第二层是Service Worker的请求拦截机制,在网络请求发出前识别地图相关目标地址,将其按规则重写到指定中转域并保留原始路径与参数后再继续请求。
这样业务侧仍按原有地图调用方式开发,不需要逐页面改接口地址,即可实现透明转发。其核心原理是"应用层做策略控制、网络层做请求重写",优点是改造成本低、切换灵活、可统一治理地图访问稳定性与跨域受限问题。
废话不多说,直接上源码!!!
- 域名匹配替换部分(amapProxyHook.js)
js
/**
* 将匹配到指定 host 前缀的 URL 重写到代理地址。
*
* 例如:
* https://webapi.amap.com/xxx -> {proxyHost}/webapi.amap.com/xxx
*/
function rewriteByHost(url, prefix, proxyHost) {
const httpsPrefix = `https://${prefix}`
const httpPrefix = `http://${prefix}`
const protocolRelativePrefix = `//${prefix}`
// 处理 https://host
if (url.indexOf(httpsPrefix) !== -1) {
return `${proxyHost}/${prefix}${url.split(httpsPrefix)[1]}`
}
// 处理 http://host
if (url.indexOf(httpPrefix) !== -1) {
return `${proxyHost}/${prefix}${url.split(httpPrefix)[1]}`
}
// 处理 //host(协议相对地址)
if (url.indexOf(protocolRelativePrefix) !== -1) {
return `${proxyHost}/${prefix}${url.split(protocolRelativePrefix)[1]}`
}
return null
}
/**
* 创建一个"高德 URL 重写函数"。
*
* 目的:
* 将高德地图相关静态资源和数据请求统一改写到反向代理域名,
* 从而规避直接访问高德域名时的跨域/网络策略限制。
*/
function createAmapUrlRewriter(proxyHost) {
// 按产品线区分的高德基础域名
const mapsUrl = 'https://webapi.amap.com/maps'
const webapiHttpsUrl = 'https://webapi.amap.com'
const restApiUrl = 'https://restapi.amap.com'
const jsApiUrl = 'https://jsapi.amap.com'
const o4ApiUrl = 'https://o4.amap.com'
// 需要按"原 host + path"直通代理的数据域名
const dataHosts = [
'webapi.amap.com',
'jsapi-service.amap.com'
]
/**
* 基于 URL.hostname 兜底匹配。
* 对 dataHosts 中的域名,按 {proxyHost}/{host}{pathname}{search} 形式重写。
*/
const toProxyByHostname = sourceUrl => {
try {
const urlObj = new URL(sourceUrl, window.location.origin)
const host = urlObj.host
if (dataHosts.indexOf(host) !== -1) {
return `${proxyHost}/${host}${urlObj.pathname}${urlObj.search || ''}`
}
return null
} catch (e) {
// 非法 URL 或无法解析时,不做改写
return null
}
}
return source => {
const sourceUrl = String(source)
// 按固定前缀优先改写,保持后续路径与查询参数不变
if (sourceUrl.indexOf(mapsUrl) !== -1) {
return `${proxyHost}/maps${sourceUrl.split(mapsUrl)[1]}`
}
if (sourceUrl.indexOf(webapiHttpsUrl) !== -1) {
return `${proxyHost}/webAmap${sourceUrl.split(webapiHttpsUrl)[1]}`
}
if (sourceUrl.indexOf(restApiUrl) !== -1) {
return `${proxyHost}/restAmap${sourceUrl.split(restApiUrl)[1]}`
}
if (sourceUrl.indexOf(jsApiUrl) !== -1) {
return `${proxyHost}/jsAmap${sourceUrl.split(jsApiUrl)[1]}`
}
if (sourceUrl.indexOf(o4ApiUrl) !== -1) {
return `${proxyHost}/o4Amap${sourceUrl.split(o4ApiUrl)[1]}`
}
// 再尝试 dataHosts 前缀改写
for (let i = 0; i < dataHosts.length; i++) {
const rewritten = rewriteByHost(sourceUrl, dataHosts[i], proxyHost)
if (rewritten) {
return rewritten
}
}
// 最后尝试 hostname 兜底改写
const byHostname = toProxyByHostname(sourceUrl)
if (byHostname) {
return byHostname
}
// 非高德地址或未命中规则时,保持原样
return source
}
}
/**
* 初始化高德代理 Hook。
*
* 功能:
* 1. 劫持常见 DOM 属性赋值(script/img/link/a)
* 2. 劫持 setAttribute
* 3. 劫持 XMLHttpRequest.open
* 4. 劫持 window.fetch
* 5. 注入高德安全配置(_AMapSecurityConfig)
*/
export function initAmapProxyHook(options = {}) {
// 避免重复初始化造成多次重写
if (window.__amapScriptHooked__) {
return
}
const proxyHost = options.proxyHost
const amapKey = options.key
const rewriteAmapUrl = createAmapUrlRewriter(proxyHost)
// Hook <script>.src
const scriptProperty = Object.getOwnPropertyDescriptor(HTMLScriptElement.prototype, 'src')
if (scriptProperty && scriptProperty.set) {
const nativeSet = scriptProperty.set
Object.defineProperty(HTMLScriptElement.prototype, 'src', {
configurable: true,
enumerable: scriptProperty.enumerable,
get: scriptProperty.get,
set(url) {
nativeSet.call(this, rewriteAmapUrl(url))
}
})
}
// Hook <img>.src
const imageProperty = Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, 'src')
if (imageProperty && imageProperty.set) {
const nativeImgSet = imageProperty.set
Object.defineProperty(HTMLImageElement.prototype, 'src', {
configurable: true,
enumerable: imageProperty.enumerable,
get: imageProperty.get,
set(url) {
nativeImgSet.call(this, rewriteAmapUrl(url))
}
})
}
// Hook <link>.href
const linkProperty = Object.getOwnPropertyDescriptor(HTMLLinkElement.prototype, 'href')
if (linkProperty && linkProperty.set) {
const nativeLinkSet = linkProperty.set
Object.defineProperty(HTMLLinkElement.prototype, 'href', {
configurable: true,
enumerable: linkProperty.enumerable,
get: linkProperty.get,
set(url) {
nativeLinkSet.call(this, rewriteAmapUrl(url))
}
})
}
// Hook <a>.href
const anchorProperty = Object.getOwnPropertyDescriptor(HTMLAnchorElement.prototype, 'href')
if (anchorProperty && anchorProperty.set) {
const nativeAnchorSet = anchorProperty.set
Object.defineProperty(HTMLAnchorElement.prototype, 'href', {
configurable: true,
enumerable: anchorProperty.enumerable,
get: anchorProperty.get,
set(url) {
nativeAnchorSet.call(this, rewriteAmapUrl(url))
}
})
}
// Hook setAttribute,覆盖通过属性字符串方式设置 src/href 的场景
const nativeSetAttribute = Element.prototype.setAttribute
Element.prototype.setAttribute = function(name, value) {
if ((this.tagName === 'SCRIPT' || this.tagName === 'IMG') && name === 'src') {
return nativeSetAttribute.call(this, name, rewriteAmapUrl(value))
}
if (this.tagName === 'LINK' && name === 'href') {
return nativeSetAttribute.call(this, name, rewriteAmapUrl(value))
}
if (this.tagName === 'A' && name === 'href') {
return nativeSetAttribute.call(this, name, rewriteAmapUrl(value))
}
return nativeSetAttribute.call(this, name, value)
}
// Hook XHR 请求
const nativeXhrOpen = XMLHttpRequest.prototype.open
XMLHttpRequest.prototype.open = function(method, url, async, user, password) {
const nextUrl = typeof url === 'string' ? rewriteAmapUrl(url) : url
return nativeXhrOpen.call(this, method, nextUrl, async, user, password)
}
// Hook fetch 请求
if (window.fetch) {
const nativeFetch = window.fetch
window.fetch = function(input, init) {
if (typeof input === 'string') {
return nativeFetch.call(this, rewriteAmapUrl(input), init)
}
if (input instanceof Request) {
const nextRequest = new Request(rewriteAmapUrl(input.url), input)
return nativeFetch.call(this, nextRequest, init)
}
if (input && input.url) {
return nativeFetch.call(this, rewriteAmapUrl(input.url), init)
}
return nativeFetch.call(this, input, init)
}
}
// 高德 JSAPI 安全配置:关闭 SW,指定服务地址走代理
window._AMapSecurityConfig = {
serviceWorker: false,
serviceHost: `${proxyHost}/_AMapService`
}
/**
* 自检函数:用于快速验证代理链路是否可用。
*
* 检查逻辑:
* - 请求一个高德初始化接口
* - 返回不是 HTML 且不是明显错误页,则视为通过
*/
window.__amapProxySelfCheck__ = async function() {
const checkUrl = `${proxyHost}/jsAmap/web/init?key=${amapKey}&v=2.0`
try {
const res = await window.fetch(checkUrl, { method: 'GET' })
const text = await res.text()
const isHtml = text.indexOf('<html') !== -1 || text.indexOf('<!DOCTYPE') !== -1
const maybeBlocked = text.indexOf('error') !== -1 || text.indexOf('forbidden') !== -1
const pass = res.ok && !isHtml && !maybeBlocked
console.log('[AMapProxySelfCheck]', {
pass,
status: res.status,
url: checkUrl,
contentType: res.headers.get('content-type'),
sample: text.slice(0, 180)
})
return pass
} catch (error) {
console.error('[AMapProxySelfCheck] request failed', error)
return false
}
}
// 标记已初始化
window.__amapScriptHooked__ = true
}
- Service Worker部分(amap-sw.js 置与public目录下与index.html同级)
js
/* eslint-disable no-restricted-globals */
const AMAP_PROXY_HOSTS = new Set([
'jsapi-data1.amap.com',
'jsapi-data2.amap.com',
'jsapi-data3.amap.com',
'jsapi-data4.amap.com',
'jsapi-data5.amap.com'
])
const MIN_TILE_BYTES = 200
function getProxyHost() {
const params = new URLSearchParams(self.location && self.location.search ? self.location.search : '')
return params.get('proxyHost') || ''
}
function isTileRequest(url) {
return /\/tile\/.+\/pbf\//.test(url.pathname)
}
function isLikelyBadTile(contentType, byteLength) {
const isText = contentType && contentType.toLowerCase().includes('text')
return isText || byteLength < MIN_TILE_BYTES
}
self.addEventListener('install', () => {
self.skipWaiting()
})
self.addEventListener('activate', event => {
event.waitUntil(self.clients.claim())
})
self.addEventListener('fetch', event => {
const req = event.request
const url = new URL(req.url)
if (!AMAP_PROXY_HOSTS.has(url.host)) return
const proxyHost = getProxyHost()
const proxyUrl = `${proxyHost}/proxy/${url.host}${url.pathname}${url.search}`
event.respondWith((async() => {
try {
const proxyRes = await fetch(proxyUrl)
if (!proxyRes || !proxyRes.ok) {
throw new Error(`proxy fetch failed: ${proxyRes ? proxyRes.status : 'unknown'}`)
}
if (isTileRequest(url)) {
const clone = proxyRes.clone()
const buf = await clone.arrayBuffer()
const contentType = proxyRes.headers.get('content-type') || ''
const badTile = isLikelyBadTile(contentType, buf.byteLength)
if (badTile) {
return fetch(req)
}
}
return proxyRes
} catch (e) {
return fetch(req)
}
})())
})
- main.js配置
js
import { initAmapProxyHook } from '@/utils/amapProxyHook'
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
const swUrl = `/amap-sw.js?proxyHost=${encodeURIComponent(store.getters.amapProxyHost)}`
navigator.serviceWorker
.register(swUrl)
.then(() => navigator.serviceWorker.ready)
.then(() => {
console.log('[AMap SW] ready')
})
.catch(err => {
console.error('[AMap SW] register failed', err)
})
})
}
// 全局高德代理hook:必须在任何高德SDK加载前执行
initAmapProxyHook({
proxyHost: store.getters.amapProxyHost,
key: store.getters.amapKey
})
- 代理服务器nginx配置
nginx
server {
listen 监听端口;
server_name localhost;
# DNS 解析器配置(放在 server 块内)
# 将114.114.114.114 8.8.8.8替换为你内网自己的DNS解析器
resolver 114.114.114.114 8.8.8.8 valid=300s;
resolver_timeout 10s;
# ===== 统一 CORS 配置 =====
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With" always;
# 处理 OPTIONS 预检请求
if ($request_method = OPTIONS) {
return 204;
}
# ===== 通用代理参数 =====
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_ssl_server_name on;
proxy_ssl_verify off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_connect_timeout 15s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
# ===== 1. 处理直接请求 /maps 或 /maps/ 的情况 =====
location /maps/ {
proxy_pass https://webapi.amap.com/maps/;
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 Accept-Encoding "";
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Allow-Credentials;
}
location /webAmap/ {
rewrite ^/webAmap/(.*)$ /$1 break;
proxy_pass https://webapi.amap.com;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Allow-Credentials;
}
location /jsapi-service.amap.com/ {
proxy_set_header Host jsapi-service.amap.com;
proxy_pass https://jsapi-service.amap.com/;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Allow-Credentials;
}
location /webapi.amap.com/ {
proxy_set_header Host webapi.amap.com;
proxy_pass https://webapi.amap.com/;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Allow-Credentials;
}
# ===== 2. 高德服务端 API 代理(安全密钥验证) =====
location /_AMapService/ {
rewrite ^/_AMapService/(.*)$ /$1 break;
# 关键:追加安全密钥
set $args "$args&jscode=你的高德安全密钥";
proxy_pass https://restapi.amap.com;
proxy_set_header Host restapi.amap.com;
proxy_set_header Referer https://restapi.amap.com;
proxy_set_header User-Agent "Mozilla/5.0";
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Allow-Credentials;
}
# ===== 3. JS API 脚本代理 =====
location /jsAmap/ {
rewrite ^/jsAmap/(.*)$ /$1 break;
proxy_pass https://jsapi.amap.com;
proxy_set_header Host jsapi.amap.com;
proxy_set_header Referer https://jsapi.amap.com;
proxy_set_header Origin https://jsapi.amap.com;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Allow-Credentials;
}
location /restAmap/ {
rewrite ^/restAmap/(.*)$ /$1 break;
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_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Allow-Credentials;
}
location /o4Amap/ {
rewrite ^/o4Amap/(.*)$ /$1 break;
proxy_pass https://o4.amap.com;
proxy_set_header Host o4.amap.com;
proxy_set_header Referer https://o4.amap.com;
proxy_set_header Origin https://o4.amap.com;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Allow-Credentials;
}
# 动态匹配 o4.amap.com 或 jsapi-data*.amap.com 等
location ~ ^/proxy/([^/]+)/(.*)$ {
set $target_host $1;
set $target_path $2;
proxy_pass https://$target_host/$target_path$is_args$args;
proxy_ssl_server_name on;
proxy_ssl_name $target_host;
proxy_set_header Host $target_host;
proxy_set_header Referer "https://$target_host/";
proxy_set_header Origin "https://$target_host";
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Expose-Headers;
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET, OPTIONS" always;
add_header Access-Control-Allow-Headers "*" always;
add_header Access-Control-Expose-Headers "Content-Length,Content-Range,ETag" always;
if ($request_method = OPTIONS) { return 204; }
}
access_log /data/project/dev/nginxLog/access.log;
error_log /data/project/dev/nginxLog/error.log;
}
方案二:@huangjs888/amap-proxy组件 (推荐使用)
css
npm i @huangjs888/amap-proxy
- main.js配置
js
import ap from '@huangjs888/amap-proxy'
// http://IP:端口/proxy/ 为代理服务器IP和nginx监听端口,/proxy/为自定义前缀
ap(
'http://IP:端口/proxy/',
/(https?:)?\/\/((jsapi-data[1-5]|webapi|restapi|jsapi|o4|mapplugin|jsapi-service)\.amap\.com)/g
)
- 代理服务器nginx配置
nginx
server {
listen 8068;
server_name localhost;
# DNS 解析器配置(放在 server 块内)
# 将114.114.114.114 8.8.8.8替换为你内网自己的DNS解析器
resolver 114.114.114.114 8.8.8.8 valid=300s;
resolver_timeout 10s;
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With" always;
if ($request_method = OPTIONS) {
return 204;
}
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_ssl_server_name on;
proxy_ssl_verify off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_connect_timeout 15s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
# ===== 规则1: webapi.amap.com/maps =====
location ~* ^/proxy/webapi\.amap\.com/maps(?:/(.*))?$ {
set $webapi_path $1;
proxy_pass https://webapi.amap.com/maps/${webapi_path}$is_args$args;
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 Accept-Encoding "";
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Allow-Credentials;
}
# ===== 规则2: jsapi-service.amap.com =====
location ~* ^/proxy/jsapi-service\.amap\.com(?:/(.*))?$ {
set $jsapi_service_path $1;
proxy_pass https://jsapi-service.amap.com/${jsapi_service_path}$is_args$args;
proxy_set_header Host jsapi-service.amap.com;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Allow-Credentials;
}
# ===== 规则3: webapi.amap.com =====
location ~* ^/proxy/webapi\.amap\.com(?:/(.*))?$ {
set $webapi_path $1;
proxy_pass https://webapi.amap.com/${webapi_path}$is_args$args;
proxy_set_header Host webapi.amap.com;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Allow-Credentials;
}
# ===== 规则4: restapi.amap.com(安全密钥验证)=====
location ~* ^/proxy/restapi\.amap\.com/(.*)$ {
set $restapi_path $1;
set $args "$args&jscode=0409a616c842552f2f836d3f6f402836";
proxy_pass https://restapi.amap.com/$restapi_path$is_args$args;
proxy_set_header Host restapi.amap.com;
#proxy_set_header Referer https://restapi.amap.com;
#proxy_set_header User-Agent "Mozilla/5.0";
#add
#proxy_set_header Origin https://jsapi.amap.com;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Allow-Credentials;
}
# ===== 规则5: jsapi.amap.com =====
location ~* ^/proxy/jsapi\.amap\.com/(.*)$ {
set $jsapi_path $1;
proxy_pass https://jsapi.amap.com/$jsapi_path$is_args$args;
proxy_set_header Host jsapi.amap.com;
#proxy_set_header Referer https://jsapi.amap.com;
#proxy_set_header Origin https://jsapi.amap.com;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Allow-Credentials;
}
# ===== 规则6: 通用兜底 *.amap.com =====
location ~ ^/proxy/([^/]+)/(.*)$ {
set $target_host $1;
set $target_path $2;
proxy_pass https://$target_host/$target_path$is_args$args;
proxy_ssl_server_name on;
proxy_ssl_name $target_host;
proxy_set_header Host $target_host;
proxy_set_header Referer "https://$target_host/";
proxy_set_header Origin "https://$target_host";
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Expose-Headers;
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET, OPTIONS" always;
add_header Access-Control-Allow-Headers "*" always;
add_header Access-Control-Expose-Headers "Content-Length,Content-Range,ETag" always;
if ($request_method = OPTIONS) { return 204; }
}
access_log /data/nginxLogs/access.log;
error_log /data/nginxLogs/error.log;
}