Vite

Установка Leaflet в современном фронтенд-проекте на Vite начинается с подключения зависимостей и подготовки структуры приложения. Vite использует ES-модули и мгновенную сборку через dev-сервер, что делает работу с картографическими библиотеками более предсказуемой и быстрой по сравнению с классическими бандлерами.

Установка:

npm create vite@latest leaflet-vite-app
cd leaflet-vite-app
npm install
npm install leaflet

Дополнительно часто требуется установка типов (если используется TypeScript):

npm install -D @types/leaflet

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


Импорт Leaflet и подключение стилей

Leaflet не работает корректно без подключения CSS. В Vite стили импортируются напрямую из node_modules.

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

Инициализация карты выполняется после того, как DOM-элемент уже существует:

const map = L.map('map').setView([51.505, -0.09], 13);

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

HTML-структура:

<div id="map"></div>

CSS для контейнера карты критически важен:

#map {
  height: 100vh;
  width: 100%;
}

Без явной высоты контейнер будет иметь нулевую высоту, и карта не отобразится.


Архитектура Vite и влияние на Leaflet

Vite использует ESM-модули в режиме разработки и Rollup для production-сборки. Это влияет на работу Leaflet в нескольких аспектах:

  • модули импортируются напрямую без бандлинга в dev-режиме
  • HMR обновляет код без перезагрузки страницы
  • статические ресурсы обрабатываются отдельно через public/ и import

Leaflet изначально проектировался для классических бандлеров и CDN, поэтому некоторые внутренние пути (особенно иконки) требуют корректировки.


Проблема иконок маркеров в Vite

Одна из типичных проблем — отсутствие стандартных иконок маркеров. В production-сборке Vite не сохраняет пути Leaflet к изображениям автоматически.

Решение заключается в ручной переопределении путей:

import L from 'leaflet';
import iconUrl from 'leaflet/dist/images/marker-icon.png';
import iconRetinaUrl from 'leaflet/dist/images/marker-icon-2x.png';
import shadowUrl from 'leaflet/dist/images/marker-shadow.png';

delete L.Icon.Default.prototype._getIconUrl;

L.Icon.Default.mergeOptions({
  iconRetinaUrl,
  iconUrl,
  shadowUrl
});

Это обеспечивает корректную работу иконок как в dev, так и в production-сборке.


Работа с публичными ресурсами в Vite

Vite использует папку public/ для статических файлов, которые должны быть доступны без обработки сборщиком.

Структура:

public/
  images/
  data/

Пример использования кастомной иконки:

const customIcon = L.icon({
  iconUrl: '/images/custom-marker.png',
  iconSize: [32, 32],
  iconAnchor: [16, 32]
});

L.marker([51.5, -0.09], { icon: customIcon }).addTo(map);

Важно: путь начинается с /, так как Vite копирует содержимое public в корень сборки.


Динамическая инициализация карты в компонентах

В приложениях с компонентной архитектурой (React, Vue, Svelte) карта должна инициализироваться после монтирования DOM.

Пример логики:

import { useEffect } from 'react';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';

export default function MapComponent() {
  useEffect(() => {
    const map = L.map('map').setView([51.505, -0.09], 13);

    L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
      maxZoom: 19
    }).addTo(map);

    return () => {
      map.remove();
    };
  }, []);

  return <div id="map" style={{ height: '100vh' }} />;
}

Удаление карты при размонтировании важно для предотвращения утечек памяти.


Vite dev-server и горячая перезагрузка (HMR)

Vite обеспечивает мгновенное обновление модулей без полной перезагрузки страницы. В контексте Leaflet это влияет на:

  • повторную инициализацию карты
  • утечку DOM-слоев при частых обновлениях
  • необходимость очистки контейнера

Если карта пересоздаётся при HMR, требуется явное удаление предыдущего экземпляра:

if (import.meta.hot) {
  import.meta.hot.dispose(() => {
    map.remove();
  });
}

Без этого возможно накопление слоёв и некорректное отображение тайлов.


Работа с тайловыми слоями и оптимизация загрузки

Leaflet в Vite не ограничивает выбор источников тайлов. Наиболее распространённый вариант — OpenStreetMap.

L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
  maxZoom: 19,
  updateWhenIdle: true,
  keepBuffer: 2
}).addTo(map);

Оптимизационные параметры:

  • keepBuffer — снижает количество перерисовок
  • updateWhenIdle — обновляет тайлы только при остановке перемещения
  • maxZoom — ограничивает детализацию

Использование слоёв и модульной архитектуры

Vite способствует разделению логики на модули, что удобно для работы с слоями карты.

Пример организации:

src/
  map/
    createMap.js
    layers.js
    markers.js

createMap.js:

import L from 'leaflet';

export function createMap(containerId) {
  return L.map(containerId).setView([51.505, -0.09], 13);
}

layers.js:

import L from 'leaflet';

export function createBaseLayer() {
  return L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
    maxZoom: 19
  });
}

Такой подход упрощает масштабирование приложения.


Производственная сборка и base path

Vite при сборке может размещать приложение не в корне домена. Leaflet требует корректной настройки base path для ассетов.

В vite.config.js:

import { defineConfig } from 'vite';

export default defineConfig({
  base: '/app/'
});

Если используется кастомный base, важно учитывать это при загрузке ресурсов из public.


Ленивая загрузка картографических модулей

Leaflet может быть вынесен в динамический импорт для уменьшения начального бандла.

async function initMap() {
  const L = await import('leaflet');
  await import('leaflet/dist/leaflet.css');

  const map = L.map('map').setView([51.505, -0.09], 13);

  L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
}

Vite автоматически создаёт отдельный chunk для Leaflet, уменьшая размер initial bundle.


Интеграция с переменными окружения

Vite использует .env файлы с префиксом VITE_.

Пример:

VITE_TILE_URL=https://tile.openstreetmap.org/{z}/{x}/{y}.png

Использование:

const tileUrl = import.meta.env.VITE_TILE_URL;

L.tileLayer(tileUrl).addTo(map);

Это позволяет переключать источники тайлов между окружениями без изменения кода.


Обработка SSR-ограничений

Leaflet зависит от браузерного DOM и не может выполняться на сервере. При использовании SSR-фреймворков с Vite (например, Nuxt или custom SSR) требуется изоляция:

if (typeof window !== 'undefined') {
  const L = await import('leaflet');
}

Или полное отключение выполнения на сервере.


Масштабирование и производительность в Vite-сборке

При увеличении количества маркеров и слоёв важны следующие аспекты:

  • использование кластеризации (leaflet.markercluster)
  • минимизация перерисовок
  • разделение слоёв по функциональности
  • отказ от избыточных реактивных обновлений в UI-фреймворках

Пример кластеризации:

import 'leaflet.markercluster';

const markers = L.markerClusterGroup();

markers.addLayer(L.marker([51.5, -0.09]));
markers.addTo(map);

Типизация и работа с TypeScript

Leaflet в TypeScript требует явного указания типов для карты и слоёв.

import L, { Map } from 'leaflet';

let map: Map;

map = L.map('map').setView([51.505, -0.09], 13);

Типы помогают избежать ошибок при работе с событиями и кастомными слоями:

map.on('click', (e: L.LeafletMouseEvent) => {
  console.log(e.latlng);
});

Расширение функциональности через плагины

Экосистема Leaflet включает множество плагинов, совместимых с Vite без дополнительной конфигурации:

  • рисование геометрии
  • маршрутизация
  • тепловые карты
  • геокодинг

Пример подключения heatmap:

import 'leaflet.heat';

L.heatLayer([[51.5, -0.09, 0.5]]).addTo(map);

Управление состоянием карты в модульной системе

При построении сложных приложений карта рассматривается как независимый сервис:

  • инициализация централизована
  • слои управляются отдельно
  • события маршрутизируются через абстракции

Такой подход снижает связанность кода и упрощает интеграцию с Vite-плагинами и HMR-циклом.