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:
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:Jeśli to Cię interesuje to zapraszam również na swoje social media.
Jak i do ewentualnego postawienia mi kawy :)
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.
Mateusz Mazurek
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ć.
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:
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.
Hej,
ten kawałek w od serwera wygląda dobrze, natomiast ten od klienta, akurat uciąłeś początek tego czerwonego wyjątku:) dokleisz? Te żółte są nieistotne:)
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:
Ciekawe. To chyba ślepy strzał, ale dodaj może w funkcji
def on_connection_open()
parametr „evt”:)
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.
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.
Hej,
niestety pomysłu nie mam :( a to, że kod nie działa to było do przewidzenia:) wpis był pisany ~2 lata temu, wtedy działał ale od tego czasu brython zmienił się bardzo :D więc zostaje tylko zerknąć w dokumentację brythona i wyczaić owe zmiany:D
Hmm, w sumie aż uruchomiłem z ciekawości i mi działa:
https://ibb.co/kxxhB0b
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 :-).
Polecam się na przyszłość :D
[…] Python na froncie […]