164.在 Vue3 中使用 OpenLayers 加载 Esri 地图(多种形式)
适配: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,过期与错误时有兜底提示。