当前位置: 首页 > ops >正文

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_ImageryWorld_Imagery全球卫星/航空影像
街道World_Street_MapWorld_Street_Map全球街道底图
地形World_Terrain_BaseWorld_Terrain_Base地形底图(可配合注记)
物理World_Physical_MapWorld_Physical_Map物理地貌底图
地形注记World_Terrain_ReferenceWorld_Terrain_Reference地形注记覆盖层(Reference)
海洋底图Ocean_BaseOcean/World_Ocean_Base海洋背景底图
海洋注记Ocean_ReferenceOcean/World_Ocean_Reference海图注记覆盖层
浅灰底图Canvas_Light_Gray_BaseCanvas/World_Light_Gray_Base灰白简约底图
浅灰注记Canvas_Light_Gray_ReferenceCanvas/World_Light_Gray_Reference对应注记覆盖层
深灰底图Canvas_Dark_Gray_BaseCanvas/World_Dark_Gray_Base深灰暗色底图
深灰注记Canvas_Dark_Gray_ReferenceCanvas/World_Dark_Gray_Reference对应注记覆盖层
地形阴影World_Shaded_ReliefWorld_Shaded_Relief阴影地形,常用于底纹
国界地名Boundaries_PlacesReference/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 更灵活,便于集中控制与替换。


十、常见问题(踩坑实录)

  1. 首次进入空白 / 404

    • 检查 ServicePath 是否准确;

    • 更换为本文表格中的其它服务进行对比;

    • 检查是否需要 Token,或当前 IP/地区可用性。

  2. 跨域报错

    • XYZ 加上 crossOrigin: 'anonymous'

    • 确保部署站点支持 HTTPS(多数 Esri 服务为 HTTPS 资源)。

  3. 坐标/投影错乱

    • Esri 绝大多数底图是 EPSG:3857 Web Mercator;

    • 确保 Viewprojection 与之匹配。

  4. 切换卡顿、过渡生硬

    • 设置 transition: 0 让切换更干脆;

    • 合理选择 tilePixelRatio

    • 不要频繁在短时间内切换,给到请求与缓存时间。

  5. 注记不对位

    • 确保注记层与底图同一投影(通常都是 3857);

    • 海洋、灰底等注记请使用对应的 Reference 图层。

  6. 国内访问偶发慢

    • 可在边缘节点加缓存(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>

十三、部署与上线注意事项

  1. HTTPS:生产环境务必启用 HTTPS,避免混合内容问题;

  2. 缓存:对静态资源与地图瓦片配置合理的 CDN 缓存策略;

  3. 归属声明:在页面底部或地图角落放置 Esri 归属信息;

  4. 请求上限:关注访问量与并发数,如有大量流量,考虑注册 ArcGIS 正式 Key 并评估额度;

  5. 可用性监控:在瓦片加载失败时上报或降级到备选底图。


十四、小结

本文从 项目初始化Esri 服务速查最小可运行示例下拉切换、注记叠加、高分屏优化、Token 鉴权、常见问题 做了完整演示。把 ServicePath 抽到配置文件、把 Token 与 Attribution 做成统一能力,就能在实际项目中快速复用、稳定迭代。

觉得有用的话,欢迎收藏、点赞、转发给你的同事与朋友。也欢迎在评论区补充你常用的 Esri 服务路径与优化经验。


附:快速检查清单(发布前自测)

  • 不同底图切换正常、无 404 ;

  • 注记层与底图的投影/对齐正常;

  • 高分屏下瓦片清晰;

  • 退出页面后地图正确销毁;

  • 归属声明与使用条款合规;

  • 若有 Token,过期与错误时有兜底提示。

http://www.xdnf.cn/news/19434.html

相关文章:

  • 后端Web实战-多表操作员工列表查询
  • Spring Bean生命周期的完全指南
  • 面试常考css:三列布局实现方式
  • Interceptor拦截器入门知识及其工作原理
  • 虚拟化技术是什么?电脑Bios中的虚拟化技术怎么开启
  • S32K3平台FEE 应用笔记
  • C++ 多线程实战 01|为什么需要线程:并发 vs 并行,进程 vs 线程
  • 6 种可行的方法:小米手机备份到电脑并恢复
  • js语言编写科技风格博客网站-详细源码
  • AI-调查研究-66-机器人 机械臂 软件算法体系:轨迹规划·视觉定位·力控策略
  • 网络层和数据链路层
  • 智能对话系统优化方案:解决响应偏差与个性化缺失问题
  • OpenStack网络类型解析
  • 超越Transformer:语言模型未来的认知革命与架构重构
  • 手写MyBatis第47弹:Interceptor接口设计与Invocation上下文传递机制--MyBatis动态代理生成与方法拦截的精妙实现
  • uniApp 混合开发全指南:原生与跨端的协同方案
  • shell编程基础入门-3
  • Ansible之playbook剧本
  • 【Spark Core】(三)RDD的持久化
  • nrf52840 解锁
  • Linux部署OSM本地服务测试环境
  • Ubuntu 25.10 Snapshot4 发布。
  • 电动两轮车手机导航投屏方案调研报告
  • 「日拱一码」076 深度学习——自然语言处理NLP
  • SOME/IP-SD中IPv4端点选项与IPv4 SD端点选项
  • Coze源码分析-工作空间-资源库-前端源码
  • 掌握正则表达式与文本处理:提升 Shell 编程效率的关键技巧
  • FFmpeg 不同编码的压缩命令详解
  • 【扩充位数三位变五位】2022-10-30
  • mysql导出csv中字段里有换行符的处理办法及hive导出处理办法