164.在 Vue3 中使用 OpenLayers 加载 Esri 地图(多种形式)

发布于:2025-09-03 ⋅ 阅读:(14) ⋅ 点赞:(0)

适配: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_TOKEN

const 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}` : url

const 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 const

export 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 const

export 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,过期与错误时有兜底提示。


网站公告

今日签到

点亮在社区的每一天
去签到