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

В приложениях на основе Mapbox GL JS ключевая проблема масштабирования заключается в том, чтобы избежать «монолитного» файла конфигурации карты. Переиспользуемые компоненты позволяют разносить логику по независимым модулям: источники данных, слои, стили, контролы, поведение и утилиты.

Базовый принцип заключается в том, что каждый элемент карты описывается как функция или класс, принимающий map и возвращающий объект с методами управления жизненным циклом:

export function createTrafficLayer(map) {
  const id = 'traffic-layer';

  function add() {
    if (map.getLayer(id)) return;

    map.addLayer({
      id,
      type: 'line',
      source: 'traffic-source',
      paint: {
        'line-color': '#ff0000',
        'line-width': 2
      }
    });
  }

  function remove() {
    if (map.getLayer(id)) {
      map.removeLayer(id);
    }
  }

  return { add, remove, id };
}

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

Инкапсуляция источников данных

Источники данных (sources) в Mapbox GL JS часто становятся точкой повторного использования, особенно при работе с GeoJSON или векторными тайлами.

Инкапсуляция источника позволяет стандартизировать структуру данных и обновления:

export function createGeoJsonSource(map, id, data) {
  function add() {
    if (map.getSource(id)) return;

    map.addSource(id, {
      type: 'geojson',
      data
    });
  }

  function update(newData) {
    const source = map.getSource(id);
    if (source) {
      source.setData(newData);
    }
  }

  return { add, update, id };
}

Такой компонент скрывает детали работы с map.getSource и делает API предсказуемым.

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

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

Принцип построения повторно используемого слоя:

  • фиксированная логика типа слоя (line, circle, fill)
  • параметризация стиля
  • независимость от источника
export function createLineLayer({
  id,
  source,
  color = '#000',
  width = 2
}) {
  return function attach(map) {
    if (map.getLayer(id)) return;

    map.addLayer({
      id,
      type: 'line',
      source,
      paint: {
        'line-color': color,
        'line-width': width
      }
    });

    return {
      remove() {
        if (map.getLayer(id)) {
          map.removeLayer(id);
        }
      }
    };
  };
}

Такой слой становится фабрикой, которую можно конфигурировать под разные сценарии отображения.

Контролы и расширение интерфейса карты

Контролы в Mapbox GL JS реализуются через интерфейс IControl, что делает их естественно переиспользуемыми компонентами.

Паттерн заключается в создании класса с методами onAdd и onRemove:

export class CompassControl {
  onAdd(map) {
    this.map = map;

    this.container = document.createElement('div');
    this.container.className = 'mapboxgl-ctrl compass-control';

    this.button = document.createElement('button');
    this.button.textContent = 'N';

    this.button.oncl ick = () => {
      map.resetNorth();
    };

    this.container.appendChild(this.button);
    return this.container;
  }

  onRemove() {
    this.container.parentNode.removeChild(this.container);
    this.map = undefined;
  }
}

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

Композиция компонентов через фабрики

Фабрики позволяют объединять несколько слоёв и источников в один логический модуль. Это особенно полезно для тематических карт (например, транспорт, аналитика, тепловые карты).

export function createUrbanLayerBundle(map) {
  const roads = createLineLayer({
    id: 'roads',
    source: 'roads-source',
    color: '#888',
    width: 1
  });

  const highways = createLineLayer({
    id: 'highways',
    source: 'roads-source',
    color: '#ff9900',
    width: 3
  });

  function add() {
    roads(map).remove?.();
    highways(map).remove?.();
  }

  return { add };
}

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

Унификация поведения через утилиты

Поведение карты (например, зум к объекту, фильтрация, подсветка) лучше выносить в отдельные функции, не зависящие от UI.

export function fitToFeature(map, feature) {
  const coordinates = feature.geometry.coordinates;

  const bounds = coordinates.reduce(
    (b, coord) => b.extend(coord),
    new mapboxgl.LngLatBounds(coordinates[0], coordinates[0])
  );

  map.fitBounds(bounds, {
    padding: 40
  });
}

Такие функции легко переиспользуются между разными компонентами интерфейса.

Декларативная сборка карты

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

export function initMap(map) {
  const sources = [
    createGeoJsonSource(map, 'cities', citiesData),
    createGeoJsonSource(map, 'roads', roadsData)
  ];

  const layers = [
    createLineLayer({
      id: 'roads-layer',
      source: 'roads',
      color: '#555'
    })
  ];

  sources.forEach(s => s.add());
  layers.forEach(l => l(map).remove?.());

  return { sources, layers };
}

Такой стиль упрощает масштабирование проекта и повторное использование конфигураций.

Переиспользуемые шаблоны визуализации

Визуальные паттерны (heatmap, clustering, choropleth) удобно оформлять как готовые компоненты, где меняются только данные и минимальные параметры.

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

export function createClusterLayer(map, sourceId) {
  if (!map.getSource(sourceId)) return;

  map.addLayer({
    id: 'clusters',
    type: 'circle',
    source: sourceId,
    filter: ['has', 'point_count'],
    paint: {
      'circle-color': '#51bbd6',
      'circle-radius': 18
    }
  });
}

Такой компонент можно использовать в любом проекте без адаптации логики.

Абстракция жизненного цикла компонентов

Ключевой аспект переиспользования — управление жизненным циклом. Компоненты должны поддерживать явные стадии:

  • создание
  • добавление на карту
  • обновление данных
  • удаление

Пример универсального шаблона:

export function createComponent(factory) {
  return function (map, options) {
    const instance = factory(map, options);

    return {
      mount: instance.add,
      update: instance.update,
      unmount: instance.remove
    };
  };
}

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