160.在 Vue3 中用 OpenLayers 解决国内 OpenStreetMap 地图加载不出来的问题
导读 / 摘要
在国内用 OpenLayers 加载 OpenStreetMap 瓦片时常见的异常有:空白瓦片 / 403/404 / CORS 被拦截 / 瓦片变成网格 / 坐标投影错位。本文从原理出发,给出可行的修复办法并附带完整 Vue3 + Composition API 的代码示例(含可选的 Node.js 代理代码),帮助你把文章代码直接复制到项目中运行并发布到 CSDN。
为什么在国内会“加载不出来”?
常见原因(排查清单):
使用了 HTTP 协议,但页面是 HTTPS(混合内容被浏览器拦截)。
访问到的 Tile 服务被网络限制 / 连接不稳定(国外 tile 服务器在国内连接慢或被限)。
CORS(跨域)问题:瓦片服务器没有正确返回
Access-Control-Allow-Origin
,浏览器会阻止渲染。投影(projection)错误:很多 XYZ 瓦片使用 WebMercator(EPSG:3857),但代码里用错成 EPSG:4326,会导致瓦片坐标错位或看起来“格子化”。
服务使用政策限制 / 热点限制:公共 OSM 服务器对直接大量请求有限制,正式产品应自建或使用第三方付费服务并做好缓存。
解决思路(3 种可选方案,从简单到稳妥)
方案 A(尝试/快速验证):把 URL 改为
https
+ 使用 EPSG:3857 +crossOrigin: 'anonymous'
。(适合测试和小流量)方案 B(推荐):如果直接请求国外瓦片不稳定,搭一个简单的代理 / 缓存服务(tile proxy),在国内服务器上代理外部瓦片并返回 CORS header。前端请求你自己的域名,稳定且合规。
方案 C(生产级):自建/托管瓦片服务(例如使用
mod_tile
+renderd
、mbtiles + tileserver 或购买第三方 CDN 瓦片服务)。(适合高并发/商用)
下面我们详细给出方案 A 与 B 的完整代码示例(Vue3 前端 + Node.js 代理)。
关键点回顾(在动手前务必记住)
OpenStreetMap 等 XYZ 瓦片通常基于 EPSG:3857(WebMercator),所以
View
的 center 应用fromLonLat([lng, lat])
。使用瓦片时尽量用 HTTPS,避免浏览器混合内容拦截。
若浏览器报 CORS 错误,优先考虑用服务器代理来绕过。
OSM 等公共瓦片有使用政策和流量限制;生产请自建或使用第三方服务并在页面给出合适的 attribution。
一、最小可运行(且修正了投影与 ref 写法)的 Vue3 组件(方案 A:直接请求瓦片)
<!-- OSMMap.vue -->
<template><div class="container"><div class="title">在 Vue3 中用 OpenLayers 加载 OSM(HTTPS + EPSG:3857)</div><div ref="mapContainer" class="map-box"></div></div>
</template><script setup>
/** @Author: 彭麒* @Date: 2025/08/26* @Email: 1062470959@qq.com*/
import 'ol/ol.css'
import { ref, onMounted, onUnmounted } from 'vue'
import { Map, View } from 'ol'
import TileLayer from 'ol/layer/Tile'
import XYZ from 'ol/source/XYZ'
import { fromLonLat } from 'ol/proj'const map = ref(null)
const mapContainer = ref(null)const initMap = () => {// 注意:使用 HTTPS 模板(避免浏览器混合内容)const osmSource = new XYZ({url: 'https://{a-c}.tile.openstreetmap.org/{z}/{x}/{y}.png',crossOrigin: 'anonymous' // 尝试允许跨域图片渲染})const osmLayer = new TileLayer({source: osmSource,zIndex: 1})map.value = new Map({target: mapContainer.value,layers: [osmLayer],view: new View({// 默认就是 EPSG:3857;推荐使用 fromLonLat 设置中心center: fromLonLat([116.389, 39.903]), // 北京示例zoom: 8})})
}onMounted(() => {initMap()
})onUnmounted(() => {if (map.value) {// 断开 target 可避免内存泄露map.value.setTarget(null)}
})
</script><style scoped>
.container { width: 840px; margin: 20px auto; }
.title { font-weight: 700; font-size: 20px; text-align:center; margin-bottom:10px; }
.map-box { width: 800px; height: 450px; margin: 0 auto; border: 1px solid #ccc; }
</style>
说明与排查:
若页面是 HTTPS 且上面代码仍为空白,请打开浏览器控制台看是否有 CORS 或 Mixed Content 报错。
若是 CORS 报错,则改用方案 B(见下)。
二、推荐做法:在国内部署一个简单的 Tile Proxy(方案 B)
原理:浏览器向你自己的域名请求瓦片(/tiles/{z}/{x}/{y}.png
),你的服务在服务器端去请求真正的上游瓦片(可带适当 headers / 缓存),并返回 Access-Control-Allow-Origin: *
。这样可以绕过很多 CORS / 连接问题,并能在服务器端做缓存降低流量。
1) Node (Express) 简单又实用的 tile proxy(示例)
要求 Node >= 18 或安装 node-fetch。示例使用 Node 18+ 的内置 fetch。
// tile-proxy.js
// node tile-proxy.js
import express from 'express'
const app = express()
const PORT = process.env.PORT || 3000// 简单路由:/tiles/:z/:x/:y.png
app.get('/tiles/:z/:x/:y.png', async (req, res) => {const { z, x, y } = req.params// 上游模板:可以替换成你想代理的瓦片地址// 注意:最好使用 HTTPS,上游可替换为 a.tile.openstreetmap.org 等const upstream = `https://a.tile.openstreetmap.org/${z}/${x}/${y}.png`try {const upstreamRes = await fetch(upstream)if (!upstreamRes.ok) {return res.status(upstreamRes.status).send('upstream error')}// 复制 Content-Type(通常是 image/png)const contentType = upstreamRes.headers.get('content-type') || 'image/png'res.set('Content-Type', contentType)// 让浏览器可以跨域加载res.set('Access-Control-Allow-Origin', '*')// 设置缓存(可根据需要调整)res.set('Cache-Control', 'public, max-age=86400')// 直接把上游流 pipe 到响应const body = upstreamRes.bodyif (body && body.pipe) {body.pipe(res)} else {const buffer = Buffer.from(await upstreamRes.arrayBuffer())res.send(buffer)}} catch (err) {console.error(err)res.status(500).send('proxy error')}
})app.listen(PORT, () => {console.log(`Tile proxy running at http://localhost:${PORT}`)
})
运行方式:
node --experimental-modules tile-proxy.js
(或使用npm init
+type: module
,或直接用ts-node
/ bundler`)将此部署到国内服务器(阿里云 / 腾讯云 / Vercel 等),域名对外可访问。
2) 前端如何使用代理 URL
把 XYZ 的 url 指向你的代理地址模板,例如 http://your-domain.com/tiles/{z}/{x}/{y}.png
:
const proxied = new XYZ({url: 'https://your-domain.com/tiles/{z}/{x}/{y}.png',crossOrigin: 'anonymous'
})
完整 Vue 组件只需要把 url
修改为代理模板即可(参考第一段代码中的 new XYZ({...})
)。
三、另一种绕过(不改后端)—— tileLoadFunction 动态加载(前端 fetch -> blob)
如果你不能部署代理但遇到轻微 CORS 问题(上游允许 OPTIONS,但图片直接被阻止),可以使用 tileLoadFunction
把瓦片先 fetch
成 blob 再赋值给 <img>
。不过这对跨域也受浏览器同源策略限制,且比代理更脆弱,不是首选。
示例代码片段(仅说明):
const srcTemplate = 'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png'
const xyz = new XYZ({tileLoadFunction: (imageTile, src) => {// 把 src 发送到你自己的 fetch-proxy endpoint,或直接 fetch(注意 CORS)fetch(src).then(r => r.blob()).then(blob => {const img = imageTile.getImage()const url = URL.createObjectURL(blob)img.src = url// 这里可在 img.onload 后 URL.revokeObjectURL(url)}).catch(() => {// 失败时可以设置占位图imageTile.getImage().src = '/images/tile-failed.png'})}
})
注意:tileLoadFunction
的成功依赖上游是否允许浏览器直接 fetch 以及网络情况;相比之下代理更稳定。
四、常见问题与调试技巧(实战清单)
控制台报 Mixed Content:确保瓦片 URL 使用
https://
。控制台报 CORS:优先用代理服务或确认上游返回
Access-Control-Allow-Origin
。瓦片显示成网格 / 坐标错位:检查是否误用了
projection: 'EPSG:4326'
或没有用fromLonLat
。瓦片 404 / 403:可能是上游禁止直接热点链接或请求过多,被封禁;换源或代理服务器解决。
性能:在高并发下,前端应限制
maxZoom
、开启tileCacheSize
(OpenLayers 有缓存机制)并在代理端做 HTTP 缓存。授权与归属:遵守 OSM 和瓦片供应商的使用条款,页面显著位置保留 attribution(例如:
© OpenStreetMap contributors
)。
五、可选功能:在页面中切换瓦片源(示例)
下面是一个可切换瓦片源(OSM / 代理)的简易 UI 逻辑片段(思路):
<template><div><button @click="useOSM">OSM (直接)</button><button @click="useProxy">使用本地代理</button><div ref="mapContainer" class="map-box"></div></div>
</template><script setup>
import { ref } from 'vue'
import { Map, View } from 'ol'
import TileLayer from 'ol/layer/Tile'
import XYZ from 'ol/source/XYZ'
import { fromLonLat } from 'ol/proj'const map = ref(null)
const mapContainer = ref(null)
let tileLayer = nullconst createLayer = (url) => {return new TileLayer({source: new XYZ({ url, crossOrigin: 'anonymous' })})
}const useOSM = () => {if (tileLayer) map.value.removeLayer(tileLayer)tileLayer = createLayer('https://{a-c}.tile.openstreetmap.org/{z}/{x}/{y}.png')map.value.addLayer(tileLayer)
}const useProxy = () => {if (tileLayer) map.value.removeLayer(tileLayer)tileLayer = createLayer('https://your-domain.com/tiles/{z}/{x}/{y}.png')map.value.addLayer(tileLayer)
}// init map...
</script>