Mateusz Mazurek – programista z pasją

Python, architektura, ciekawostki ze świata IT

Inżynieria oprogramowania Programowanie Programowanie webowe

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.


Czekaj, stop!

Podoba Ci się to co tworzę? Jeśli tak to zapraszam Cię do zapisania się na newsletter:
a w ramach prezentu otrzymasz całkowicie za darmo, dwa dokumenty PDF „6 (nie zawsze oczywistych) błędów popełnianych podczas nauki programowania” który jest jednym z efektów ponad siedmioletniej pracy i obserwacji rozwoju niejednego programisty oraz „Wstęp do testowania w Pythonie”, będący wprowadzeniem do biblioteki PyTest.
Jeśli to Cię interesuje to zapraszam również na swoje social media.

Jak i do ewentualnego postawienia mi kawy :)
Postaw mi kawę na buycoffee.to

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

A może wolisz nowości na mail?

Subskrybuj
Powiadom o
guest

Witryna wykorzystuje Akismet, aby ograniczyć spam. Dowiedz się więcej jak przetwarzane są dane komentarzy.

11 komentarzy
Inline Feedbacks
View all comments
Mateusz H.

Hej Mateusz,

Ja jeszcze nie miałem żadnych doświadczeń z Brythonem. Czy mógłbyś podać strukturę plików w katalogu?

Ten przykład wydaje mi się na tyle ciekawy aby go sobie dokładniej przerobić.

Mateusz H.

Hej Mateusz,

Próbowałem zreplikować stworzenie tego chatu ale dziwne bo u mnie nie działa. Nawet po przekopiowaniu całego kodu w Inspektorze w Konsoli Chrome mam błędy.

Zamieszczam tutaj link do zrzutu ekranu tych błędów:

  1. https://ibb.co/1YVVGwQ
  2. https://ibb.co/SnX13s9

Czy mógłbyś mi napisać jak poprawić te błędy. Co robię źle?

Btw. W twoim kodzie front-end masz błąd ponieważ znacznik <form> powinien się znajdować przed .row i zakończać po .row.

Hej.

Mateusz H.

Aha, faktycznie. Już doklejam :-).

Był błąd w kodzie Pythona w linijce „ifnot is_mine”. Poprawiłem to, jednak nadal mam jakiś error który widać na poniższym zrzucie ekranu:

  1. https://ibb.co/CsPx3cB
Mateusz H.

Hej Mateusz,

Dzięki za podpowiedź. Teraz błędu nie ma ale nie widzę żadnej reakcji po wpisaniu tekstu i wysłaniu go. Nie pojawia się on na stronie. Strona się przeładowuje. Myślałem że problem jest z preventDefault() więc zmieniłem parametr ev na evt ale nie pomogło.

  1. https://ibb.co/7Xzm12Z
  2. https://ibb.co/h2qmtTM
  3. https://ibb.co/qMhwgR8
  4. https://ibb.co/W5j6nT8

Czy miałbyś jakieś pomysły jak to zdebugować?

Btw. Bardzo fajny artykuł, szkoda tylko że kod niedziałający, ale pisz więcej takich artykułów tylko z działającym kodem :-).

Hej.

Mateusz H.

Jest to trochę jak tytuł popularnego bloga który od czasu do czasu czytam: „Dziwne u mnie działa.” :-D

Niestety nie będę miał czasu na debugowanie tego kodu więc chyba to pozostawię. Jednak dobrze wiedzieć że coś takiego jak Brython jest dostępne.

Dzięki Mateusz :-).

[…] Python na froncie […]