Асинхронный Python для веб-разработки

Асинхронное программирование хорошо подходит для задач, которые включают частое чтение и запись файлов или отправку данных с сервера и обратно. Асинхронные программы выполняют операции ввода-вывода неблокирующим образом, что означает, что они могут выполнять другие задачи, ожидая возврата данных от клиента, а не просто ждать, тратя ресурсы и время. Python, как и многие другие языки, по умолчанию не является асинхронным. К счастью, быстрые изменения в мире ИТ позволяют нам

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

Python, как и многие другие языки, по умолчанию не является асинхронным. К счастью, быстрые изменения в мире ИТ позволяют нам писать асинхронный код даже с использованием языков, которые изначально не предназначались для этого. С годами требования к скорости превышают возможности оборудования, и компании по всему миру объединились с Reactive Manifesto для решения этой проблемы.

Неблокирующее поведение асинхронных программ может привести к значительному увеличению производительности в контексте веб-приложения, помогая решить проблему разработки реактивных приложений.

В Python 3 собраны несколько мощных инструментов для написания асинхронных приложений. В этой статье мы рассмотрим некоторые из этих инструментов, особенно в том, что касается веб-разработки.

Мы будем разрабатывать простое реактивное приложение на основе aiohttp для отображения текущих соответствующих небесных координат планет Солнечной системы с учетом географических координат пользователя. Вы можете найти приложение здесь , а исходный код - здесь .

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

Введение в асинхронный Python

Для тех, кто знаком с написанием традиционного кода Python, переход к асинхронному коду может быть концептуально немного сложным. Асинхронный код в Python полагается на сопрограммы , которые в сочетании с циклом событий позволяют писать код, который может казаться выполняющим более одного действия одновременно.

Сопрограммы можно рассматривать как функции, у которых есть точки в коде, где они возвращают управление программой вызывающему контексту. Эти точки «доходности» позволяют приостанавливать и возобновлять выполнение сопрограмм в дополнение к обмену данными между контекстами.

Цикл событий решает, какой фрагмент кода запускается в любой момент - он отвечает за приостановку, возобновление и обмен данными между сопрограммами. Это означает, что части разных сопрограмм могут в конечном итоге выполняться в порядке, отличном от того, в котором они были запланированы. Идея выполнения различных фрагментов кода в произвольном порядке называется параллелизмом .

Размышление о параллелизме в контексте выполнения HTTP запросов может прояснить ситуацию. Представьте, что вы хотите сделать много независимых запросов к серверу. Например, мы можем запросить веб-сайт, чтобы получить статистику обо всех спортивных игроках в данном сезоне.

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

Иногда эти операции могут занимать даже несколько секунд. Приложение может испытывать сетевую задержку из-за большого количества пользователей или просто из-за ограничений скорости данного сервера.

Что, если бы наш код мог делать другие вещи, ожидая ответа от сервера? Более того, что, если он вернется к обработке данного запроса только после получения данных ответа? Мы могли бы сделать много запросов в быстрой последовательности, если бы нам не приходилось ждать завершения каждого отдельного запроса, прежде чем переходить к следующему в списке.

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

asyncio

asyncio , часть стандартной библиотеки Python, предоставляет цикл обработки событий и набор инструментов для управления им. С помощью asyncio мы можем планировать выполнение сопрограмм и создавать новые сопрограммы (на самом asyncio.Task объекты asyncio.Task, используя язык asyncio ), которые завершат выполнение только после завершения выполнения составных сопрограмм.

В отличие от других языков асинхронного программирования, Python не заставляет нас использовать цикл событий, поставляемый с языком. Как указывает Бретт Кэннон , сопрограммы Python представляют собой асинхронный API, с помощью которого мы можем использовать любой цикл событий. Существуют проекты, которые реализуют совершенно другой цикл обработки событий, например curio , или позволяют отбрасывать другую политику цикла событий для asyncio (политика цикла событий - это то, что управляет циклом событий «за кулисами»), например uvloop .

Давайте посмотрим на фрагмент кода, который одновременно запускает две сопрограммы, каждая из которых выводит сообщение через одну секунду:

 # example1.py 
 import asyncio 
 
 async def wait_around(n, name): 
 for i in range(n): 
 print(f"{name}: iteration {i}") 
 await asyncio.sleep(1.0) 
 
 async def main(): 
 await asyncio.gather(*[ 
 wait_around(2, "coroutine 0"), wait_around(5, "coroutine 1") 
 ]) 
 
 loop = asyncio.get_event_loop() 
 loop.run_until_complete(main()) 

 [email protected] :~$ time python example1.py 
 coroutine 1: iteration 0 
 coroutine 0: iteration 0 
 coroutine 1: iteration 1 
 coroutine 0: iteration 1 
 coroutine 1: iteration 2 
 coroutine 1: iteration 3 
 coroutine 1: iteration 4 
 
 real 0m5.138s 
 user 0m0.111s 
 sys 0m0.019s 

Этот код выполняется примерно за 5 секунд, поскольку asyncio.sleep устанавливает точки, в которых цикл событий может перейти к выполнению другого кода. Более того, мы сказали циклу событий запланировать оба wait_around для одновременного выполнения с функцией asyncio.gather

asyncio.gather принимает список «ожидающих» (т. е. сопрограмм или asyncio.Task ) и возвращает единственный asyncio.Task который завершается только тогда, когда завершены все его составляющие задачи / сопрограммы. Последние две строки представляют asyncio шаблон asyncio для запуска данной сопрограммы до ее завершения.

Сопрограммы, в отличие от функций, не начинают выполняться сразу после вызова. await - это то, что сообщает циклу событий запланировать выполнение сопрограммы.

Если вытаскивает await перед asyncio.sleep , программа заканчивается (почти) сразу же , как мы не сказали циклу событий на самом деле выполнить сопрограмму, который в данном случае говорит о сопрограмме для паузы в течение заданного количества время.

Поняв, как выглядит асинхронный код Python, давайте перейдем к асинхронной веб-разработке.

Установка aiohttp

aiohttp - это библиотека Python для выполнения асинхронных HTTP запросов. Кроме того, он обеспечивает основу для сборки серверной части веб-приложения. Используя Python 3.5+ и pip, мы можем установить aiohttp :

 pip install --user aiohttp 

На стороне клиента: отправка запросов

В следующих примерах показано, как с помощью aiohttp загрузить HTML-содержимое веб-сайта example.com:

 # example2_basic_aiohttp_request.py 
 import asyncio 
 import aiohttp 
 
 async def make_request(): 
 url = "https://example.com" 
 print(f"making request to {url}") 
 async with aiohttp.ClientSession() as session: 
 async with session.get(url) as resp: 
 if resp.status == 200: 
 print(await resp.text()) 
 
 loop = asyncio.get_event_loop() 
 loop.run_until_complete(make_request()) 

Несколько моментов, на которые следует обратить внимание:

  • Как и в случае с await asyncio.sleep мы должны использовать await с resp.text() , чтобы получить HTML-содержимое страницы. Если бы мы его не использовали, результат нашей программы был бы примерно таким:
1
<!-- -->
 [email protected] :~$ python example2_basic_aiohttp_request.py 
 <coroutine object ClientResponse.text at 0x7fe64e574ba0> 
  • async with - это менеджер контекста, который работает с сопрограммами вместо функций. В обоих случаях, когда он используется, мы можем представить, что внутри aiohttp закрывает соединения с серверами или иным образом освобождает ресурсы.

  • aiohttp.ClientSession имеет методы, соответствующие HTTP- глаголам. В то же самое
    способ, которым session.get выполняет запрос GET , session.post отправляет запрос POST.

Этот пример сам по себе не дает преимущества в производительности по сравнению с синхронными HTTP-запросами. Настоящая красота клиентского aiohttp заключается в выполнении нескольких одновременных запросов:

 # example3_multiple_aiohttp_request.py 
 import asyncio 
 import aiohttp 
 
 async def make_request(session, req_n): 
 url = "https://example.com" 
 print(f"making request {req_n} to {url}") 
 async with session.get(url) as resp: 
 if resp.status == 200: 
 await resp.text() 
 
 async def main(): 
 n_requests = 100 
 async with aiohttp.ClientSession() as session: 
 await asyncio.gather( 
 *[make_request(session, i) for i in range(n_requests)] 
 ) 
 
 loop = asyncio.get_event_loop() 
 loop.run_until_complete(main()) 

Вместо того, чтобы делать каждый запрос последовательно, мы просим asyncio выполнять их одновременно с asycio.gather .

Веб-приложение PlanetTracker

В ходе этого раздела я намерен продемонстрировать, как собрать приложение, которое сообщает текущие координаты планет в небе в местоположении пользователя (эфемериды).

Пользователь указывает свое местоположение с помощью веб- API геолокации , который выполняет всю работу за нас.

В конце я покажу, как настроить Procfile для развертывания приложения на Heroku . Если вы планируете продолжить, пока я работаю над сборкой приложения, вы должны сделать следующее, предполагая, что у вас установлены Python 3.6 и pip:

 [email protected] :~$ mkdir planettracker && cd planettracker 
 [email protected] :~/planettracker$ pip install --user pipenv 
 [email protected] :~/planettracker$ pipenv --python=3 

Планета Эфемериды с PyEphem

Эфемериды астрономического объекта - это его текущее положение на небе в данном месте и в определенное время на Земле. PyEphem - это библиотека Python, которая позволяет точно вычислять эфемериды.

Он особенно хорошо подходит для поставленной задачи, так как в нем есть обычные астрономические объекты, приготовленные в библиотеке. Сначала установим PyEphem :

 [email protected] :~/planettracker$ pipenv install ephem 

Получить текущие координаты Марса так же просто, как использовать экземпляр класса Observer compute его координат:

 import ephem 
 import math 
 convert = math.pi / 180. 
 mars = ephem.Mars() 
 greenwich = ephem.Observer() 
 greenwich.lat = "51.4769" 
 greenwich.lon = "-0.0005" 
 mars.compute(observer) 
 az_deg, alt_deg = mars.az*convert, mars.alt*convert 
 print(f"Mars' current azimuth and elevation: {az_deg:.2f} {alt_deg:.2f}") 

Чтобы упростить получение эфемерид планет, давайте PlanetTracker класс PlanetTracker с методом, который возвращает текущий азимит и высоту данной планеты в градусах ( PyEphem по умолчанию использует радианы, а не градусы для внутреннего представления углов):

 # planet_tracker.py 
 import math 
 import ephem 
 
 class PlanetTracker(ephem.Observer): 
 
 def __init__(self): 
 super(PlanetTracker, self).__init__() 
 self.planets = { 
 "mercury": ephem.Mercury(), 
 "venus": ephem.Venus(), 
 "mars": ephem.Mars(), 
 "jupiter": ephem.Jupiter(), 
 "saturn": ephem.Saturn(), 
 "uranus": ephem.Uranus(), 
 "neptune": ephem.Neptune() 
 } 
 
 def calc_planet(self, planet_name, when=None): 
 convert = 180./math.pi 
 if when is None: 
 when = ephem.now() 
 
 self.date = when 
 if planet_name in self.planets: 
 planet = self.planets[planet_name] 
 planet.compute(self) 
 return { 
 "az": float(planet.az)*convert, 
 "alt": float(planet.alt)*convert, 
 "name": planet_name 
 } 
 else: 
 raise KeyError(f"Couldn't find {planet_name} in planets dict") 

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

 from planet_tracker import PlanetTracker 
 tracker = PlanetTracker() 
 tracker.lat = "51.4769" 
 tracker.lon = "-0.0005" 
 tracker.calc_planet("mars") 

Выполнение этого фрагмента кода даст:

 {'az': 92.90019644871396, 'alt': -23.146670983905302, 'name': 'mars'} 

Серверный aiohttp: HTTP-маршруты

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

Прежде чем мы сможем начать писать код, мы должны подумать о том, какие HTTP-команды мы хотим связать с каждой из этих задач. Для первой задачи имеет смысл использовать POST , так как мы устанавливаем географические координаты наблюдателя. Учитывая, что мы получаем эфемериды, имеет смысл использовать GET для второй задачи:

 # aiohttp_app.py 
 from aiohttp import web 
 
 from planet_tracker import PlanetTracker 
 
 
 @routes.get("/planets/{name}") 
 async def get_planet_ephmeris(request): 
 planet_name = request.match_info['name'] 
 data = request.query 
 try: 
 geo_location_data = { 
 "lon": str(data["lon"]), 
 "lat": str(data["lat"]), 
 "elevation": float(data["elevation"]) 
 } 
 except KeyError as err: 
 # default to Greenwich Observatory 
 geo_location_data = { 
 "lon": "-0.0005", 
 "lat": "51.4769", 
 "elevation": 0.0, 
 } 
 print(f"get_planet_ephmeris: {planet_name}, {geo_location_data}") 
 tracker = PlanetTracker() 
 tracker.lon = geo_location_data["lon"] 
 tracker.lat = geo_location_data["lat"] 
 tracker.elevation = geo_location_data["elevation"] 
 planet_data = tracker.calc_planet(planet_name) 
 return web.json_response(planet_data) 
 
 
 app = web.Application() 
 app.add_routes(routes) 
 
 web.run_app(app, host="localhost", port=8000) 

Здесь route.get указывает, что мы хотим, get_planet_ephmeris сопрограмма get_planet_ephmeris была обработчиком для маршрута GET

Прежде чем запустить это, давайте установим aiohttp с помощью pipenv:

 [email protected] :~/planettracker$ pipenv install aiohttp 

Теперь мы можем запустить наше приложение:

 [email protected] :~/planettracker$ pipenv run python aiohttp_app.py 

Когда мы запускаем это, мы можем указать нашему браузеру разные маршруты, чтобы увидеть данные, которые возвращает наш сервер. Если я localhost:8000/planets/mars в адресную строку браузера, я должен увидеть следующий ответ:

 {"az": 98.72414165963292, "alt": -18.720718647020792, "name": "mars"} 

Это то же самое, что и следующая команда curl:

 [email protected] :~$ curl localhost:8000/planets/mars 
 {"az": 98.72414165963292, "alt": -18.720718647020792, "name": "mars"} 

Если вы не знакомы с curl , это удобный инструмент командной строки, в том числе для тестирования ваших HTTP-маршрутов.

Мы можем предоставить URL-адрес GET для curl :

 [email protected] :~$ curl localhost:8000/planets/mars 
 {"az": 98.72414165963292, "alt": -18.720718647020792, "name": "mars"} 

Это дает нам эфемериды Марса в Гринвичской обсерватории в Великобритании.

Мы можем закодировать координаты в URL-адресе GET чтобы мы могли получить эфемериды Марса в других местах (обратите внимание на кавычки вокруг URL-адреса):

 [email protected] :~$ curl "localhost:8000/planets/mars?lon=145.051&lat=-39.754&elevation=0" 
 {"az": 102.30273048280189, "alt": 11.690380174890928, "name": "mars" 

curl также можно использовать для выполнения запросов POST:

 [email protected] :~$ curl --header "Content-Type: application/x-www-form-urlencoded" --data "lat=48.93&lon=2.45&elevation=0" localhost:8000/geo_location 
 {"lon": "2.45", "lat": "48.93", "elevation": 0.0} 

Обратите внимание, что, предоставляя поле --data curl автоматически предполагает, что мы делаем запрос POST.

Прежде чем мы продолжим, я должен отметить, что web.run_app запускает наше приложение блокирующим образом. Это определенно не то, чего мы хотим достичь!

Чтобы запустить его одновременно, нам нужно добавить еще немного кода:

 # aiohttp_app.py 
 import asyncio 
 ... 
 
 # web.run_app(app) 
 
 async def start_app(): 
 runner = web.AppRunner(app) 
 await runner.setup() 
 site = web.TCPSite( 
 runner, parsed.host, parsed.port) 
 await site.start() 
 print(f"Serving up app on {parsed.host}:{parsed.port}") 
 return runner, site 
 
 loop = asyncio.get_event_loop() 
 runner, site = loop.run_until_complete(start_async_app()) 
 try: 
 loop.run_forever() 
 except KeyboardInterrupt as err: 
 loop.run_until_complete(runner.cleanup()) 

Обратите внимание на наличие loop.run_forever вместо вызова loop.run_until_complete который мы видели ранее. Вместо выполнения заданного количества сопрограмм мы хотим, чтобы наша программа запускала сервер, который будет обрабатывать запросы, пока мы не выйдем с помощью ctrl+c , после чего она корректно завершит работу сервера.

Клиент HTML / JavaScript

aiohttp позволяет нам обслуживать файлы HTML и JavaScript. Использование aiohttp для обслуживания «статических» ресурсов, таких как CSS и JavaScript, не рекомендуется, но для целей этого приложения это не должно быть проблемой.

Давайте добавим несколько строк в наш aiohttp_app.py для обслуживания HTML-файла, который ссылается на файл JavaScript:

 # aiohttp_app.py 
 ... 
 @routes.get('/') 
 async def hello(request): 
 return web.FileResponse("./index.html") 
 
 
 app = web.Application() 
 app.add_routes(routes) 
 app.router.add_static("/", "./") 
 ... 

hello сопрограмма настраивает маршрут GET на localhost:8000/ который обслуживает содержимое index.html , расположенного в том же каталоге, из которого мы запускаем наш сервер.

app.router.add_static устанавливает маршрут на localhost:8000/ для обслуживания файлов в том же каталоге, из которого мы запускаем наш сервер. Это означает, что наш браузер сможет найти файл JavaScript, на который мы index.html в index.html.

Примечание . В производственной среде имеет смысл переместить файлы HTML, CSS и JS в отдельный каталог, который обслуживается самостоятельно. Это делает так, что любопытный пользователь не может получить доступ к нашему серверному коду.

Файл HTML довольно прост:

 <!DOCTYPE html> 
 <html lang='en'> 
 
 <head> 
 <meta charset="utf-8"> 
 <meta name="viewport" content="width=device-width, initial-scale=1"> 
 <title>Planet Tracker</title> 
 </head> 
 <body> 
 <div id="app"> 
 <label id="lon">Longitude: <input type="text"/></label><br/> 
 <label id="lat">Latitude: <input type="text"/></label><br/> 
 <label id="elevation">Elevation: <input type="text"/></label><br/> 
 </div> 
 <script src="/app.js"></script> 
 </body> 

Хотя файл JavaScript немного сложнее:

 var App = function() { 
 
 this.planetNames = [ 
 "mercury", 
 "venus", 
 "mars", 
 "jupiter", 
 "saturn", 
 "uranus", 
 "neptune" 
 ] 
 
 this.geoLocationIds = [ 
 "lon", 
 "lat", 
 "elevation" 
 ] 
 
 this.keyUpInterval = 500 
 this.keyUpTimer = null 
 this.planetDisplayCreated = false 
 this.updateInterval = 2000 // update very second and a half 
 this.updateTimer = null 
 this.geoLocation = null 
 
 this.init = function() { 
 this.getGeoLocation().then((position) => { 
 var coords = this.processCoordinates(position) 
 this.geoLocation = coords 
 this.initGeoLocationDisplay() 
 this.updateGeoLocationDisplay() 
 return this.getPlanetEphemerides() 
 }).then((planetData) => { 
 this.createPlanetDisplay() 
 this.updatePlanetDisplay(planetData) 
 }).then(() => { 
 return this.initUpdateTimer() 
 }) 
 } 
 
 this.update = function() { 
 if (this.planetDisplayCreated) { 
 this.getPlanetEphemerides().then((planetData) => { 
 this.updatePlanetDisplay(planetData) 
 }) 
 } 
 } 
 
 this.get = function(url, data) { 
 var request = new XMLHttpRequest() 
 if (data !== undefined) { 
 url += `?${data}` 
 } 
 // console.log(`get: ${url}`) 
 request.open("GET", url, true) 
 return new Promise((resolve, reject) => { 
 request.send() 
 request.onreadystatechange = function(){ 
 if (this.readyState === XMLHttpRequest.DONE && this.status === 200) { 
 resolve(this) 
 } 
 } 
 request.onerror = reject 
 }) 
 } 
 
 this.processCoordinates = function(position) { 
 var coordMap = { 
 'longitude': 'lon', 
 'latitude': 'lat', 
 'altitude': 'elevation' 
 } 
 var coords = Object.keys(coordMap).reduce((obj, name) => { 
 var coord = position.coords[name] 
 if (coord === null || isNaN(coord)) { 
 coord = 0.0 
 } 
 obj[coordMap[name]] = coord 
 return obj 
 }, {}) 
 return coords 
 } 
 
 this.coordDataUrl = function (coords) { 
 postUrl = Object.keys(coords).map((c) => { 
 return `${c}=${coords[c]}` 
 }) 
 return postUrl 
 } 
 
 this.getGeoLocation = function() { 
 return new Promise((resolve, reject) => { 
 navigator.geolocation.getCurrentPosition(resolve) 
 }) 
 } 
 
 this.getPlanetEphemeris = function(planetName) { 
 var postUrlArr = this.coordDataUrl(this.geoLocation) 
 return this.get(`/planets/${planetName}`, postUrlArr.join("&")).then((req) => { 
 return JSON.parse(req.response) 
 }) 
 } 
 
 this.getPlanetEphemerides = function() { 
 return Promise.all( 
 this.planetNames.map((name) => { 
 return this.getPlanetEphemeris(name) 
 }) 
 ) 
 } 
 
 this.createPlanetDisplay = function() { 
 var div = document.getElementById("app") 
 var table = document.createElement("table") 
 var header = document.createElement("tr") 
 var headerNames = ["Name", "Azimuth", "Altitude"] 
 headerNames.forEach((headerName) => { 
 var headerElement = document.createElement("th") 
 headerElement.textContent = headerName 
 header.appendChild(headerElement) 
 }) 
 table.appendChild(header) 
 this.planetNames.forEach((name) => { 
 var planetRow = document.createElement("tr") 
 headerNames.forEach((headerName) => { 
 planetRow.appendChild( 
 document.createElement("td") 
 ) 
 }) 
 planetRow.setAttribute("id", name) 
 table.appendChild(planetRow) 
 }) 
 div.appendChild(table) 
 this.planetDisplayCreated = true 
 } 
 
 this.updatePlanetDisplay = function(planetData) { 
 planetData.forEach((d) => { 
 var content = [d.name, d.az, d.alt] 
 var planetRow = document.getElementById(d.name) 
 planetRow.childNodes.forEach((node, idx) => { 
 var contentFloat = parseFloat(content[idx]) 
 if (isNaN(contentFloat)) { 
 node.textContent = content[idx] 
 } else { 
 node.textContent = contentFloat.toFixed(2) 
 } 
 }) 
 }) 
 } 
 
 this.initGeoLocationDisplay = function() { 
 this.geoLocationIds.forEach((id) => { 
 var node = document.getElementById(id) 
 node.childNodes[1].onkeyup = this.onGeoLocationKeyUp() 
 }) 
 var appNode = document.getElementById("app") 
 var resetLocationButton = document.createElement("button") 
 resetLocationButton.setAttribute("id", "reset-location") 
 resetLocationButton.onclick = this.onResetLocationClick() 
 resetLocationButton.textContent = "Reset Geo Location" 
 appNode.appendChild(resetLocationButton) 
 } 
 
 this.updateGeoLocationDisplay = function() { 
 Object.keys(this.geoLocation).forEach((id) => { 
 var node = document.getElementById(id) 
 node.childNodes[1].value = parseFloat( 
 this.geoLocation[id] 
 ).toFixed(2) 
 }) 
 } 
 
 this.getDisplayedGeoLocation = function() { 
 var displayedGeoLocation = this.geoLocationIds.reduce((val, id) => { 
 var node = document.getElementById(id) 
 var nodeVal = parseFloat(node.childNodes[1].value) 
 val[id] = nodeVal 
 if (isNaN(nodeVal)) { 
 val.valid = false 
 } 
 return val 
 }, {valid: true}) 
 return displayedGeoLocation 
 } 
 
 this.onGeoLocationKeyUp = function() { 
 return (evt) => { 
 // console.log(evt.key, evt.code) 
 var currentTime = new Date() 
 if (this.keyUpTimer !== null){ 
 clearTimeout(this.keyUpTimer) 
 } 
 this.keyUpTimer = setTimeout(() => { 
 var displayedGeoLocation = this.getDisplayedGeoLocation() 
 if (displayedGeoLocation.valid) { 
 delete displayedGeoLocation.valid 
 this.geoLocation = displayedGeoLocation 
 console.log("Using user supplied geo location") 
 } 
 }, this.keyUpInterval) 
 } 
 } 
 
 this.onResetLocationClick = function() { 
 return (evt) => { 
 console.log("Geo location reset clicked") 
 this.getGeoLocation().then((coords) => { 
 this.geoLocation = this.processCoordinates(coords) 
 this.updateGeoLocationDisplay() 
 }) 
 } 
 } 
 
 this.initUpdateTimer = function () { 
 if (this.updateTimer !== null) { 
 clearInterval(this.updateTimer) 
 } 
 this.updateTimer = setInterval( 
 this.update.bind(this), 
 this.updateInterval 
 ) 
 return this.updateTimer 
 } 
 
 this.testPerformance = function(n) { 
 var t0 = performance.now() 
 var promises = [] 
 for (var i=0; i<n; i++) { 
 promises.push(this.getPlanetEphemeris("mars")) 
 } 
 Promise.all(promises).then(() => { 
 var delta = (performance.now() - t0)/1000 
 console.log(`Took ${delta.toFixed(4)} seconds to do ${n} requests`) 
 }) 
 } 
 } 
 
 var app 
 document.addEventListener("DOMContentLoaded", (evt) => { 
 app = new App() 
 app.init() 
 }) 

Это приложение будет периодически (каждые 2 секунды) обновлять и отображать эфемериды планет. Мы можем предоставить наши собственные географические координаты или позволить API геолокации в Интернете определить наше текущее местоположение. Приложение обновляет геолокацию, если пользователь перестает печатать на полсекунды или более.

Хотя это не учебник по JavaScript, я думаю, полезно понять, что делают разные части скрипта:

  • createPlanetDisplay динамически создает элементы HTML и привязывает их к объектной модели документа (DOM).
  • updatePlanetDisplay принимает данные, полученные от сервера, и заполняет элементы, созданные createPlanetDisplay
  • get делает запрос GET к серверу. Объект XMLHttpRequest позволяет сделать это без перезагрузки страницы.
  • post отправляет POST-запрос на сервер. Как и в случае с get это делается без перезагрузки страницы.
  • getGeoLocation использует API веб-геолокации для получения текущих географических координат пользователя. Это должно быть выполнено «в безопасном контексте» (т.е. мы должны использовать HTTPS не HTTP ).
  • getPlanetEphemeris и getPlanetEphemerides отправляют GET-запросы к серверу, чтобы получить эфемериды для конкретной планеты и для получения эфемерид для всех планет, соответственно.
  • testPerformance делает n запросов к серверу и определяет, сколько времени это займет.

Учебник по развертыванию на Heroku

Heroku - это сервис для простого развертывания веб-приложений. Heroku заботится о настройке веб-компонентов приложения, например о настройке обратных прокси-серверов или о балансировке нагрузки. Heroku - отличный бесплатный хостинг для приложений, обрабатывающих небольшое количество запросов и небольшое количество пользователей.

В последние годы развертывание приложений Python в Heroku стало очень простым. По сути, мы должны создать два файла, в которых перечислены зависимости нашего приложения и сообщают Heroku, как запускать наше приложение.

Pipfile заботится о первом, а Procfile заботится о втором. Pipfile поддерживается с помощью pipenv - мы добавляем в наш Pipfile (и Pipfile.lock) каждый раз, когда устанавливаем зависимость.

Чтобы запустить наше приложение на Heroku, нам нужно добавить еще одну зависимость:

 [email protected] :~/planettracker$ pipenv install gunicorn 

Мы можем создать наш собственный Procfile, добавив в него следующую строку:

 web: gunicorn aiohttp_app:app --worker-class aiohttp.GunicornWebWorker 

По сути, это говорит Heroku использовать Gunicorn для запуска нашего приложения, используя специальный веб-воркер aiohttp.

Прежде чем вы сможете развернуть приложение на Heroku, вам нужно начать отслеживать приложение с помощью Git:

 [email protected] :~/planettracker$ git init 
 [email protected] :~/planettracker$ git add . 
 [email protected] :~/planettracker$ git commit -m "first commit" 

Теперь вы можете следовать инструкциям в центре разработки Heroku здесь, чтобы развернуть свое приложение. Обратите внимание, что вы можете пропустить этап «Подготовка приложения» в этом руководстве, так как у вас уже есть приложение, отслеживаемое git.

После развертывания приложения вы можете перейти к выбранному URL-адресу Heroku в своем браузере и просмотреть приложение, которое будет выглядеть примерно так:

{.ezlazyload .img-responsive}

Заключение

В этой статье мы погрузились в то, как выглядит асинхронная веб-разработка на Python - ее преимущества и использование. Впоследствии мы создали простое реактивное приложение на основе aiohttp , которое динамически отображает текущие соответствующие координаты неба планет Солнечной системы с учетом географических координат пользователя.

После создания приложения мы подготовили его к развертыванию на Heroku.

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

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus

Содержание