О нас Руководства Проекты Контакты
Админка
пожалуйста подождите

Leaflet.js — ведущая open-source JavaScript-библиотека для интерактивных карт, оптимизированных для мобильных устройств. В отличие от Google Maps или Mapbox, Leaflet является лёгкой (~42KB в gzipped), работает с любым провайдером тайлов и обеспечивает полный контроль над стилями и поведением. Это руководство посвящено созданию production-ready картографических приложений с точки зрения senior-разработчика.

Почему Leaflet

Leaflet предлагает убедительные преимущества:

  1. Лёгкость: всего 42KB, быстро загружается везде
  2. Независимость от провайдера: работает с OpenStreetMap, Mapbox, пользовательскими тайлами
  3. Mobile-first: поддержка touch и адаптивность из коробки
  4. Расширяемость: богатая экосистема плагинов под любые задачи
  5. Бесплатно: для базового использования не требуются API-ключи

Начало работы

Установка

<!--CSS-->
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
<!--JavaScript-->
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
<!--Контейнер-->
<div id="map" style="height: 500px;"></div>

Или через npm:

npm install leaflet
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';

Базовая карта

// Инициализировать карту с центром по координатам
const map = L.map('map').setView([51.505, -0.09], 13);
// Добавить тайлы OpenStreetMap
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© OpenStreetMap contributors'
}).addTo(map);
// Добавить маркер
L.marker([51.5, -0.09])
.addTo(map)
.bindPopup('Hello from London!')
.openPopup();

Провайдеры тайлов

OpenStreetMap (бесплатно)

L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);

Mapbox (кастомизация стилей)

L.tileLayer('https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}', {
attribution: '© Mapbox © OpenStreetMap',
id: 'mapbox/streets-v11',
accessToken: 'your.mapbox.access.token'
}).addTo(map);

Спутниковые снимки

L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: '© Esri, Maxar, Earthstar Geographics'
}).addTo(map);

Переключение слоёв

const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap'
});
const satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: '© Esri'
});
const map = L.map('map', {
center: [51.505, -0.09],
zoom: 13,
layers: [osm] // Слой по умолчанию
});
L.control.layers({
'Street Map': osm,
'Satellite': satellite
}).addTo(map);

Маркеры и всплывающие окна

Базовые маркеры

// Простой маркер
const marker = L.marker([51.5, -0.09]).addTo(map);
// Со всплывающим окном
marker.bindPopup('<b>Location</b><br>Description here');
// Открыть всплывающее окно сразу
marker.openPopup();
// С подсказкой (показывается при наведении)
marker.bindTooltip('Quick info');

Пользовательские иконки

const customIcon = L.icon({
iconUrl: '/images/marker.png',
iconSize: [32, 32],
iconAnchor: [16, 32], // Точка, соответствующая позиции маркера
popupAnchor: [0, -32] // Точка, где открывается всплывающее окно относительно iconAnchor
});
L.marker([51.5, -0.09], { icon: customIcon }).addTo(map);

Кластеры маркеров

Для большого количества маркеров используйте плагин MarkerCluster:

<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster/dist/MarkerCluster.css" />
<script src="https://unpkg.com/leaflet.markercluster/dist/leaflet.markercluster.js"></script>
const markers = L.markerClusterGroup();
locations.forEach(loc => {
const marker = L.marker([loc.lat, loc.lng])
.bindPopup(loc.name);
markers.addLayer(marker);
});
map.addLayer(markers);

Рисование фигур

Круги и полигоны

// Круг (радиус в метрах)
L.circle([51.508, -0.11], {
color: 'red',
fillColor: '#f03',
fillOpacity: 0.5,
radius: 500
}).addTo(map);
// Полигон
L.polygon([
[51.509, -0.08],
[51.503, -0.06],
[51.51, -0.047]
], {
color: 'blue',
fillOpacity: 0.3
}).addTo(map);
// Прямоугольник
L.rectangle([
[51.49, -0.08],
[51.5, -0.06]
], {
color: '#ff7800',
weight: 1
}).addTo(map);
// Ломаная (без заливки)
L.polyline([
[51.505, -0.09],
[51.51, -0.1],
[51.51, -0.12]
], {
color: 'green',
weight: 3,
dashArray: '5, 10'
}).addTo(map);

Работа с GeoJSON

GeoJSON — стандартный формат для географических данных:

const geojsonData = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"name": "Central Park",
"category": "park"
},
"geometry": {
"type": "Point",
"coordinates": [-73.965355, 40.782865]
}
},
{
"type": "Feature",
"properties": {
"name": "Manhattan",
"population": 1628706
},
"geometry": {
"type": "Polygon",
"coordinates": [[
[-74.047285, 40.679548],
[-73.907000, 40.679548],
[-73.907000, 40.882214],
[-74.047285, 40.882214],
[-74.047285, 40.679548]
]]
}
}
]
};
// Добавить GeoJSON со стилизацией
L.geoJSON(geojsonData, {
style: function(feature) {
return {
color: feature.properties.category === 'park' ? 'green' : 'blue',
fillOpacity: 0.5
};
},
pointToLayer: function(feature, latlng) {
return L.circleMarker(latlng, {
radius: 8,
fillColor: '#ff7800',
color: '#000',
weight: 1,
fillOpacity: 0.8
});
},
onEachFeature: function(feature, layer) {
layer.bindPopup(`<b>${feature.properties.name}</b>`);
}
}).addTo(map);

Загрузка GeoJSON по URL

fetch('/api/locations.geojson')
.then(response => response.json())
.then(data => {
L.geoJSON(data).addTo(map);
});

События карты

События клика

// Клик по карте
map.on('click', function(e) {
console.log('Clicked at:', e.latlng);
L.popup()
.setLatLng(e.latlng)
.setContent(`Coordinates: ${e.latlng.lat.toFixed(4)}, ${e.latlng.lng.toFixed(4)}`)
.openOn(map);
});
// Клик по маркеру
marker.on('click', function(e) {
console.log('Marker clicked');
});

Перемещение карты

map.on('moveend', function() {
const center = map.getCenter();
const zoom = map.getZoom();
const bounds = map.getBounds();
console.log('Center:', center);
console.log('Zoom:', zoom);
console.log('Bounds:', bounds);
// Загрузить данные для видимой области
loadMarkersInBounds(bounds);
});
map.on('zoomend', function() {
const zoom = map.getZoom();
// Показывать/скрывать слои в зависимости от масштаба
if (zoom > 15) {
detailLayer.addTo(map);
} else {
map.removeLayer(detailLayer);
}
});

Местоположение пользователя

// Определить местоположение пользователя
map.locate({ setView: true, maxZoom: 16 });
map.on('locationfound', function(e) {
const radius = e.accuracy / 2;
L.marker(e.latlng)
.addTo(map)
.bindPopup(`You are within ${radius} meters`)
.openPopup();
L.circle(e.latlng, { radius: radius }).addTo(map);
});
map.on('locationerror', function(e) {
alert('Location access denied');
});

Пользовательские элементы управления

Создание пользовательского элемента управления

const InfoControl = L.Control.extend({
options: {
position: 'bottomright'
},
onAdd: function(map) {
const container = L.DomUtil.create('div', 'info-control');
container.innerHTML = '<h4>Map Info</h4><div id="info-content"></div>';
// Предотвратить взаимодействие с картой при клике по элементу управления
L.DomEvent.disableClickPropagation(container);
return container;
},
update: function(content) {
document.getElementById('info-content').innerHTML = content;
}
});
const infoControl = new InfoControl();
map.addControl(infoControl);
// Обновлять при наведении на объект
layer.on('mouseover', function(e) {
infoControl.update(`<b>${e.target.feature.properties.name}</b>`);
});

Интеграция с Vue.js

<template>
<div id="map" ref="mapContainer"></div>
</template>
<script>
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
export default {
props: {
center: {
type: Array,
default: () => [51.505, -0.09]
},
markers: {
type: Array,
default: () => []
}
},
data() {
return {
map: null,
markerLayer: null
};
},
mounted() {
this.initMap();
},
watch: {
markers: {
handler(newMarkers) {
this.updateMarkers(newMarkers);
},
deep: true
}
},
methods: {
initMap() {
this.map = L.map(this.$refs.mapContainer).setView(this.center, 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap'
}).addTo(this.map);
this.markerLayer = L.layerGroup().addTo(this.map);
this.updateMarkers(this.markers);
},
updateMarkers(markers) {
this.markerLayer.clearLayers();
markers.forEach(m => {
L.marker([m.lat, m.lng])
.bindPopup(m.popup)
.addTo(this.markerLayer);
});
}
},
beforeDestroy() {
if (this.map) {
this.map.remove();
}
}
};
</script>
<style>
#map {
height: 400px;
width: 100%;
}
</style>

Советы по производительности

Большие наборы данных

// Использовать Canvas renderer для тысяч маркеров
const map = L.map('map', {
renderer: L.canvas()
});
// Или для каждого слоя
L.circleMarker([lat, lng], {
renderer: L.canvas()
}).addTo(map);
// Упрощать GeoJSON при изменении масштаба
L.geoJSON(complexData, {
style: { weight: 2 },
smoothFactor: 1.5 // Упрощать пути
});

Ленивая загрузка

// Загружать маркеры только в пределах видимых границ
map.on('moveend', debounce(async function() {
const bounds = map.getBounds();
const response = await fetch(`/api/markers?bbox=${bounds.toBBoxString()}`);
const data = await response.json();
markerLayer.clearLayers();
L.geoJSON(data).addTo(markerLayer);
}, 300));

Ключевые выводы

  1. Выберите подходящего провайдера тайлов: OSM — бесплатно, Mapbox — для кастомных стилей
  2. Кластеризуйте маркеры: критично для наборов данных более ~100 маркеров
  3. Используйте GeoJSON: стандартный формат работает везде
  4. Корректно обрабатывайте события: применяйте debounce для дорогих операций
  5. Canvas для производительности: переключайте renderer для больших наборов данных
  6. Mobile-first: тщательно тестируйте touch-взаимодействия

Leaflet предоставляет всё необходимое для профессиональных картографических приложений, оставаясь лёгким и гибким — идеально для проектов, которым нужны карты без привязки к конкретному вендору.

 
 
 
Языки
Темы
Copyright © 1999 — 2026
Зетка Интерактив