В процессе развития картографического приложения код редко остаётся неизменным. Появляются новые требования, изменяется структура данных, обновляются версии библиотек, возникают задачи повышения производительности и читаемости. Переписывание кода (refactoring и code rewriting) представляет собой процесс изменения внутренней структуры программы без изменения её внешнего поведения.
Для проектов на Mapbox GL JS переписывание особенно важно по нескольким причинам:
Грамотное переписывание кода позволяет уменьшить количество ошибок, упростить поддержку проекта и повысить скорость разработки новых функций.
Некоторые признаки напрямую указывают на то, что код требует переработки.
Плохой пример:
map.addLayer({
id: 'cities',
type: 'circle',
source: 'places',
filter: ['==', ['get', 'type'], 'city']
});
map.addLayer({
id: 'villages',
type: 'circle',
source: 'places',
filter: ['==', ['get', 'type'], 'village']
});
map.addLayer({
id: 'towns',
type: 'circle',
source: 'places',
filter: ['==', ['get', 'type'], 'town']
});
Одинаковая структура слоя повторяется несколько раз.
После переписывания:
function addSettlementLayer(id, type) {
map.addLayer({
id,
type: 'circle',
source: 'places',
filter: ['==', ['get', 'type'], type]
});
}
addSettlementLayer('cities', 'city');
addSettlementLayer('villages', 'village');
addSettlementLayer('towns', 'town');
Код становится компактнее и проще для изменения.
Плохой пример:
function initializeMap() {
map.addSource(...);
map.addLayer(...);
map.on(...);
map.on(...);
map.on(...);
map.on(...);
map.on(...);
map.on(...);
}
Подобные функции быстро разрастаются до нескольких сотен строк.
После переписывания:
function initializeSources() {
// Источники
}
function initializeLayers() {
// Слои
}
function initializeEvents() {
// События
}
function initializeMap() {
initializeSources();
initializeLayers();
initializeEvents();
}
Каждый блок ответственности отделён от остальных.
Одной из наиболее распространённых задач является управление слоями.
map.addLayer({
id: 'roads',
type: 'line',
source: 'transport',
paint: {
'line-color': '#ff0000',
'line-width': 2
}
});
map.addLayer({
id: 'railways',
type: 'line',
source: 'transport',
paint: {
'line-color': '#0000ff',
'line-width': 2
}
});
Структуры практически идентичны.
function createLineLayer(id, color) {
return {
id,
type: 'line',
source: 'transport',
paint: {
'line-color': color,
'line-width': 2
}
};
}
map.addLayer(createLineLayer('roads', '#ff0000'));
map.addLayer(createLineLayer('railways', '#0000ff'));
Подход позволяет централизованно изменять параметры всех линейных слоёв.
Со временем приложение начинает содержать большое количество событий.
map.on('click', 'cities', function(e) {
console.log(e.features[0]);
});
map.on('click', 'villages', function(e) {
console.log(e.features[0]);
});
map.on('click', 'towns', function(e) {
console.log(e.features[0]);
});
Код дублируется.
function handleSettlementClick(event) {
console.log(event.features[0]);
}
['cities', 'villages', 'towns']
.forEach(layer => {
map.on('click', layer, handleSettlementClick);
});
Логика сосредоточена в одном месте.
Жёстко прописанные параметры усложняют поддержку.
Плохой пример:
map.setPaintProperty(
'cities',
'circle-color',
'#00ff00'
);
map.setPaintProperty(
'cities',
'circle-radius',
8
);
После переписывания:
const CITY_STYLE = {
color: '#00ff00',
radius: 8
};
map.setPaintProperty(
'cities',
'circle-color',
CITY_STYLE.color
);
map.setPaintProperty(
'cities',
'circle-radius',
CITY_STYLE.radius
);
Теперь изменение стиля выполняется в одном месте.
Часто встречается ситуация, когда данные создаются непосредственно внутри функций.
map.addSource('places', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: [...]
}
});
const placesData = {
type: 'FeatureCollection',
features: [...]
};
map.addSource('places', {
type: 'geojson',
data: placesData
});
Логика отображения отделяется от логики хранения данных.
При большом количестве однотипных объектов удобно использовать фабричные функции.
map.addLayer({
id: 'restaurants',
type: 'symbol',
source: 'pois'
});
map.addLayer({
id: 'hotels',
type: 'symbol',
source: 'pois'
});
map.addLayer({
id: 'museums',
type: 'symbol',
source: 'pois'
});
function createSymbolLayer(id) {
return {
id,
type: 'symbol',
source: 'pois'
};
}
[
'restaurants',
'hotels',
'museums'
].forEach(layer => {
map.addLayer(createSymbolLayer(layer));
});
Современные версии JavaScript позволяют заменить вложенные цепочки вызовов более читаемыми конструкциями.
fetch('/api/places')
.then(response => response.json())
.then(data => {
map.getSource('places')
.setData(data);
})
.catch(error => {
console.error(error);
});
async function loadPlaces() {
try {
const response = await fetch('/api/places');
const data = await response.json();
map.getSource('places')
.setData(data);
}
catch (error) {
console.error(error);
}
}
Код становится линейным и легче воспринимается.
При росте приложения полезно разделять код по функциональным зонам.
Пример структуры проекта:
src/
│
├── map/
│ ├── map.js
│ ├── layers.js
│ ├── sources.js
│ └── events.js
│
├── services/
│ ├── api.js
│ └── geojson.js
│
└── app.js
Такое разделение значительно упрощает сопровождение.
map.setFilter(
'cities',
[
'all',
['==', ['get', 'country'], 'France'],
['>', ['get', 'population'], 500000]
]
);
const cityFilter = [
'all',
['==', ['get', 'country'], 'France'],
['>', ['get', 'population'], 500000]
];
map.setFilter(
'cities',
cityFilter
);
Фильтр можно переиспользовать и изменять независимо от вызова API.
if (type === 'city') {
map.setPaintProperty(
'places',
'circle-color',
'#ff0000'
);
}
if (type === 'village') {
map.setPaintProperty(
'places',
'circle-color',
'#00ff00'
);
}
if (type === 'town') {
map.setPaintProperty(
'places',
'circle-color',
'#0000ff'
);
}
const colors = {
city: '#ff0000',
village: '#00ff00',
town: '#0000ff'
};
map.setPaintProperty(
'places',
'circle-color',
colors[type]
);
Количество условий сокращается, а код становится расширяемым.
map.on('click', 'cities', function(e) {
const popup = new mapboxgl.Popup();
popup
.setLngLat(e.lngLat)
.setHTML(
'<h3>' +
e.features[0].properties.name +
'</h3>'
)
.addTo(map);
});
function showPopup(lngLat, html) {
new mapboxgl.Popup()
.setLngLat(lngLat)
.setHTML(html)
.addTo(map);
}
map.on('click', 'cities', e => {
const name =
e.features[0].properties.name;
showPopup(
e.lngLat,
`<h3>${name}</h3>`
);
});
Функция становится универсальной для любых слоёв.
setInterval(() => {
fetch('/api/data')
.then(response => response.json())
.then(data => {
map.removeSource('places');
map.addSource('places', {
type: 'geojson',
data
});
});
}, 5000);
Источник пересоздаётся каждый раз.
setInterval(async () => {
const response =
await fetch('/api/data');
const data =
await response.json();
map.getSource('places')
.setData(data);
}, 5000);
Количество операций значительно уменьшается.
Множество отдельных условий можно заменить выражениями Mapbox.
if (population > 1000000) {
color = 'red';
}
else if (population > 500000) {
color = 'orange';
}
else {
color = 'green';
}
'circle-color': [
'step',
['get', 'population'],
'green',
500000,
'orange',
1000000,
'red'
]
Логика переносится непосредственно в стиль слоя.
При обновлении библиотеки полезно выполнять систематический аудит проекта.
Основные направления миграции:
Во многих случаях миграция становится хорошей возможностью для глубокой очистки проекта от накопленного технического долга.
Эффективное переписывание обычно выполняется поэтапно:
Для проектов на Mapbox GL JS особенно важно после каждого этапа проверять:
Систематическое переписывание кода позволяет поддерживать картографическое приложение в предсказуемом, расширяемом и производительном состоянии даже при длительном развитии проекта и значительном росте объёма функциональности.