Разделение логики и представления в картографических веб-приложениях становится критически важным по мере роста сложности интерактивных карт, увеличения числа слоёв, источников данных и пользовательских сценариев. В экосистеме Mapbox GL JS это разделение позволяет удерживать управляемость кода, минимизировать связанность компонентов и обеспечить предсказуемость поведения карты при изменении состояния приложения.
Ключевая идея заключается в том, что карта и её визуализация рассматриваются как слой представления, тогда как обработка данных, бизнес-правила, состояние и взаимодействие с внешними API выносятся в отдельные сущности, не зависящие от API рендеринга карты.
Представление в контексте Mapbox GL JS ограничивается задачами визуализации:
Mapbox GL JS фактически выступает как декларативный движок рендеринга, где состояние визуализации описывается через стиль, источники данных и слои. Это создаёт естественное разделение: карта не должна содержать бизнес-логику, она лишь отражает входное состояние.
Пример роли представления:
map.addSource('cities', {
type: 'geojson',
data: '/api/cities'
});
map.addLayer({
id: 'cities-layer',
type: 'circle',
source: 'cities',
paint: {
'circle-radius': 6,
'circle-color': '#ff5500'
}
});
В данном случае карта лишь отображает источник cities,
не определяя, откуда он берётся и как формируется.
Логика приложения включает:
Эта часть не должна зависеть от API карты. Она работает с абстракциями данных, а не с конкретными слоями или стилями.
Пример логического слоя:
class CityService {
async fetchCities() {
const response = await fetch('/api/cities');
const data = await response.json();
return data.features.map(feature => ({
id: feature.id,
name: feature.properties.name,
coordinates: feature.geometry.coordinates
}));
}
}
Здесь отсутствует любая привязка к Mapbox GL JS. Это позволяет переиспользовать сервис в других визуализациях.
Одним из ключевых механизмов разделения становится введение промежуточного состояния приложения (state layer). Оно выступает мостом между логикой и представлением.
Состояние может включать:
Пример состояния:
const state = {
cities: [],
selectedCityId: null,
filters: {
populationMin: 100000
}
};
Изменение состояния не должно напрямую манипулировать картой. Вместо этого оно должно инициировать синхронизацию.
Слой синхронизации отвечает за перевод состояния приложения в команды Mapbox GL JS:
Пример:
function syncCitiesLayer(map, state) {
const filtered = state.cities.filter(city =>
city.population >= state.filters.populationMin
);
const geojson = {
type: 'FeatureCollection',
features: filtered.map(city => ({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: city.coordinates
},
properties: {
id: city.id
}
}))
};
const source = map.getSource('cities');
source.setData(geojson);
}
Этот слой не содержит бизнес-логики фильтрации — она может быть вынесена отдельно, но здесь показан принцип передачи уже подготовленных данных в карту.
При использовании MVC-подхода:
Контроллер связывает пользовательские действия с изменением состояния.
class MapController {
constructor(map, service) {
this.map = map;
this.service = service;
}
async init() {
const cities = await this.service.fetchCities();
this.updateState({ cities });
this.render();
}
updateState(partial) {
this.state = { ...this.state, ...partial };
}
render() {
syncCitiesLayer(this.map, this.state);
}
}
Контроллер не содержит деталей рендеринга, он только координирует взаимодействие.
Mapbox GL JS предоставляет богатую систему событий, однако их обработка не должна смешиваться с логикой рендера.
Типичный подход:
map.on('click', 'cities-layer', (e) => {
const cityId = e.features[0].properties.id;
controller.selectCity(cityId);
});
Контроллер:
selectCity(id) {
this.state.selectedCityId = id;
this.render();
}
Таким образом, карта не принимает решений — она лишь сообщает о событиях.
Работа со слоями в Mapbox GL JS часто становится источником тесной связанности кода. Разделение достигается через слой абстракции:
class CitiesLayer {
constructor(map) {
this.map = map;
this.sourceId = 'cities';
this.layerId = 'cities-layer';
}
setData(data) {
this.map.getSource(this.sourceId).setData(data);
}
setVisibility(visible) {
this.map.setLayoutProperty(
this.layerId,
'visibility',
visible ? 'visible' : 'none'
);
}
}
Такой подход позволяет изолировать API Mapbox GL JS от остальной логики приложения.
Стили в Mapbox GL JS часто ошибочно рассматриваются как часть логики. Однако они должны оставаться исключительно частью представления.
Неправильный подход:
if (city.population > 1000000) {
map.setPaintProperty('cities-layer', 'circle-color', 'red');
}
Правильный подход:
const enriched = cities.map(city => ({
...city,
category: city.population > 1000000 ? 'large' : 'small'
}));
map.setPaintProperty('cities-layer', 'circle-color', [
'match',
['get', 'category'],
'large', '#ff0000',
'small', '#00aaff',
'#cccccc'
]);
Таким образом, карта не принимает решений, а лишь отображает заранее вычисленные признаки.
При росте сложности приложений на Mapbox GL JS состояние становится центральной точкой управления:
Типовая структура состояния:
const state = {
map: {
center: [0, 0],
zoom: 3
},
data: {
cities: [],
routes: []
},
ui: {
selected: null,
filters: {}
}
};
Любое изменение состояния инициирует пересборку представления, но не прямое вмешательство в карту.
Однонаправленный поток данных снижает количество ошибок синхронизации:
Этот поток исключает сценарии, где карта напрямую модифицирует состояние приложения.
API Mapbox может изменяться, поэтому прямое использование его в бизнес-логике создаёт хрупкость системы.
Решение — адаптер:
class MapAdapter {
constructor(map) {
this.map = map;
}
updatePoints(sourceId, geojson) {
this.map.getSource(sourceId).setData(geojson);
}
setLayerVisibility(layerId, visible) {
this.map.setLayoutProperty(
layerId,
'visibility',
visible ? 'visible' : 'none'
);
}
}
Бизнес-логика работает с адаптером, а не с Mapbox GL JS напрямую.
При увеличении количества слоёв, источников и интерактивных элементов становится критичным:
map.* в бизнес-логике;Такой подход обеспечивает устойчивую архитектуру даже при сложных сценариях: динамические фильтры, real-time данные, мультилэйерные визуализации и взаимодействие с внешними API.