Организация кода

Организация кода в приложениях на Mapbox GL JS напрямую влияет на масштабируемость, производительность и поддерживаемость. Даже при небольшом объёме логики карта быстро превращается в центральный компонент приложения, вокруг которого выстраиваются источники данных, слои, взаимодействия и бизнес-логика.

Типовая ошибка — размещение всей логики инициализации карты в одном файле. Такой подход допустим только для демонстрационных примеров. В реальных проектах карта становится частью архитектуры, где ответственность распределяется по слоям: инициализация, конфигурация, работа с данными, UI-взаимодействия, управление состоянием.


Базовое разделение ответственности

Код приложения на Mapbox GL JS обычно делится на несколько логических зон:

  • инициализация карты
  • управление источниками данных (sources)
  • управление слоями (layers)
  • обработка событий
  • бизнес-логика (фильтрация, состояние, вычисления)
  • визуальные компоненты интерфейса

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


Инициализация карты как отдельный модуль

Создание экземпляра карты не должно смешиваться с добавлением слоёв и источников. Базовая инициализация выносится в отдельный модуль.

import mapboxgl from "mapbox-gl";

export function createMap(containerId) {
  return new mapboxgl.Map({
    container: containerId,
    style: "mapbox://styles/mapbox/streets-v12",
    center: [37.6173, 55.7558],
    zoom: 10
  });
}

Такой подход позволяет:

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

Конфигурация как отдельный слой абстракции

Жёстко зашитые параметры ухудшают сопровождаемость. Все параметры карты, стили и ключи доступа выносятся в конфигурационные модули.

export const MAP_CONFIG = {
  style: "mapbox://styles/mapbox/light-v11",
  center: [37.6173, 55.7558],
  zoom: 9
};

export const API_CONFIG = {
  token: "YOUR_MAPBOX_TOKEN"
};

Дальнейшее расширение системы становится проще, поскольку логика не зависит от конкретных значений.


Управление источниками данных (Sources)

Источники данных в Mapbox GL JS являются фундаментом всей визуализации. Их добавление должно быть централизованным.

export function addCitySource(map) {
  map.addSource("cities", {
    type: "geojson",
    data: "/data/cities.geojson"
  });
}

В более сложных приложениях источники группируются:

/sources
  geojsonSources.js
  vectorSources.js
  rasterSources.js

Такое разделение снижает когнитивную нагрузку и упрощает поиск нужного источника.


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

Слои представляют визуальную интерпретацию данных и должны быть отделены от источников.

export function addCityLayer(map) {
  map.addLayer({
    id: "city-points",
    type: "circle",
    source: "cities",
    paint: {
      "circle-radius": 6,
      "circle-color": "#3b82f6"
    }
  });
}

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

/layers
  cityLayers.js
  heatmapLayers.js
  routeLayers.js

Такой подход позволяет:

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

Композиция модулей карты

Инициализация карты должна быть точкой сборки, а не местом хранения логики.

import { createMap } from "./map/createMap";
import { addCitySource } from "./sources/cities";
import { addCityLayer } from "./layers/cityLayers";

export function initApp() {
  const map = createMap("map");

  map.on("load", () => {
    addCitySource(map);
    addCityLayer(map);
  });

  return map;
}

Такой подход обеспечивает:

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

Разделение бизнес-логики и Mapbox-логики

Критическая ошибка — смешивание вычислений и визуализации. Бизнес-логика должна быть независимой от Mapbox GL JS.

Пример неправильного подхода:

map.setFilter("cities", ["==", ["get", "population"], 1000]);

Вместо этого фильтры должны формироваться в отдельном модуле:

export function getPopulationFilter(min) {
  return ["all", [">=", ["get", "population"], min]];
}

И применяться уже в слое взаимодействия:

import { getPopulationFilter } from "./filters/population";

map.setFilter("cities", getPopulationFilter(1000));

Слой управления состоянием

При росте приложения появляется необходимость централизованного состояния: выбранные объекты, активные фильтры, текущие режимы отображения.

Простейшая модель:

export const state = {
  selectedCity: null,
  filters: {
    populationMin: 0
  }
};

Обновление состояния отделяется от Mapbox-операций:

export function setSelectedCity(city) {
  state.selectedCity = city;
}

Реакция Mapbox-кода на состояние:

import { state } from "./state";

export function updateCityFilter(map) {
  map.setFilter(
    "cities",
    [">=", ["get", "population"], state.filters.populationMin]
  );
}

Работа с событиями карты

События Mapbox GL JS должны обрабатываться в отдельном модуле, а не в инициализации карты.

export function bindMapEvents(map) {
  map.on("click", "city-points", (e) => {
    const feature = e.features[0];
    console.log(feature.properties.name);
  });

  map.on("mouseenter", "city-points", () => {
    map.getCanvas().style.cursor = "pointer";
  });

  map.on("mouseleave", "city-points", () => {
    map.getCanvas().style.cursor = "";
  });
}

Такой подход позволяет:

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

Организация директорий проекта

Типовая структура проекта:

/src
  /map
    createMap.js
    config.js

  /sources
    cities.js
    routes.js

  /layers
    cityLayers.js
    routeLayers.js

  /events
    mapEvents.js
    uiEvents.js

  /state
    index.js

  /filters
    population.js

  /utils
    geo.js

  initApp.js

Такая структура отражает архитектурное разделение ответственности, а не техническую группировку файлов.


Работа с асинхронными данными

Источники данных часто загружаются динамически. Логика загрузки не должна быть связана с визуализацией.

export async function loadCities() {
  const response = await fetch("/api/cities");
  return response.json();
}

Подключение к карте отделяется:

import { loadCities } from "./data/loadCities";

export async function addDynamicCities(map) {
  const data = await loadCities();

  map.addSource("cities", {
    type: "geojson",
    data
  });
}

Переиспользуемые абстракции

С ростом приложения появляются повторяющиеся операции:

  • добавление источников
  • создание слоёв
  • регистрация событий

Эти операции выносятся в утилиты:

export function addGeoJsonSource(map, id, data) {
  map.addSource(id, {
    type: "geojson",
    data
  });
}
export function addCircleLayer(map, id, source, paint) {
  map.addLayer({
    id,
    type: "circle",
    source,
    paint
  });
}

Масштабирование архитектуры

При увеличении сложности приложения Mapbox GL JS часто становится только визуальным рендерером, а вся логика уходит в отдельные слои:

  • data layer (API, кеширование, трансформации)
  • domain layer (бизнес-правила)
  • presentation layer (Mapbox GL JS)
  • interaction layer (события и UI)

Такое разделение снижает связанность компонентов и упрощает тестирование.


Типизация и строгие контракты

При использовании TypeScript структура кода становится более предсказуемой.

export interface City {
  id: string;
  name: string;
  population: number;
  coordinates: [number, number];
}

Типизация источников:

import { City } from "../types";

export function createCitySource(data: City[]) {
  return {
    type: "geojson",
    data
  };
}

Изоляция Mapbox-специфичного кода

Код, связанный с Mapbox GL JS, должен быть максимально локализован. Остальная часть системы не должна знать о:

  • слоях
  • источниках
  • фильтрах Mapbox
  • событиях карты

Это достигается через адаптеры:

export function setCitiesVisibility(map, visible) {
  map.setLayoutProperty(
    "city-points",
    "visibility",
    visible ? "visible" : "none"
  );
}

Такой слой абстракции защищает архитектуру от привязки к конкретной библиотеке Mapbox GL JS и упрощает потенциальную миграцию.


Связь с платформой Mapbox

Экосистема тесно связана с платформой Mapbox, предоставляющей стили, тайлы и API. Это накладывает дополнительные требования к структуре кода:

  • управление токенами доступа отдельно от логики карты
  • разделение пользовательских и платформенных стилей
  • изоляция API-слоя от UI-слоя

Такое разделение снижает зависимость приложения от внешней инфраструктуры и упрощает поддержку в долгосрочной перспективе