適配:Vue 3 + Vite + TypeScript(也兼容 JS)
地圖引擎:OpenLayers v10+
目標:一次性學會 多種 Esri 底圖加載方式、注記疊加、動態切換、令牌(Token)鑒權、常見坑位排查。
一、效果預覽
二、為什么選 OpenLayers + Esri
OpenLayers:開源、功能強、國產項目生態友好,坐標系與投影支持完善;
Esri Basemaps:樣式豐富、全球覆蓋、質量高(影像、街道、灰底、地形、海洋等);
開箱即用的 XYZ:Esri 的許多底圖以
XYZ/MapServer tile/{z}/{y}/{x}
形式提供,接入簡單。
?? 合規與用量:請遵守 Esri 使用條款與歸屬聲明(Attribution)。部分服務或高并發訪問可能需要 ArcGIS API Key/Token。
三、項目初始化
1)創建工程
# TypeScript 推薦
npm create vite@latest ol-esri-demo -- --template vue-ts
cd ol-esri-demo
npm i
2)安裝依賴
npm i ol element-plus # element-plus 可選,用于演示切換控件
如果你使用 TailwindCSS 或 UnoCSS 也可以按需集成,這里不強依賴。
四、Esri 底圖服務速查(常用)
Esri 多數底圖可通過以下 URL 模板訪問:
https://server.arcgisonline.com/ArcGIS/rest/services/{ServicePath}/MapServer/tile/{z}/{y}/{x}
常用 ServicePath 示例(可按需取舍):
類別 | 名稱(鍵) | ServicePath | 說明 |
---|---|---|---|
影像 | World_Imagery | World_Imagery | 全球衛星/航空影像 |
街道 | World_Street_Map | World_Street_Map | 全球街道底圖 |
地形 | World_Terrain_Base | World_Terrain_Base | 地形底圖(可配合注記) |
物理 | World_Physical_Map | World_Physical_Map | 物理地貌底圖 |
地形注記 | World_Terrain_Reference | World_Terrain_Reference | 地形注記覆蓋層(Reference) |
海洋底圖 | Ocean_Base | Ocean/World_Ocean_Base | 海洋背景底圖 |
海洋注記 | Ocean_Reference | Ocean/World_Ocean_Reference | 海圖注記覆蓋層 |
淺灰底圖 | Canvas_Light_Gray_Base | Canvas/World_Light_Gray_Base | 灰白簡約底圖 |
淺灰注記 | Canvas_Light_Gray_Reference | Canvas/World_Light_Gray_Reference | 對應注記覆蓋層 |
深灰底圖 | Canvas_Dark_Gray_Base | Canvas/World_Dark_Gray_Base | 深灰暗色底圖 |
深灰注記 | Canvas_Dark_Gray_Reference | Canvas/World_Dark_Gray_Reference | 對應注記覆蓋層 |
地形陰影 | World_Shaded_Relief | World_Shaded_Relief | 陰影地形,常用于底紋 |
國界地名 | Boundaries_Places | Reference/World_Boundaries_and_Places | 國界與地名注記 |
🔎 提示:服務路徑可能會調整,若某個服務 404/空白,請替換為上表中其它常用項或在 ArcGIS 官方檢索同名服務。
五、最小可運行示例(Composition API)
下面是最簡實現:一個底圖源 + 一個注記源(可選),并支持按鈕切換底圖。
<!-- src/components/EsriMap.vue -->
<template><div class="container"><div class="toolbar"><el-button size="small" type="primary" @click="setBase('World_Imagery')">影像</el-button><el-button size="small" type="primary" @click="setBase('World_Street_Map')">街道</el-button><el-button size="small" type="primary" @click="setBase('World_Terrain_Base')">地形</el-button><el-button size="small" type="primary" @click="setBase('World_Physical_Map')">物理</el-button><el-switch v-model="showLabels" active-text="疊加注記" class="ml-3" /></div><div id="ol-container" /></div>
</template><script setup lang="ts">
import 'ol/ol.css'
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
import { Map, View } from 'ol'
import TileLayer from 'ol/layer/Tile'
import XYZ from 'ol/source/XYZ'
import { fromLonLat } from 'ol/proj'// --- 工具:拼接 Esri XYZ URL ---
const esriUrl = (servicePath: string, token?: string) => {const base = `https://server.arcgisonline.com/ArcGIS/rest/services/${servicePath}/MapServer/tile/{z}/{y}/{x}`return token ? `${base}?token=${token}` : base
}// --- 常用底圖 & 注記(可按需擴充) ---
const BASEMAPS: Record<string, string> = {World_Imagery: 'World_Imagery',World_Street_Map: 'World_Street_Map',World_Terrain_Base: 'World_Terrain_Base',World_Physical_Map: 'World_Physical_Map',Canvas_Light_Gray_Base: 'Canvas/World_Light_Gray_Base',Canvas_Dark_Gray_Base: 'Canvas/World_Dark_Gray_Base',Ocean_Base: 'Ocean/World_Ocean_Base',World_Shaded_Relief: 'World_Shaded_Relief',
}const LABELS: Record<string, string> = {Boundaries_Places: 'Reference/World_Boundaries_and_Places',World_Terrain_Reference: 'World_Terrain_Reference',Canvas_Light_Gray_Reference: 'Canvas/World_Light_Gray_Reference',Canvas_Dark_Gray_Reference: 'Canvas/World_Dark_Gray_Reference',Ocean_Reference: 'Ocean/World_Ocean_Reference',
}// --- 地圖實例與圖層 ---
const map = ref<Map | null>(null)
const baseSource = new XYZ({ crossOrigin: 'anonymous' })
const labelSource = new XYZ({ crossOrigin: 'anonymous' })const baseLayer = new TileLayer({ source: baseSource })
const labelLayer = new TileLayer({ source: labelSource, visible: false })// 可選:若有 Token,可在此統一配置
const ESRI_TOKEN = '' // 例如:import.meta.env.VITE_ESRI_TOKENconst setBase = (key: keyof typeof BASEMAPS) => {baseSource.setUrl(esriUrl(BASEMAPS[key], ESRI_TOKEN))
}const setLabel = (key: keyof typeof LABELS) => {labelSource.setUrl(esriUrl(LABELS[key], ESRI_TOKEN))
}const showLabels = ref(false)watch(showLabels, (val) => {labelLayer.setVisible(val)if (val && !labelSource.getUrls() && !labelSource.getUrl()) {// 默認選擇一個通用注記setLabel('Boundaries_Places')}
})onMounted(() => {map.value = new Map({target: 'ol-container',layers: [baseLayer, labelLayer],view: new View({projection: 'EPSG:3857',center: fromLonLat([116.3913, 39.9075]), // 北京天安門示例zoom: 4,}),})// 默認加載影像底圖 + 關閉注記setBase('World_Imagery')labelLayer.setVisible(false)
})onBeforeUnmount(() => {map.value?.setTarget(undefined)map.value = null
})
</script><style scoped>
.container { width: 100%; max-width: 980px; height: 600px; margin: 24px auto; border: 1px solid #e5e7eb; border-radius: 12px; overflow: hidden; }
.toolbar { display: flex; align-items: center; gap: 8px; padding: 10px; border-bottom: 1px solid #f1f5f9; }
#ol-container { width: 100%; height: calc(600px - 50px); }
</style>
以上示例已涵蓋:
動態切換不同 底圖;
可選疊加 注記;
Composition API 生命周期與資源釋放;
token
統一拼接擴展位。
六、基于下拉選擇的優雅切換(Element Plus)
<!-- 片段:替換按鈕為下拉選擇 -->
<template><div class="toolbar"><el-select v-model="baseKey" placeholder="選擇底圖" size="small" style="width: 220px"><el-option v-for="(path, key) in BASEMAPS" :key="key" :label="key" :value="key" /></el-select><el-select v-model="labelKey" placeholder="選擇注記" size="small" style="width: 240px" :disabled="!showLabels"><el-option v-for="(path, key) in LABELS" :key="key" :label="key" :value="key" /></el-select><el-switch v-model="showLabels" active-text="疊加注記" class="ml-3" /></div>
</template><script setup lang="ts">
const baseKey = ref<keyof typeof BASEMAPS>('World_Imagery')
const labelKey = ref<keyof typeof LABELS>('Boundaries_Places')watch(baseKey, (k) => setBase(k))
watch(labelKey, (k) => { if (showLabels.value) setLabel(k) })onMounted(() => {setBase(baseKey.value)setLabel(labelKey.value)
})
</script>
七、進階:高分屏渲染與平滑體驗
OpenLayers 的 XYZ
支持以下優化參數:
const baseSource = new XYZ({crossOrigin: 'anonymous',// 高分屏:按需提高像素比(會增加帶寬)tilePixelRatio: window.devicePixelRatio > 1 ? 2 : 1,// 關閉淡入動畫,切換更干脆transition: 0,
})
提示:高像素比會明顯提升清晰度,但也會提升瓦片請求量。根據終端與網絡狀況權衡開啟。
八、為 Esri 服務添加 Attribution(歸屬)
在很多情況下你需要為底圖添加歸屬信息:
const attribution = '? Esri — Source: Esri, others. See Esri Terms.'
const baseSource = new XYZ({crossOrigin: 'anonymous',attributions: attribution,
})
務必遵守 Esri 的使用條款,不同底圖可能要求的歸屬文本略有差異,請以官方說明為準。
九、帶 Token 的安全訪問(可選)
若你的組織開啟了受保護的服務,可通過以下方式統一附加 token
:
const ESRI_TOKEN = import.meta.env.VITE_ESRI_TOKEN
const withToken = (url: string) => ESRI_TOKEN ? `${url}?token=${ESRI_TOKEN}` : urlconst baseSource = new XYZ({crossOrigin: 'anonymous',tileLoadFunction: (imageTile, src) => {(imageTile.getImage() as HTMLImageElement).src = withToken(src)},
})
也可以在 URL 拼接時直接加上
?token=...
,但tileLoadFunction
更靈活,便于集中控制與替換。
十、常見問題(踩坑實錄)
首次進入空白 / 404
檢查 ServicePath 是否準確;
更換為本文表格中的其它服務進行對比;
檢查是否需要 Token,或當前 IP/地區可用性。
跨域報錯
為
XYZ
加上crossOrigin: 'anonymous'
;確保部署站點支持 HTTPS(多數 Esri 服務為 HTTPS 資源)。
坐標/投影錯亂
Esri 絕大多數底圖是 EPSG:3857 Web Mercator;
確保
View
的projection
與之匹配。
切換卡頓、過渡生硬
設置
transition: 0
讓切換更干脆;合理選擇
tilePixelRatio
;不要頻繁在短時間內切換,給到請求與緩存時間。
注記不對位
確保注記層與底圖同一投影(通常都是 3857);
海洋、灰底等注記請使用對應的 Reference 圖層。
國內訪問偶發慢
可在邊緣節點加緩存(CDN 反代);
對影像類底圖設置合適的初始
zoom
,避免一次性請求大量瓦片。
十一、可復用的 Basemap 注冊中心(推薦封裝)
抽離一份 esri-basemaps.ts
,集中管理底圖與注記:
// src/utils/esri-basemaps.ts
export const BASEMAPS = {World_Imagery: 'World_Imagery',World_Street_Map: 'World_Street_Map',World_Terrain_Base: 'World_Terrain_Base',World_Physical_Map: 'World_Physical_Map',Canvas_Light_Gray_Base: 'Canvas/World_Light_Gray_Base',Canvas_Dark_Gray_Base: 'Canvas/World_Dark_Gray_Base',Ocean_Base: 'Ocean/World_Ocean_Base',World_Shaded_Relief: 'World_Shaded_Relief',
} as constexport const LABELS = {Boundaries_Places: 'Reference/World_Boundaries_and_Places',World_Terrain_Reference: 'World_Terrain_Reference',Canvas_Light_Gray_Reference: 'Canvas/World_Light_Gray_Reference',Canvas_Dark_Gray_Reference: 'Canvas/World_Dark_Gray_Reference',Ocean_Reference: 'Ocean/World_Ocean_Reference',
} as constexport const esriUrl = (servicePath: string) =>`https://server.arcgisonline.com/ArcGIS/rest/services/${servicePath}/MapServer/tile/{z}/{y}/{x}`
然后在組件中直接引用:
import { BASEMAPS, LABELS, esriUrl } from '@/utils/esri-basemaps'
十二、完整頁面示例(帶布局樣式)
<!--
* @Author: 彭麒
* @Date: 2025/09/01
* @Email: 1062470959@qq.com
* @Description: Vue3 + OpenLayers 加載Esri地圖(多種形式) Composition API寫法
-->
<template><div class="container"><div class="w-full flex justify-center flex-wrap"><div class="font-bold text-[24px]">在Vue3中使用OpenLayers加載Esri地圖(多種形式)</div></div><h4><el-button type="primary" size="small" @click="showmap('World_Imagery')">World_Imagery</el-button><el-button type="primary" size="small" @click="showmap('World_Street_Map')">World_Street</el-button><el-button type="primary" size="small" @click="showmap('World_Terrain_Base')">World_Terrain</el-button><el-button type="primary" size="small" @click="showmap('World_Physical_Map')">World_Physical</el-button></h4><div id="vue-openlayers"></div></div>
</template><script setup>
import 'ol/ol.css'
import { ref, onMounted } from 'vue'
import { Map, View } from 'ol'
import Tile from 'ol/layer/Tile'
import XYZ from 'ol/source/XYZ'
import { fromLonLat } from 'ol/proj'const map = ref(null)const source = new XYZ({crossOrigin: 'anonymous'
})const showmap = (x) => {source.setUrl(`https://server.arcgisonline.com/ArcGIS/rest/services/${x}/MapServer/tile/{z}/{y}/{x}`)
}const initMap = () => {map.value = new Map({target: 'vue-openlayers',layers: [new Tile({source: source}),new Tile({source: new XYZ({crossOrigin: 'anonymous',url: 'https://server.arcgisonline.com/ArcGIS/rest/services/Ocean/World_Ocean_Reference/MapServer/tile/{z}/{y}/{x}'})})],view: new View({projection: 'EPSG:3857',center: fromLonLat([-114.064839, 22.548857]),zoom: 3})})
}onMounted(() => {initMap()showmap('Ocean/World_Ocean_Base')
})
</script><style scoped>
.container {width: 840px;height: 600px;margin: 50px auto;border: 1px solid #42b983;
}
#vue-openlayers {width: 800px;height: 430px;margin: 0 auto;border: 1px solid #42b983;position: relative;
}
</style>
十三、部署與上線注意事項
HTTPS:生產環境務必啟用 HTTPS,避免混合內容問題;
緩存:對靜態資源與地圖瓦片配置合理的 CDN 緩存策略;
歸屬聲明:在頁面底部或地圖角落放置 Esri 歸屬信息;
請求上限:關注訪問量與并發數,如有大量流量,考慮注冊 ArcGIS 正式 Key 并評估額度;
可用性監控:在瓦片加載失敗時上報或降級到備選底圖。
十四、小結
本文從 項目初始化、Esri 服務速查、最小可運行示例 到 下拉切換、注記疊加、高分屏優化、Token 鑒權、常見問題 做了完整演示。把 ServicePath
抽到配置文件、把 Token 與 Attribution 做成統一能力,就能在實際項目中快速復用、穩定迭代。
覺得有用的話,歡迎收藏、點贊、轉發給你的同事與朋友。也歡迎在評論區補充你常用的 Esri 服務路徑與優化經驗。
附:快速檢查清單(發布前自測)
不同底圖切換正常、無 404 ;
注記層與底圖的投影/對齊正常;
高分屏下瓦片清晰;
退出頁面后地圖正確銷毀;
歸屬聲明與使用條款合規;
若有 Token,過期與錯誤時有兜底提示。