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.pswp.cn/diannao/97629.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/97629.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

深入了解Flink核心:Slot资源管理机制

TaskExecutor、Task 和 Slot 简单来说&#xff0c;它们的关系可以比作&#xff1a;TaskExecutor&#xff1a;一个工厂&#xff0c;拥有固定的生产资源。TaskSlot&#xff1a;工厂里的一个工位。每个工位都预先分配了一份独立的资源&#xff08;主要是内存&#xff09;。Task&am…

java web 练习demo。生成简单验证码前端是jsp

目录结构 demo\ ├── WEB-INF\ │ └── weblogic.xml # WebLogic服务器配置文件 ├── demo.iml # IntelliJ IDEA项目配置文件 ├── lib\ # Java EE核心依赖库 │ ├── javax.annotation.jar │ ├── javax.ejb.jar │ ├── javax.…

拥抱智能高效翻译 ——8 款视频翻译工具深度测评

前阵子帮知识博主做跨境视频翻译&#xff0c;踩了不少坑&#xff1a;把 “内卷” 直译成 “involution” 让海外观众困惑&#xff0c;多语种版本赶工 3 天只出 2 种&#xff0c;还得手动核对 “碳中和”“非遗” 这类特色词的译法&#xff1b;用传统工具译完&#xff0c;视频要…

[知识点记录]SQLite 数据库和MySQL 数据库有什么区别?

核心区别&#xff1a;一个“内嵌”&#xff0c;一个“独立”SQLite (你的个人笔记本)本质&#xff1a; 它是“无服务器”的&#xff0c;或者叫“内嵌式”数据库。它不需要一个独立的程序一直在后台运行。你的应用程序&#xff08;比如Strapi&#xff09;直接就能读写它的数据库…

【Spark Core】(二)RDD编程入门

目录1 程序入口&#xff1a;SparkContext对象2 RDD的创建2.1 本地创建2.2 读取文件创建3 RDD算子4 常用Transform算子4.1 map算子4.2 flatMap算子4.3 reduceBykey算子4.4 mapValues算子<实例> WordCount4.5 groupBy算子4.6 filter算子4.7 distinct算子4.8 union算子4.9 j…

java IDEA run/Debug异常:“jdk1.8injava.exe“ CreateProcess error=206, 文件名或扩展名太长

&#x1f9d1; 博主简介&#xff1a;CSDN博客专家、CSDN平台优质创作者&#xff0c;高级开发工程师&#xff0c;数学专业&#xff0c;10年以上C/C, C#,Java等多种编程语言开发经验&#xff0c;拥有高级工程师证书&#xff1b;擅长C/C、C#等开发语言&#xff0c;熟悉Java常用开发…

Java 函数编程之【过滤器filter()合并】【predicate(断言)】与【谓词逻辑】

Java函数式编程之【过滤器filter合并】【predicate&#xff08;断言&#xff09;】与【谓词逻辑】一、合并多个过滤器filter &#xff08;Lambda版本&#xff09;二、合并多个过滤器filter &#xff08;谓词逻辑&#xff08;Predicate&#xff09;版本&#xff09;&#xff08;…

CentOS10安装RabbitMQ

1.下载资源 &#xff08;1&#xff09;下载erlang-rpm 注意&#xff1a;按照图片中的下载&#xff0c;用绿色三角形指向的是重点关注的。 网址&#xff1a; erlang-rpmhttps://github.com/rabbitmq/erlang-rpm/releases &#xff08;2&#xff09;下载rabbitmq-server 注…

JVM——八股文

1. JDK, JRE和JVM的关系JDK JRE Java开发工具JRE JVM Java核心类库JDK供Java程序开发人员开发软件&#xff0c;JRE供客户使用&#xff0c;只需要JVM运行环境即可。JVM运行的是class字节码&#xff0c;不仅能运行Java代码&#xff0c;还能运行其他语言&#xff0c;只要语言能…

骑行把带定期换,维乐 Skin Wrap 把带焕新骑行

在公路骑行的装备体系里&#xff0c;把带是最易被忽视却至关重要的“消耗品”。它是骑手手部与车身的直接连接&#xff0c;每一次转向、变速、刹车&#xff0c;都需通过把带传递力量与操控意图&#xff1b;同时&#xff0c;它还承担着吸汗、减震、保护车把的作用。可长期使用后…

LeetCode100-73矩阵置零

本文基于各个大佬的文章 上点关注下点赞&#xff0c;明天一定更灿烂&#xff01; 前言 Python基础好像会了又好像没会&#xff0c;所有我直接开始刷leetcode一边抄样例代码一边学习吧。本系列文章用来记录学习中的思考&#xff0c;写给自己看的&#xff0c;也欢迎大家在评论区指…

宁波市第八届网络安全大赛 -- Crypto -- WriteUp

宁波市第八届网络安全大赛 – Crypto – WriteUp Three-prime RSA task import gmpy2 from Crypto.Util.number import *from secret import flagp getPrime(512) q getPrime(512) r getPrime(512) n p * q * r random_num getPrime(28) D ((p q r) * random_num) % n …

大语言模型 (LLM) 与多模态大模型 (MLM)

文章目录概述&#xff1a;从“模型”到“大”模型1、大语言模型 (Large Language Model, LLM)1.1 定义与概述关键特征&#xff1a;1.2 核心技术与架构Transformer架构自注意力机制 (Self-Attention)1.3 训练过程1.4 工作原理2. 多模态大模型 (Multimodal Large Model, MLM)2.1 …

HTML应用指南:利用GET请求获取全国招商银行网点位置信息

招商银行&#xff08;China Merchants Bank, CMB&#xff09;作为中国领先的股份制商业银行&#xff0c;始终坚持“以客户为中心”的服务理念&#xff0c;致力于为个人客户、企业客户及机构客户提供专业、高效、便捷的综合金融服务。依托“轻型银行”战略与“金融科技银行”建设…

JVM性能监控工具的使用

了解JVM性能监控工具并能熟练使用&#xff0c;是Java开发者进阶的必备技能。下面本文将为你介绍一些主流的JVM性能监控工具及其使用方法&#xff0c;并通过一些场景案例来分析如何应用这些工具解决实际问题。 &#x1f6e0;️ JVM性能监控与调优工具指南 ✨ 工具概览 以下是几款…

【工作】一些找工作需要了解避雷的知识

面试前 1.公司的具体情况 公司全称&#xff0c;办公地点&#xff0c;涉及岗位 要求hr做个简单的公司介绍 2.岗位职责/业务方向 工作内容、公司业务 3.薪资待遇&#xff0c;构成&#xff0c;底薪&#xff0c;五险一金 问一下工资范围 底薪 &#xff08;有责&#xff0c;无…

五、练习2:Git分支操作

练习2&#xff1a;Git分支操作 练习目标 掌握Git分支的创建、切换、合并等操作&#xff0c;理解分支在开发中的作用。 练习步骤 步骤1&#xff1a;准备基础仓库 # 创建练习目录 mkdir branch-practice cd branch-practice# 初始化仓库 git init# 创建初始文件 echo "# 分支…

【笔记】算法设计:异或空间线性基

Content1.什么是异或&#xff08;定义和性质&#xff09;2.异或空间线性基的构造方法3.异或空间线性基的应用4.算法设计例举5.小结说明算法设计应用之前&#xff0c;首先明确异或空间线性基&#xff1a;一种数据结构。用于处理异或关系&#xff08;运算&#xff09;下的向量空间…

Filebeat采集数据与日志分析实战

&#x1f31f;Filebeat采集数据的原理 Filebeat默认按行采集数据&#xff0c;如果数据没有换行&#xff0c;则该条数据无法采集到 属于有状态服务&#xff0c;可以记录上一次采集数据的位置点信息 修改配置文件 vim /etc/filebeat/config/03-log-to-console.yaml filebeat.inp…

Fluent Bit针对kafka心跳重连机制详解(下)

#作者&#xff1a;程宏斌 文章目录disconnectreconnect接上篇&#xff1a;https://blog.csdn.net/qq_40477248/article/details/150957571?spm1001.2014.3001.5501disconnect 断开连接的情况主要是两种: 连接或传输过程中有错误发生 超时, 比如空闲时间超时 ** * Close and …