一. 背景
随着全球化业务发展,我们需要为海外用户提供地图服务。经过技术调研发现:
- Google 地图成本问题:Google Maps API 采用按量计费模式,随着用户量增长会产生高昂费用
- 国内地图国际化不足:百度、高德等国内地图服务对英文支持有限,海外覆盖不全面,英文展示效果不佳
- OpenStreetMap 优势 :
- 完全免费的开源地图数据
- 全球覆盖且支持多语言
- 活跃的社区持续更新数据
基于以上考量,我们选择leaflet + OpenStreetMap技术方案:
- leaflet 提供轻量级、高性能的地图展示能力
- OpenStreetMap 提供免费可靠的全球地图数据
- 两者结合既能满足功能需求,又能有效控制成本
二. 介绍
1. leaflet
leaflet 是一个开源的 JavaScript 库,用于创建交互式、移动友好的网络地图。它轻量级(约 39KB 的 JS 代码),但功能强大,支持大多数桌面和移动平台。

2. OpenStreetMap(OSM)
OpenStreetMap(OSM) 是一个由用户社区创建和维护的免费可编辑的世界地图,常被称为"地理维基百科"。
说到 OpenStreetMap,就不得不说一下地图瓦片。
3. 地图瓦片(Tiles)
地图瓦片是将地图分割成小的正方形图片(通常是 256×256 或 512×512 像素),按不同缩放级别组织起来的系统。这种技术允许:
- 高效加载:只加载当前视图需要的部分地图
- 快速渲染:预渲染的图片比动态渲染快
- 缓存友好:瓦片可以被浏览器和 CDN 缓存
- 并行加载:可以同时加载多个瓦片
瓦片坐标系统
瓦片通常使用(x,y,z)坐标标识:
- z:缩放级别(0 是最小缩放,整个世界显示在一个瓦片中)
- x 和 y:在该缩放级别下的行列位置
瓦片使用流程
- 请求瓦片:当用户查看地图时,Leaflet 根据当前视图的中心和缩放级别计算需要的瓦片坐标
- 获取瓦片:从瓦片服务器(如 OpenStreetMap 的服务器)请求这些瓦片
- 拼接显示:将获取的瓦片拼接成无缝的地图视图
- 预加载:通常还会预加载周边瓦片,以便用户平移地图时流畅显示
4. 二者关系
leaflet 是一个地图显示库,而 OpenStreetMap 是地图数据源之一,也就是瓦片源。Leaflet 可以显示来自 OpenStreetMap 的地图瓦片,也可以显示其他来源的地图数据。OpenStreetMap 数据可以被多种客户端(包括 Leaflet)使用来渲染地图。
除了 OpenStreetMap,Leaflet 的灵活性使其能够兼容多种瓦片源,包括:
- 商业地图(Google、Bing、HERE)
- 卫星影像(NASA、Mapbox)
- 国家专用地图(天地图、Swisstopo)
- 矢量瓦片(GeoServer、Mapbox)
- 开源地图(Stamen、Esri)
具体接入方式需参考各服务的 API 文档或对应 leaflet 插件说明,本篇文章不做过多赘述!
接下来,我们将介绍如何借助 Tare 来实现 leaflet + OpenStreetMap 地图,来从 0 到 1 构建一个简单的地图应用程序。
三. 安装
接下来,我借助 Trae IDE 提供的内置智能体 Builder ,来帮助我从 0 到 1 开发一个完整的项目。根据我发出的需求, Builder 会调用不同的工具,包括运行命令的工具,来有效地处理我的需求。

在这里,我取消了 Builder 智能体的执行命令,我手动安装必要库 leaflet
,最新版本为 1.9.4
sh
pnpm install leaflet
还需要添加 types
支持,不然 ts
项目会报类型文件错误
sh
pnpm install @types/leaflet
四. 初始化地图
安装完 leaflet
之后,我们就可以,创建一个地图组件,以 London
, UK
为中心,缩放级别为 13
。
js
<script lang="ts" setup>
import { onMounted } from 'vue';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
onMounted(() => {
const map = L.map('map').setView([51.505, -0.09], 5);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution:
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
maxZoom: 19,
}).addTo(map);
// 添加标记点位
L.marker([51.5, -0.09]).addTo(map).bindPopup('London, UK').openPopup();
});
</script>
<template>
<div id="map" style="width: 100%; height: 500px"></div>
</template>

不同级别下的点位

这样,一个大体的雏形就出来了。
五. 暗黑模式支持
已有项目中支持暗黑模式,所以地图也要支持暗黑模式,问一下 Trae
,如何实现暗黑模式?

html
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import { Page } from '@vben/common-ui'
import L from 'leaflet'
import 'leaflet/dist/leaflet.css'
const darkMode = ref(true)
onMounted(() => {
const map = L.map('map').setView([51.505, -0.09], 13)
// 浅色图层
const lightLayer = L.tileLayer(
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
{
attribution:
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
maxZoom: 19
}
)
// 深色图层
const darkLayer = L.tileLayer(
'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',
{
attribution:
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
maxZoom: 19
}
)
// 根据darkMode适配主题
darkMode.value ? darkLayer.addTo(map) : lightLayer.addTo(map)
// 添加标记
L.marker([51.5, -0.09]).addTo(map).bindPopup('London, UK').openPopup()
})
</script>
<template>
<div id="map" style="width: 100%; height: 100%"></div>
</template>

六. 优化报错问题

偶发性报错,因为 leaflet
地图容器被重复初始化导致的。我们需要优化一下流程,确保地图只在组件挂载时初始化一次,并在组件卸载时正确销毁地图实例。
通过添加销毁逻辑和使用 ref 来跟踪地图实例状态来解决这个问题。
html
<script lang="ts" setup>
import { onMounted, onUnmounted, ref } from 'vue'
import { usePreferences } from '@vben/preferences'
import L from 'leaflet'
import 'leaflet/dist/leaflet.css'
const { isDark } = usePreferences()
const darkMode = ref(isDark.value)
const mapInstance = ref<L.Map | null>(null)
onMounted(() => {
if (mapInstance.value) {
mapInstance.value.remove()
}
mapInstance.value = L.map('map').setView([51.505, -0.09], 5)
// 浅色图层
const lightLayer = L.tileLayer(
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
{
attribution:
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
maxZoom: 19
}
)
// 深色图层
const darkLayer = L.tileLayer(
'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',
{
attribution:
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
maxZoom: 19
}
)
darkMode.value
? darkLayer.addTo(mapInstance.value as L.Map)
: lightLayer.addTo(mapInstance.value as L.Map)
// 添加标记
L.marker([51.5, -0.09])
.addTo(mapInstance.value as L.Map)
.bindPopup('London, UK')
.openPopup()
})
onUnmounted(() => {
if (mapInstance.value) {
mapInstance.value.remove()
mapInstance.value = null
}
})
</script>
<template>
<div id="map" style="width: 100%; height: 100%"></div>
</template>
非常简单,以上代码,简洁版的地图组件就完成了。接下来要实现更多的功能完善,比如:添加标记组、连线、轨迹等功能。
注意:要使用 🪜 才能正确渲染地图哦!
七. 总结
leaflet + OpenStreetMap 是一个强大的组合,它提供了一个简单、灵活且高效的方式来展示全球地图。通过使用 leaflet 提供的地图显示功能,我们可以轻松地将 OpenStreetMap 的地图数据集成到我们的应用程序中。
而 OpenStreetMap 是免费的,同时覆盖了全球的大部分地区,包括大陆、海洋、南极洲等,还支持多种语言、暗黑模式等。
总之,瓦片技术是现代网络地图高效运行的基础,Leaflet 和 OpenStreetMap 的结合提供了一个强大而免费的地图解决方案。