Python na frontendzie?!

No cześć :)

Jeśli znasz Python’a lub chociaż trochę wiesz co to za język, to doskonale również wiesz że jest to język strony serwerowej (ang. backend language). Oznacza to tyle że nie piszę się w nim tego co klient, wchodząc na stronę, widzi – żadnych interakcji po stronie klienta nie realizuje się w języku backend’owym.

A co jeśli powiem że Python może być językiem frontend’owym?

Ale zacznijmy od początku…

Implementacja języka

Sam język, na przykład Python to w sumie tylko opis składni i pewnych rozwiązań, ładnie widać to na przykładzie tworzenia własnej bazy NoSQL, gdzie implementowaliśmy ową bazę w Python’ie. Ale nic nie stało na przeszkodzie by zrobić to np. w Javie

Analogicznie mamy np. bytecode który występuję w ekosystemie JVMa – i o tym również warto poczytać. W gruncie rzeczy wygenerować kod pośredni możemy z każdego języka, by następnie interpretować go na konkretnej maszynie wirtualnej.

Skoro wiemy już że język to tylko opis składni to czym jest to czego używamy? Ano używamy tak naprawdę konkretnych implementacji danego języka. I tak na przykład Python, ma kilka implementacji:

  • CPython – implementacja w języku C, najczęściej używana
  • PyPy – implementacja Pythona w RPythonie, dodam że RPython to Python o ograniczonych możliwościach (np. posiadający wymóg statycznego typowania)
  • IronPython – implementacja w C#
  • Jython – implementacja Pythona w… Javie:) uruchamiana na JVMie

Po co istnieje więcej niż jedna implementacja danego języka?

Odpowiedź jest bardzo prosta – konkretna implementacja daje konkretne zyski. I tak, CPython to implementacja wzorcowa języka, ale pisząc np. w Jythonie dostajemy wszystko co oferuje JVM – a w tym np JIT’a. PyPy jest natomiast w wielu przypadkach szybszy niż CPython:

Wykres ten przedstawia znormalizowane pomiary dla PyPy – pod znormalizowane mam na myśli że jeśli konkretny test dla CPython’a zajmuje x sekund to jakim procentem dla tego wyniku jest wynik PyPy’a.

I tak, wiem że porównanie jest dla Python’a 2.7 – a mamy już 3.7.2 – chciałem jednak pokazać skalę.

Czemu więc używamy nadal CPython’a? Bo żadna inna implementacja nie zapewnia kompatybilności ze wszystkimi bibliotekami które pod ten język zostały stworzone. Co utrudnia pracę na nich bardzo.

Co z tym Python’em w przeglądarce?

Skoro już wiemy że istnieją implementacje Python’a w C, Javie, C# itp to czas dodać że istnieje implementacja tego języka również w JavaScript’cie! Co daje nam możliwość użycia go na po stronie klienta. Pobawimy się dziś implementacją która nazywa się Brython – od Browser Python.

Napiszmy prosty… Czat

Nasz czat będzie działał na protokole WebSocket – już kiedyś go tu używałem, warto zerknąć sobie.

W naszym czacie mamy jeden pokój, czyli każda kolejna osoba która się podłączy – pisze do pozostałych, nie widząc historii trwającej rozmowy.

Żeby móc zacząć pisać wygodnie front, napiszmy pierw serwer websocketowy:

1
2
3
4
5
6
7
8
9
10
11
12
13
from websocket_server import WebsocketServer


def on_message_received(client, server, message):
    sender_id = client['id']
    clients_to_notify = filter(lambda c: c['id'] != sender_id, server.clients)
    for dst_client in clients_to_notify:
        dst_client['handler'].send_message(message)


server = WebsocketServer(8765, host='127.0.0.1')
server.set_fn_message_received(on_message_received)
server.run_forever()

Korzystam tu z biblioteki websocket-server więc przed uruchomieniem programu trzeba ją sobie pip’em dociągnąć

pip install websocket-server

i działa.

Kawałek kodu uruchamia serwer na porcie 8765 i na każdą wiadomość wysłaną do niego reaguje tak, że odsyła ją do wszystkich, poza nadawcą, realizowane jest to tu:

1
2
3
4
5
def on_message_received(client, server, message):
    sender_id = client['id']
    clients_to_notify = filter(lambda c: c['id'] != sender_id, server.clients)
    for dst_client in clients_to_notify:
        dst_client['handler'].send_message(message)

pobieramy sobie id klienta wysyłającego wiadomość, następnie filtrujemy wszystkich klientów, tak by zostali Ci, których ID jest różne od przed chwilą pobranego i do każdego z nich jest wysyłana wiadomość. I to w sumie koniec.

I czas na nasz front, zacznijmy od kawałka HTMLa:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<html>

<head>
<meta charset="utf-8">
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"  crossorigin="anonymous">

<script src="brython.js"></script>
<script src="brython_stdlib.js"></script>

</head>

<body onload="brython()">

</body>

</html>

Tutaj praktycznie nic się nie dzieje, dorzucamy sobie Bootstrap’a, żeby potem móc dodać jakieś ładne kolorki do naszego chata, wczytujemy dwie biblioteki brythona’a:

  • brython.js
  • brython_stdlib.js

i w „onload” tagu body dodajemy:

brython()

Wygląd czatu

Skoro mamy szkielet frontu, to dodajmy kawałki HTML’a budujące odpowiedniego dla nas grid’a:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div class="container">
<div class="row align-items-end">
<div class="col">
<div id="chat-area"></div>
</div>
</div>
<div class="row align-items-end">
<div class="col">
<form>
<div class="form-group">
                    <input class="form-control" id="txtarea" rows="3" placeholder="Wpis wiadomość.."></input></div>
</div>
</div>
<div class="row align-items-end">
<div class="col">
            <button type="submit" id="send-button" class="btn btn-primary" disabled>Submit</button></div>
</form>

</div>
</div>

Mamy tu kontener z trzema wierszami – każdy po jednej kolumnie. Pierwszy wiersz będzie zapisem naszej rozmowy, drugi jak widać, to pole do wprowadzenia wiadomości a trzeci to przycisk „wyślij” – banalne do bólu.

Implementacja czatu w Pythonie w przeglądarce

No i teraz, po tym całym wstępnie, uświadamiającym co, gdzie, po co i jak, możemy napisać kawałek kodu w Pythonie.

Brython wymaga by kod ten w pliku HTML był w odpowiednim tagu:

1
2
<script type="text/python">
</script>

I zobaczmy pierw gotowy kod:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
from browser import document, alert, websocket
from browser.html import DIV
import datetime


send_button = document['send-button']
txtarea = document['txtarea']
ws = websocket.WebSocket("ws://localhost:8765")

def append_to_chat_area(message, is_mine=False):
    chat_area = document['chat-area']
    time_as_str = str(datetime.datetime.now().time())
    separator = ' | '
    if not is_mine:
        main_msg_css_class = 'text-left alert alert-primary'
        message = time_as_str + separator + message
    else:
        main_msg_css_class = 'text-right alert alert-dark'
        message += separator + time_as_str

   
    chat_area <= DIV(message, Class=main_msg_css_class)
    send_button.scrollIntoView()

def on_click_on_send_button(ev):
    message = txtarea.value
    if not message:
        return
    ws.send(message)
    append_to_chat_area(message, is_mine=True)
    txtarea.value = ""
    ev.preventDefault()

send_button.bind('click', on_click_on_send_button)

def on_connection_open():
    del send_button.attrs['disabled']

def on_message(evt):
    msg = evt.data
    chat_area = document['chat-area']
    append_to_chat_area(msg)

def on_close():
    alert("Connection is closed")


ws.bind('open', on_connection_open)
ws.bind('message', on_message)
ws.bind('close', on_close)

i lecimy od góry:

1
2
3
from browser import document, alert, websocket
from browser.html import DIV
import datetime

Ładujemy moduły document, alert i websocket – pozwalają one, odpowiednio – poruszać się po drzewie DOM, korzystać z alertu JS’owego i łączyć się po WebSockecie. W drugiej linijce pobieramy klasę DIV – pozwalającą tworzyć tag div. I na samym końcu importujemy datetime – tutaj warto zauważyć że moduł ten jest częścią biblioteki standardowej Pythona.

Lecimy dalej :)

1
2
3
4
5
6
7
8
from browser import document, alert, websocket
from browser.html import DIV
import datetime


send_button = document['send-button']
txtarea = document['txtarea']
ws = websocket.WebSocket("ws://localhost:8765")

Dokładamy kolejną cegiełkę – pobieramy, dzięki modułowy document, referencję do naszego przycisku, potem do elementu z nową wiadomością i na koniec łączymy się do serwera websocketowego.

Teraz czas na logikę, zacznijmy pierw od funkcji dodającej wiadomości do obszaru czatu:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from browser import document, alert, websocket
from browser.html import DIV
import datetime


send_button = document['send-button']
txtarea = document['txtarea']
ws = websocket.WebSocket("ws://localhost:8765")

def append_to_chat_area(message, is_mine=False):
    chat_area = document['chat-area']
    time_as_str = str(datetime.datetime.now().time())
    separator = ' | '
    if not is_mine:
        main_msg_css_class = 'text-left alert alert-primary'
        message = time_as_str + separator + message
    else:
        main_msg_css_class = 'text-right alert alert-dark'
        message += separator + time_as_str

   
    chat_area <= DIV(message, Class=main_msg_css_class)
    send_button.scrollIntoView()

Atrybut is_mine oznacz czy wiadomość którą dodajmy jest nasza czy przyszła do nas od innych.

Pierw pobieramy referencję do obszaru czatu, potem aktualny czas (znów widać zgodność z CPython’em – funkcja str).

I wybieramy – jeśli wiadomość nie jest moja – te klasy które podświetlą ją na niebiesko (alert-primary) i dosuną do lewej (text-left), natomiast, jeśli wiadomość jest moja to dosuwamy ją do prawej i kolorujemy na szaro.

Teraz dochodzimy do takich dwóch linijek:

1
2
    chat_area <= DIV(message, Class=main_msg_css_class)
    send_button.scrollIntoView()

tworzymy tu obiekt DIV (odpowiednik tagu div) z zawartością równą naszej wiadomości i klasami które wcześniej wybraliśmy. I na koniec upewniamy się że mimo napływających wiadomości nadal jesteśmy nad przyciskiem (scrollIntoView). Kolejna funkcja jest odpalana w momencie wysyłania formularza:

1
2
3
4
5
6
7
8
def on_click_on_send_button(ev):
    message = txtarea.value
    if not message:
        return
    ws.send(message)
    append_to_chat_area(message, is_mine=True)
    txtarea.value = ""
    ev.preventDefault()

i pobieramy tu wpisaną w pole wiadomości wartość i jeśli jest nie pusta to wysyłamy ją przez WebSocket, dodajemy do obszaru czatu, czyścimy i na koniec upewniamy się że ten formularz nie przeładuje nam strony.

Reszta kodu jest już bardzo prosta:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
send_button.bind('click', on_click_on_send_button)

def on_connection_open():
    del send_button.attrs['disabled']

def on_message(evt):
    msg = evt.data
    append_to_chat_area(msg)

def on_close():
    alert("Connection is closed")


ws.bind('open', on_connection_open)
ws.bind('message', on_message)
ws.bind('close', on_close)

bindujemy zdarzenie wysłania formularza do funkcji oraz reagujemy na nową wiadomość po prostu dodając ją do historii czata.

Do tego dwie proste funkcje:

  • on_connection_open – odblokowuje przycisk wysyłania gdy połączymy się z serwerem
  • on_close – daj info użytkownikowi że połączenie zostało przerwane

i na samiutkim końcu bindujemy funkcje ze zdarzeniami WebSocketowymi.

No i efekt:

Co dalej z tym?

Sądzę że… Nic:)

Raczej nie odważyłbym się tego używać jeszcze na produkcji, chociaż może ktoś ma jakieś doświadczenia? Jeśli tak, to proszę, podziel się w komentarzu nimi.

Dzięki za wizytę,
Mateusz Mazurek
Podziel się na:
    Facebook email PDF Wykop Twitter

Dodaj komentarz

avatar

This site uses Akismet to reduce spam. Learn how your comment data is processed.

  Subscribe  
Powiadom o