глава 3 и правки

master
gman 1 year ago
parent cda76f34f9
commit 4e4070d4f9

@ -12,6 +12,8 @@ import Question from '../../../components/Question.astro'
- клиент и сервер - клиент и сервер
- HTML, CSS, JavaScript - HTML, CSS, JavaScript
В рамках практической части создадим карту мира на основе статических GeoJSON-файлов.
## Карты в Интернете ## Карты в Интернете

@ -14,6 +14,8 @@ import Question from '../../../components/Question.astro'
- асинхронность JavaScript - асинхронность JavaScript
- запрос `fetch...then` - запрос `fetch...then`
В рамках практической части создадим карту вакансий на основе изменяющегося содержания Google-таблицы.
## API ## API
API обычно переводят как прикладной программный интерфейс или программный интерфейс приложений. На практике чаще говорят просто "апи". API обычно переводят как прикладной программный интерфейс или программный интерфейс приложений. На практике чаще говорят просто "апи".

@ -0,0 +1,351 @@
---
title: Бэкенд
---
import { Card, FileTree, LinkCard, Tabs, TabItem } from '@astrojs/starlight/components';
В этой главе
- понятия бэкенда и фронтенда
- SQL
- Flask
- CORS
В рамках практической части создадим карту индекса качества городской среды по базе данных SQLite.
## Бэкенд
Мы уже знаем, что веб-приложения можно разделить на клиентскую и серверную части. Разработку клиентской части называют фронтендом. Разработку серверной части называют бэкендом. Фронтенд общается с бэкендом через API. Бэкенд предоставляет метода API, а фронтенд к ним обращается.
Когда происходит вызов метода API -- запрос определённого URL -- выполняется соответствующая серверная функция. Для программирования серверных функций могут использоваться различные языки программирования Python, Go, Rust и даже JavaScript (NodeJS).
В предыдущем занятии мы обращались к бэкенду через API, а в этот раз разработаем бэкенд сами. Наш бэкенд мы разработаем на языке Python с использованием библиотеки Flask.
## Подготовка
Создадим папку с заготовкой для нашей карты.
Добавим туда папку `backend`. Загрузим [отсюда](https://github.com/gtitov/flask-maplibre-map/raw/refs/heads/main/backend/cities_index.sqlite) и положим туда базу данных. И создадим в этой папке файл с нашим бэкендом `app.py`.
<FileTree>
- backend/
- app.py
- cities_index.sqlite
- index.html
- style.css
- main.js
</FileTree>
<details>
<summary>Что это за база данных такая -- SQLite</summary>
SQLite -- встраиваемая база данных, которая хранит все свои данные в одном файле. В одном файле хранятся все таблицы, индексы и другая информация о базе данных, что упрощает управление и резервное копирование. Благодаря этому, она не требует отдельного сервера и легко интегрируется в различные приложения.
</details>
## Разработка бэкенда
### Установка Flask
Установим Python и после установки через терминал загрузим в Python библиотеку Flask.
```sh title=Терминал
pip install Flask
```
Теперь откроем ранее созданный нами файл `backend/app.py`. Подключим необходимые библиотеки и создадим объект `app`, в рамках которого мы будем определять доступные методы API.
```py title="app.py"
from flask import Flask, Response
import sqlite3
import json
import time
app = Flask(__name__)
DB_LOCATION = "cities_index.sqlite"
```
### Список городов по году
Добавим первый метод API. Он возвращает пользователю все города за выбранный год. При обращении к этому методу бэкенд выполняет запрос к базе данных и формирует на основе ответа GeoJSON файл, который мы сможем сразу отправить на карту.
> Мы могли бы отправить на фронтенд и неподготовленный файл. Собрать GeoJSON на клиентской стороне, как в карте вакансий, когда Google возвращал нам CSV. Но то был чужой API. А этот наш. И в нашем мы можем сделать так, как будет удобнее нам!
```py title=app.py
...
@app.route("/cities/<year>") # путь API, к которому обращается пользователь
def cities_by_year(year): # функция, которая будет выполняться при обращении
# start_time = time.time()
db = sqlite3.connect(DB_LOCATION) # подключение к базе данных
db.row_factory = sqlite3.Row # указание, что в строках мы будем сохранять название колонки и значение
cursor = db.execute("SELECT * FROM cities WHERE year = ?", (year,)) # выполняем запрос к базе, подставляя год, введённый пользователем
cities = cursor.fetchall() # забираем результат запроса
cursor.close() # закрываем запрос
db.close() # закрываем подключение
geojson = { # приводим к формату GeoJSON
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [row["longitude"], row["latitude"]],
},
"properties": dict(row),
}
for row in cities
],
}
r = Response( # формируем ответ
json.dumps(geojson, ensure_ascii=False), # ensure_ascii=False, чтобы нормально отображалась кириллица
mimetype="text/json" # указываем тип данных
)
# print("--- %s seconds ---" % (time.time() - start_time))
return r
```
Запускаем бэкенд на локальном сервере для проверки. Открываем терминал в папке `backend` и выполняем команду
```sh title=Терминал
flask run --debug
```
После чего увидим что-то вроде
```sh title=Терминал
flask run --debug
* Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
* Restarting with stat
* Debugger is active!
* Debugger PIN: 106-994-678
```
Теперь мы можем обратиться к нашему бэкенду через API по адресу [`http://127.0.0.1:5000/cities/2020`](http://127.0.0.1:5000/cities/2020).
### Сведения о городе по идентификатору
Перейдём ко второму методу. Получим подробные сведения о городе по его идентификатору.
```py title=app.py
@app.route("/city/<id>")
def city_by_id(id):
start_time = time.time()
db = sqlite3.connect(DB_LOCATION)
db.row_factory = sqlite3.Row
cursor = db.execute("SELECT * FROM cities WHERE id = ?", (id,))
city = cursor.fetchone()
cursor.close()
db.close()
r = Response(
json.dumps(dict(city), ensure_ascii=False),
mimetype="text/json",
)
# print("--- %s seconds ---" % (time.time() - start_time))
return r
```
Проверим этот метод [`http://127.0.0.1:5000/city/1000`](http://127.0.0.1:5000/city/1000).
## Разработка фронтенда
### Подключение данных и CORS
Наш бэкенд возвращает данные в формате GeoJSON, поэтому мы можем сразу подключить их в нашу карту.
```js title=main.js
map.on("load", () => {
map.addSource('cities', {
type: 'geojson',
data: "http://localhost:5000/cities/2020" // бэкенд должен быть запущен
});
map.addLayer({
'id': 'cities-layer',
'source': 'cities',
'type': 'circle',
'paint': {
'circle-stroke-width': 1,
'circle-stroke-color': '#FFFFFF',
// SELECT MIN(total_points), MAX(total_points) FROM cities
'circle-color': [
'interpolate',
['linear'],
['get', 'total_points'],
50,
'#d7191c',
150,
'#ffffbf',
250,
'#1a9641'
],
'circle-opacity': 0.8,
// SELECT DISTINCT group_name FROM cities
'circle-radius': [
"match",
['get', 'group_name'],
'Малый город', 3,
'Средний город', 6,
'Большой город', 6,
'Крупный город', 8,
'Крупнейший город', 12,
0 // остальные
]
}
});
})
```
Однако на карте мы не увидим искомых городов. Чтобы узнать почему, проверим вкладку "Сеть" в инструментах разработчика. У запроса к списку городов мы увидим надпись **Ошибка CORS**.
Механизм CORS -- Cross-Origin Resource Sharing -- призван повысить безопасность веб-страницы. Нам, чтобы избежать ошибки CORS, надо указать, что API может отвечать на запросы любых веб-страниц.
> CORS -- это история про *веб-страницы*, поэтому выполняя запросы к API напрямую мы с ней не сталкивались.
```diff lang=py title=app.py
...
@app.route("/cities/<int:year>") # путь API, к которому обращается пользователь
def cities_by_year(year): # функция, которая будет выполняться при обращении
...
r = Response( # формируем ответ
json.dumps(geojson, ensure_ascii=False), # ensure_ascii=False, чтобы нормально отображалась кириллица
mimetype="text/json", # указываем тип данных
+ headers={"Access-Control-Allow-Origin": "*"}
)
# print("--- %s seconds ---" % (time.time() - start_time))
return r
@app.route("/city/<int:id>")
def city_by_id(id):
...
r = Response(
json.dumps(dict(city), ensure_ascii=False),
mimetype="text/json",
+ headers={"Access-Control-Allow-Origin": "*"}
)
# print("--- %s seconds ---" % (time.time() - start_time))
return r
```
После добавления заголовков о том, что API может обслуживать любые веб-страницы, в ответ мы получаем наш список городов в формате GeoJSON.
### Выбор года
Дадим пользователю возможность выбирать год, за который он хочет видеть индекс городов.
Разметим элемент с выпадающем списком.
```diff lang=html title=index.html
<body>
<div id='map'></div>
<dialog id="city-details-modal" onmousedown="this.close()"></dialog>
+ <div>
+ <p class="text-center">Год</p>
+ <select id="year-selector">
+ <option value="2020" selected>2020</option>
+ <option value="2019">2019</option>
+ <option value="2018">2018</option>
+ </select>
+ </div>
<script src="main.js"></script>
</body>
```
Сделаем так, чтобы он выводился поверх карты.
```diff lang=css title=style.css
#map {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
+ z-index: -1;
}
```
Запрограммируем реакцию карты на выбор года. Асинхронный запрос за нас выполнит картографическая библиотека.
```js title=main.js
map.on("load", () => {
...
document.getElementById("year-selector").addEventListener(
'change',
(e) => {
const year = e.target.value // фиксируем выбранный год
map.getSource('cities').setData(`http://localhost:5000/cities/${year}`) // меняем источник данных
}
)
})
```
### Сведения о городе
Выведем по клику на город подробные сведения о нём. Для отображения информации используем модальное окно, то есть диалоговое окно вспывающее поверх страницы.
```diff lang=html title=index.html
<body>
<div id='map'></div>
...
+ <dialog id="city-details-modal" onmousedown="this.close()"></dialog>
<script src="main.js"></script>
</body>
```
При клике будем выполнять запрос к методу API, который возвращает подробную информацию о городе. Здесь мы работаем с асинхронностью самостоятельно.
```js title=main.js
map.on("load", () => {
...
map.on('click', 'cities-layer', (e) => {
// console.log(e.features[0].properties.id)
fetch(`http://localhost:5000/city/${e.features[0].properties.id}`)
.then(response => response.json())
.then(cityProperties => {
// console.log(cityProperties)
document.getElementById("city-details-modal").innerHTML = `<h1>${cityProperties.name}</h1>
<img src="${cityProperties.emblem_url}" height="200">
<h3>Численность населения</h3><h2>${cityProperties.people_count} тыс. чел</h2>
<h3>Индекс качества городской среды</h3><h2>${cityProperties.total_points} / 360</h2>
<hr>
<h3>Жилье и прилегающие пространства</h3><h2>${cityProperties.house_points} / 60</h2>
<h3>Озелененные пространства</h3><h2>${cityProperties.park_points} / 60</h2>
<h3>Общественно-деловая инфраструктура</h3><h2>${cityProperties.business_points} / 60</h2>
<h3>Социально-досуговая инфраструктура</h3><h2>${cityProperties.social_points} / 60</h2>
<h3>Улично-дорожная</h3><h2>${cityProperties.street_points} / 60</h2>
<h3>Общегородское пространство</h3><h2>${cityProperties.common_points} / 60</h2>`
document.getElementById("city-details-modal").showModal() // showModal() -- встроенный метод элемента <dialog>
})
})
map.on('mouseenter', 'cities-layer', () => {
map.getCanvas().style.cursor = 'pointer';
});
map.on('mouseleave', 'cities-layer', () => {
map.getCanvas().style.cursor = '';
});
})
```
> Если вы уже заметили кое-какую нестыковку в наших запросах, посмотрите упражнения в конце главы
## Упражнения
1. Создайте метод, который вернёт список доступных годов
1. Выведите модальное окно слева и выполните подлёт к точке клика
1. Вы могли заметить, что то, что мы получаем методом запроса деталей о городе уже содержится в полному списке городов: вам предлагается избавиться от этой избыточности
<details>
<summary>Есть два варианта -- подумайте над ними. Это тест на то, что вам ближе, бэкенд или фронтент</summary>
Бэкендер: можно убрать из метода для списка городов лишние атрибуты<br/>Фронтендер: на клик по объекту можно не обращаться к серверу, а использовать данные из атрибутов объекта
</details>
## Чтение
1. Что такое CORS / Дока [ссылка](https://doka.guide/tools/cors/)
1. Безопасность веб-приложений и распространённые атаки / Дока [ссылка](https://doka.guide/tools/web-security/)

@ -13,3 +13,5 @@ title: Внеклассное чтение
Отдельно стоит упомянуть геопортал и картографические веб-сервисы. Их мы формируем от родовых IT-терминов приземляя к картографии и пространственным данным. *Геопортал* — это веб-ресурс, предоставляющий доступ к каталогам пространственных данных, наборам пространственных данных, веб-сервисам, публикующим пространственные данные. *Картографический веб-сервис* — это веб-ресурс, предоставляющий возможности обращения к пространственным данным или метаданным, в т. ч. по стандартизированным протоколам обмена (WMS, WFS, WCS и т. д.). Картографические веб-сервисы обычно не имеют графического интерфейса пользователя, к этим сервисам обращаются программно через API (прикладной программный интерфейс). Отдельно стоит упомянуть геопортал и картографические веб-сервисы. Их мы формируем от родовых IT-терминов приземляя к картографии и пространственным данным. *Геопортал* — это веб-ресурс, предоставляющий доступ к каталогам пространственных данных, наборам пространственных данных, веб-сервисам, публикующим пространственные данные. *Картографический веб-сервис* — это веб-ресурс, предоставляющий возможности обращения к пространственным данным или метаданным, в т. ч. по стандартизированным протоколам обмена (WMS, WFS, WCS и т. д.). Картографические веб-сервисы обычно не имеют графического интерфейса пользователя, к этим сервисам обращаются программно через API (прикладной программный интерфейс).
Этими терминами злоупотребляют по отношению к любым веб-ресурсам, связанным с пространственными данными и картами. Картографические продукты, публикуемые в сети, правильнее объединить под названием *картографические веб-ресурсы*, так как они являются веб-ресурсами, основным назначением которых является предоставление доступа к картографической информации. Этими терминами злоупотребляют по отношению к любым веб-ресурсам, связанным с пространственными данными и картами. Картографические продукты, публикуемые в сети, правильнее объединить под названием *картографические веб-ресурсы*, так как они являются веб-ресурсами, основным назначением которых является предоставление доступа к картографической информации.
<!-- ## Стиль программирования карты -->
Loading…
Cancel
Save