Späť na blog
Tipy a triky

Websockety - message board

Miroslav Beka
04.10.2018
22 minút čítania
Websockety - message board
Ahoj, naposledy sme hovorili o websocketoch vo flasku. Používali sme knižnicu flask-socketio a prešli sme si základnú funkcionalitu. Táto knižnica používa koncept miestností alebo rooms, ktorý slúži na to, aby sme vedeli adresovať klientov v nejakých skupinách.
Tento koncept sa používa v chatových aplikáciách, kde používatelia vidia správy len v miestnosti, v ktorej sa nachádzajú. Nedostanú správy zo žiadnej inej.
Pozrieme sa teda na tento koncept a aby sme spravili aj nejaký reálny príklad, spravíme vlastnú chatovaciu appku. Používatelia sa budú môcť pridať do existujúcej miestnosti, chatovať s ostatnými, vytvárať nové miestnosti a podobne. Bude to veľmi jednoduchý message board.

Základ projektu

Začne tým, že si vytvoríme virtualenv! Bez toho sa ani nepohneme.
$ mkdir websockets_message_board
$ cd websockets_message_board
$ virtualenv venv
$ . venv/bin/activate
Inštalujeme závislosti. Budeme používať to isté, čo v predchádzajúcom článku
(venv)$ pip install flask, flask-socketio
Ideme na boilerplate pre našu appku. Štruktúra vyzerá asi takto:
▾ websockets_message_board/
  ▾ static/
    ▾ css/
        main.css
    ▾ js/
        main.js
  ▾ templates/
      board.jinja
  ▸ venv/
    server.py
Súbory main.css a main.js sú zatiaľ prázdne, slúžia len ako placeholder. Pokračujeme teda so súborom server.py a ideme ho naplniť kódom.
from flask import Flask
from flask import render_template
from flask import redirect
from flask import url_for
from flask_socketio import SocketIO
app = Flask(__name__)
app.config['SECRET_KEY'] = '\xfe\x060|\xfb\xf3\xe9F\x0c\x93\x95\xc4\xbfJ\x12gu\xf1\x0cP\xd8\n\xd5'
socketio = SocketIO(app)
### WEB CONTROLLER
@app.route("/")
def index():
    return redirect(url_for("view_board"))
@app.route("/board/")
def view_board():
    return render_template("board.jinja")
if __name__ == '__main__':
    socketio.run(app, debug=True)
Rozdiel oproti minimálnej flask appke je ten, že ju inak spúšťame. Nepoužijeme
if __name__ == '__main__':
    app.run()
ale budeme ju spúšťať cez socketIO.
if __name__ == '__main__':
    socketio.run(app, debug=True)
To preto, aby aplikácia vedela spustiť viacero vlákien pre každého používateľa. Tak isto je dobré vedieť, že deployment na produkčný server takejto aplikácie je trošku komplikovanejší ako keď máme klasickú flask appku.
Obsah základného templejtu board.jinja (aj jediného, ktorý budeme používať) je nasledovný:
<!DOCTYPE HTML>
<html>
<head>
    <title>Short Term Memory Message Board</title>
    <script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
    <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/socket.io/1.3.5/socket.io.min.js"></script>
    <script type="text/javascript" src="{{ url_for("static", filename="js/main.js")}}"></script>
    <link rel="stylesheet" type="text/css" href={{url_for("static", filename="css/main.css")}}>
</head>
<body>
    Hello
</body>
</html>
máme tam zopár dôležitých importov ako socket.io, jquery a tak isto aj css a js súbory našej appky.
Takýto jednoduchý základ môžeme spustiť a uvidíme, či všetko šlape ako má
$(venv) python server.py
WebSocket transport not available. Install eventlet or gevent and gevent-websocket for improved performance.
 * Serving Flask app "server" (lazy loading)
 * Environment: production
   WARNING: Do not use the development server in a production environment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
WebSocket transport not available. Install eventlet or gevent and gevent-websocket for improved performance.
 * Debugger is active!
 * Debugger PIN: 112-998-522

Facelift

Tento krok nie je vôbec potrebný, ale keďže všetci majú radi pekné veci, nainštalujeme si css framework zvaný semantic-ui. Je to fajn framework, mám s ním dobré skúsenosti. Dokumentácia je možno trošku tažšia na pochopenie, ale okrem toho to funguje a hlavne vyzerá veľmi pekne.
Stačí stiahnuť toto zipko a integrovať do svojho projektu. Je to veľmi jednoduché. Zip rozbalíme a prekopírujeme nasledovné súbory
  • themes -> websockets_message_board/static/css/
  • semantic.min.css -> websockets_message_board/static/css/
  • semantic.min.js -> websockets_message_board/static/js/
Súbory semantic.min.js a semantic.min.css musím includnuť na svoju stránku, takže bežím do board.jinja a prihodím do hlavičky ďalšie riadky:
<head>
    <title>Short Term Memory Message Board</title>
    <script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
    <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/socket.io/1.3.5/socket.io.min.js"></script>
    <script type="text/javascript" src="{{ url_for("static", filename="js/semantic.min.js")}}"></script>
    <script type="text/javascript" src="{{ url_for("static", filename="js/main.js")}}"></script>
    <link rel="stylesheet" type="text/css" href={{url_for("static", filename="css/semantic.min.css")}}>
    <link rel="stylesheet" type="text/css" href={{url_for("static", filename="css/main.css")}}>
</head>
Je dôležité dať si pozor, aby sme najprv pridali jquery a až potom semantic.min.js, inak sa mi semantic-ui bude sťažovať, že nevie nájsť jquery knižnicu. V priečinku themes sú hlavne ikony a nejaké obrázky, ktoré semantic-uiposkytuje.
Po inštalácií css frameworku môžem hneď vidieť zmenu v podobe iného fontu na mojej smutnej stránke. Nič iné tam ešte nieje.

UI

Spravíme teraz približný náčrt UI, aby som vedel, ako appka asi bude vyzerať a aké funkcie jej vlastne spravíme. Nebude to nič svetoborné. Budeme mať jednu stránku ktorú rozdelím na 3 sekcie. Hlavná bude obsahovať správy, takže to bude môj message board. Bočný panel bude obsahovať zoznam miestností, do ktorých sa budem vedieť prepínať. No a na spodnej lište bude input pre moju správu.

Zhmotním túto svoju predstavu do kódu. Otvorím board.jinja a nahádžem tam nejaké <div> elementy. Keďže používame semnatic-ui ako náš css framework, budem rovno používať triedy v html. Použijeme grid systém, ktorý nám zjednoduší prácu pri ukladaní ui elementov.
<body class="ui container">
    <div class="ui grid">
        <div class="ten wide column">
        message board
        </div> {# end ten wide column #}
        <div class="six wide column">
        rooms
        </div> {# end six wide column #}
    </div> {# end grid #}
    <footer>
    text input
    </footer>
</body>
Môžem skúsiť naplniť tieto časti aj nejakým obsahom. Len tak zo zvedavosti, ako to bude vyzerať. Všetko bude zatiaľ len tak naoko (prototypovanie).
Začneme tým najhlavnejším: message boardom
<div class="ten wide column">
    <h1 id="room_heading" class="ui header">Johny @ Music room</h1>
    <div id="msg_board">
        <div class="ui mini icon message">
            <i class="comment icon"></i>
            <div class="content">
                <div class="header">Johny</div>
                    <p>Hello there</p>
            </div>
        </div>
        <div class="ui mini icon message">
            <i class="comment icon"></i>
            <div class="content">
                <div class="header">Tommy</div>
                    <p>Hi!</p>
            </div>
        </div>
        <div class="ui mini icon message">
            <i class="comment icon"></i>
            <div class="content">
                <div class="header">Tommy</div>
                    <p>What's up?</p>
            </div>
        </div>
    </div> {# end msg board #}
</div> {# end ten wide column #}
Všetky správy som obalil do div s id msg_board aby som potom jednoducho vedel pridávať nové správy do tohto elementu.

Spravíme to isté pre zoznam miestností. Rozhodol som sa, že do tohto bočného panelu strčíme aj formulár na zmenu mena používateľa. Ten by mal mať možnosť zmeniť svoje meno. Bude to vyzerať asi takto:
<div class="six wide column">
    <h4 class="ui dividing header">Change username</h4>
    <form id="choose_username" class="ui form" method="post">
        <div class="field">
            <div class="ui action input">
                <input type="text" id="user_name" placeholder="username...">
                <button class="ui button">Change</button>
            </div>
        </div>
    </form>
    <h4 class="ui dividing header">Rooms</h4>
    <form id="choose_room" class="ui form" method="post">
        <div class="grouped fields">
            <label for="fruit">Select available room:</label>
            <div id="room_list">
                <div class="field">
                    <div class="ui radio checkbox">
                        <input type="radio" name="room" class="hidden" value="Lobby">
                        <label>Lobby</label>
                    </div>
                </div>
                <div class="field">
                    <div class="ui radio checkbox">
                        <input type="radio" name="room" class="hidden" value="Music">
                        <label>Music</label>
                    </div>
                </div>
                <div class="field">
                    <div class="ui radio checkbox">
                        <input type="radio" name="room" class="hidden" value="Movies">
                        <label>Movies</label>
                    </div>
                </div>
            </div>
            <div class="field">
                <input type="text" id="new_room" placeholder="create new room">
            </div>
            <button class="ui button"> Change Room</button>
        </div>
    </form>
</div> {# end six wide column #}

Tak isto som pridal aj <input /> na vytváranie nových miestností. Myslím, že takúto možnosť by používateľ mohol mať.
Poslednou skladačkou bude input pre naše správy.
<footer>
    <form id="send_msg_to_room" class="ui form" method="post">
        <div class="field">
            <div class="ui fluid action input">
                <input type="text" id="msg_input" placeholder="message..."/>
                <button class="ui button" value="send">send</button>
            </div>
        </div>
    </form>
</footer>

Momentálne mi nebudú fungovať radio buttony, pretože semantic-ui potrebuje tieto inicializovať v javascripte. Pome teda na to. Otvoríme main.js a píšeme
$(document).ready(function(){
    // UI HANDLERS
    $('.ui.radio.checkbox').checkbox();
});
Tak isto môžeme rovno vybaviť iniciálne spojenie cez websockety medzi klientom a serverom.
$(document).ready(function(){
    var url = location.protocol + "//" + document.domain + ":" + location.port;
    socket = io.connect(url);
    // UI HANDLERS
    $('.ui.radio.checkbox').checkbox();
});
Posielanie správ môžem rovno aj vyskúšať v konzole prehliadača. Stačí otvoriť developer tools, prejsť na záložku console a tam už môžeme písať
socket.emit("test", "hello there")

Avšak, nič sa nedeje, pretože môj backend ešte nie je vôbec pripravený. Vrhneme sa teda na server side a implementujeme miestnosti - room.

Rooms

Presunieme sa do súboru server.py a pridáme handler pre základné eventy ktoré budeme používať: join, leave, msg_board, username_change
...
from flask_socketio import send, emit
from flask_socketio import join_room, leave_room
...
### WEB CONTROLLER
@app.route("/")
def index():
    return redirect(url_for("view_board"))
@app.route("/board/")
def view_board():
    return render_template("board.jinja")
## SOCKET CONTROLLER
@socketio.on("join")
def on_join(data):
    username = data["user_name"]
    room = data["room_name"]
    join_room(room)
    send("{} has entered the room: {}".format(username, room), room=room)
@socketio.on("leave")
def on_leave(data):
    username = data["user_name"]
    room = data["room_name"]
    leave_room(room)
    send("{} has left the room: {}".format(username, room), room=room)
@socketio.on("msg_board")
def handle_messages(msg_data):
    emit("msg_board", msg_data, room=msg_data["room_name"])
@socketio.on("username_change")
def username_change(data):
    msg = "user \"{}\" changed name to \"{}\"".format(
            data["old_name"], data["new_name"])
    send(msg, broadcast=True)
...
Eventy join, leave a username_change fungujú veľmi jednoducho. Zakaždým sa pozriem na dáta, ktoré mi prišli (premenná data) a vytvorím jednoduchú správu, ktorú potom broadcastujem na všetkých používateľov v tej danej miestnosti.
Ak si už poriadne nepamätáš, čo robil ten broadcast, pospomínaj z minulého blogu.
Dôležité je použitie funkcií join_room a leave_room. Tieto pochádzajú z knižnice flask-socketio, ktorú sme inštalovali na začiatku. Slúžia na to aby sme priradili danú session do nejakej miestnosti. Potom, keď pošlem správu do miestnosti, dostanú ju všetci v tej miestnosti. Je to fajn mechanizmus ako kontaktovať iných klientov a usporiadať si ich do nejakých kategórií.
rooms nemusím nevyhnutne používať len na chatovú funkcionalitu. Môžem to použiť na to, aby som si zoradil používateľov do nejakej spoločnej skupiny, ktorej posielam barsjaké dáta.
Dajme tomu, že by som mal appku o počasí, a nejaká skupina používateľov by mala záujem o notifikácie, či bude pršať. Tak týchto by som hodil do spoločnej skupiny - miestnosti - a notifikácie by som posielal len im. Využitie je teda všakovaké.

JavaScript

Backend bol v tomto prípade celkom jednoduchý a nepotrebovali sme toho veľa implementovať. Správy sa od nášho backendu len odrážajú ako od relátka, ktorý ich ďalej rozposiela klientom.
Na strane klienta toho bude trošku viacej. Pokračujeme v súbore main.js. Teraz sa pokúsime implementovať posielanie správy a zobrazenie prichádzajúcej správy na messageboard.
$(document).ready(function() {
    ...
    // generate random user name if needed
    setRandomNameAndRoom();
    // join default room
    joinRoom(socket);
    // UI HANDLERS
    $('.ui.radio.checkbox').checkbox();
    // send message
    $("form#send_msg_to_room").submit(function(event) {
        userName = sessionStorage.getItem("userName");
        roomName = sessionStorage.getItem("roomName");
        msg = $("#msg_input").val();
        sendMessage(socket, userName, roomName, msg);
        this.reset();
        return false;
    });
    // handle new message
    socket.on("msg_board", function(data){
        msg = '<div class="ui mini icon message">';
        msg += '<i class="comment icon"></i>';
        msg += '<div class="content">';
        msg += '<div class="header">'+data["user_name"]+'</div>';
        msg += '<p>' + data["msg"] + '</p>';
        msg += '</div>';
        msg += '</div>';
        $("#msg_board").append(msg);
    });
});
// HELPERS
function setRandomNameAndRoom(){
    if (sessionStorage.getItem("userName") == null){
        randomName = "user" + Math.floor((Math.random() * 100) + 1);
        sessionStorage.setItem("userName", randomName);
        sessionStorage.setItem("roomName", "Lobby");
    };
};
function joinRoom(socket){
    data = {
        "room_name" : sessionStorage.getItem("roomName"),
        "user_name" : sessionStorage.getItem("userName")
    };
    socket.emit("join", data);
};
function sendMessage(socket, userName, roomName, message){
    data = {
        "user_name" : userName,
        "room_name" : roomName,
        "msg" : msg
    };
    socket.emit("msg_board", data);
};

Na začiatok vytvoríme nejaké random meno používateľa a zvolíme default miestnosť "Lobby". To aby sme s týmto nemali starosti zatiaľ. Používame na to pomocné funkcie, ktoré si implementujeme bokom, aby nám nezavadzali.
Meno používateľa a názov aktuálnej miestnosti si udržiavam v sessionStorage, čo je fajn dočasné úložisko v prehliadači. Prežije aj reload stránky a navyše sa mi tento spôsob viacej páči ako udržiavať informáciu v cookies.
Keď máme potrebné dáta, môžeme sa hneď na začiatku buchnúť do nejakej miestnosti. V javascripte používame knižnicu socket.io, ktorá ale žiadny koncept miestností nepozná. Ak sa pozrieš do dokumentácie (pozor! otvor si client api), zistíš, že nič také ako rooms sa tam nespomína. Takže to je vecička knižnice flask-socketio. Použijeme teda klasický emit na handler join, ktorý existuje na servery.
Tento riadok $("form#send_msg_to_room").submit( sa pomocou jquery napichne na formulár a zachytí odoslanie formuláru. Potom môžem robiť čo sa mi zachce a nakoniec vrátim false, takže formulár sa reálne ani neodošle.
Odoslanie správy je priamočiare. Zistím UserName, zistím RoomName, vytiahnem si text správy a všetko pošlem do funkcie sendMessage.
Táto už zabezpečí zabalanie informácií do jsonu a posielam pomocou funkcie emit. Posielam na handler msg_board, ktorý som si spravil pred chvíľkou.
Ostáva mi vyriešiť prijatie správy. To robím pomocou funkcie socket.on, kde dám kód, ktorý sa vykoná pri prijatí správy. Tu si jednoducho (ale zato strašne škaredo) pozliepam kus HTML, ktoré potom strčím na koniec elementu s id msg_board.
Predtým, ako to budeš skúšať, je fajn si ešte vymazať tie fejkové správy, ktoré sme tam dali natvrdo do HTML. Takže mažeme tieto riadky
        <div class="ten wide column">
            <h1 id="room_heading" class="ui header">Johny @ Music room</h1>
            <div id="msg_board">
         --->   <div class="ui mini icon message">
         --->       <i class="comment icon"></i>
         --->       <div class="content">
         --->           <div class="header">Johny</div>
         --->               <p>Hello there</p>
         --->       </div>
         --->   </div>
         --->   <div class="ui mini icon message">
         --->       <i class="comment icon"></i>
         --->       <div class="content">
         --->           <div class="header">Tommy</div>
         --->               <p>Hi!</p>
         --->       </div>
         --->   </div>
         --->   <div class="ui mini icon message">
         --->       <i class="comment icon"></i>
         --->       <div class="content">
         --->           <div class="header">Tommy</div>
         --->               <p>What's up?</p>
         --->       </div>
         --->   </div>
            </div> {# end msg board #}
        </div> {# end ten wide column #}
Pome teda ako ďalšiu vec vybaviť zmenu používateľského mena.
$(document).ready(function(){
    ...
    // set heading
    updateHeading();
    // set user name handler
    $("form#choose_username").submit(function(event){
        // get old and new name
        var oldName = sessionStorage.getItem("userName");
        var newName = $("#user_name").val();
        //save username to local storage
        sessionStorage.setItem("userName", newName);
        // change ui
        updateHeading();
        // notify others
        notifyNameChange(socket, oldName, newName);
        //clear form
        this.reset();
        return false
    });
});
function updateHeading(){
    roomName = sessionStorage.getItem("roomName");
    userName = sessionStorage.getItem("userName");
    $("#room_heading").text(userName + " @ " + roomName);
};
function notifyNameChange(socket, oldName, newName){
    data = {
        "old_name" : oldName,
        "new_name" : newName
    }
    socket.emit("username_change", data);
};
Tak ako pri posielaní správy, napichnem sa na HTML formulár a spracujem ho ešte pred odoslaním. Zmeny uložím do sessionStorage.
Pridal som ešte 2 vychytávky.
  • funkcia updateHeading nastaví aktuálny názov miestnosti a používateľa ako hlavičku stránky,
  • notifyNameChange dá všetkým používateľom vedieť, že si niekto zmenil meno.
Meno si už môžem meniť, ale notifikáciu o zmene som nedostal. Na to ešte musíme doplniť jeden event handler na message
$(document).ready(function(){
    ...
    // system message
    socket.on("message", function(data){
        msg = '<div class="ui mini icon info message">';
        msg += '<i class="bell icon"></i>';
        msg += '<div class="content">';
        msg += '<p>' + data + '</p>';
        msg += '</div>';
        msg += '</div>';
        $("#msg_board").append(msg);
    });
});
...
Teraz sa nám začnú zobrazovať aj systémové notifikácie o tom, čo sa deje. Kto vošiel do miestnosti, kto ju opustil alebo kto si zmenil meno.
Poslednou vecou, ktorú musíme spraviť, je selekcia miestností. Toto bude vyžadovať trošku viacej práce. Zoznam existujúcich miestností si musíme udržiavať na backende. Ani na klientskej časti ani na backende z knižnice flask-socketio neviem získať zoznam všetkých miestností. Musím si ho teda udržiavať sám.
from flask import g
...
DEFAULT_ROOMS = ["Lobby"]
...
@app.route("/board/")
def view_board():
    all_rooms = getattr(g, "rooms", DEFAULT_ROOMS)
    return render_template("board.jinja", rooms=all_rooms)
...
### SOCKET CONTROLLER
@socketio.on("join")
def on_join(data):
    username = data["user_name"]
    room = data["room_name"]
    all_rooms = getattr(g, "rooms", DEFAULT_ROOMS)
    if room not in all_rooms:
        all_rooms.append(room)
        emit("handle_new_room", {"room_name" : room}, broadcast=True)
    join_room(room)
    send("{} has entered the room: {}".format(username, room), room=room)
Do templejtu board.jinja som si začal posielať nejaké dáta. Vyhodím teda tie fejkové, ktoré sú tam natvrdo, a spravíme loop, v ktorom pridám všetky existujúce miestnosti.
<div id="room_list">
    {% for room in rooms %}
    <div class="field">
        <div class="ui radio checkbox">
            <input type="radio" name="room" class="hidden" value="{{room}}">
            <label>{{room}}</label>
        </div>
    </div>
    {% endfor %}
</div>
Pokračujem v súbore main.js, kde si vytvorím funkcie, ktoré sa postarajú o zmenu miestnosti + ak bola vytvorená nová, tak ju pridám do zoznamu.
$(document).ready(function(){
    ...
    // set room name heading
    selectCurrentRoom();
    updateHeading();
    ...
    // set room handler
    $("form#choose_room").submit(function(event){
        newRoom = getRoomName();
        // first leave current room
        leaveRoom(socket);
        // set new room
        sessionStorage.setItem("roomName", newRoom);
        updateHeading();
        // join new room
        joinRoom(socket);
        //clear input
        newRoom = $("#new_room").val("");
        //clear message board
        $("#msg_board").text("");
        return false;
    });
    socket.on("handle_new_room", function(data){
        item = '<div class="field">';
        item += '<div class="ui radio checkbox">';
        item += '<input type="radio" name="room" class="hidden" value="'+ data["room_name"] + '">';
        item += '<label>' + data["room_name"] + '</label>';
        item += '</div>'
        item += '</div>'
        $("div#room_list").append(item);
        selectCurrentRoom();
    });
});
...
function leaveRoom(socket){
    data = {
        "room_name" : sessionStorage.getItem("roomName"),
        "user_name" : sessionStorage.getItem("userName")
    };
    socket.emit("leave", data);
};
function selectCurrentRoom(){
    currentRoom = sessionStorage.getItem("roomName")
    $(".ui.radio.checkbox").checkbox().each(function(){
        var value = $(this).find("input").val();
        if (value == currentRoom){
            $(this).checkbox("set checked");
        };
    });
};
function getRoomName(){
    roomName = $("#new_room").val();
    if (roomName == ""){
        roomName = $("input[type='radio'][name='room']:checked").val();
    };
    return roomName;
};
Je tu viacero pomocných funkcií, ktoré mi pomáhajú pri výbere miestnosti alebo pri vytváraní novej. Problematické časti nastávajú práve v tedy, keď chcem miestnosť aj vytvárať. V podstate ale nejde o žiadne komplikované veci.
Funkcia selectCurrentRoom mi pomôže prehodiť radio button pri zmene miestnosti. Tým, že používame semantic-ui tak sa nám to tiež trošku skomplikovalo, ale výsledok stojí za to.

Záver

Postavili sme takzvaný proof of concept, spravili sme chatovaciu appku len pomocou websocketov. Nie je to dokonalé a určite je tam veľa múch, to nám však nebránilo pochopiť ako fungujú websockety. Všetky správy žijú len v prehliadači používateľa a nie sú uložené na žiadnom serveri. Niekto to môže považovať za chybu, niekto za fičúru. To už nechám na vás.
Celý projekt sa dá stiahnuť tu.
Onedlho sa opäť vrhneme na nejakú zaujímavú tému ;)
Miroslav Beka

Ahoj, volám sa Miro a som Pythonista. Programovať som začal na strednej. Vtedy frčal ešte turbo pascal. Potom prišiel matfyz, kadejaké zveriny ako Haskell, no najviac sa mi zapáčil Python.

Od vtedy v Pythone robím všetko. Okrem vlastných vecí čo si programujem pre radosť, som pracoval v ESETe ako automatizér testovania. Samozrejme, všetko v Pythone. Potom som skočil do inej firmy, tam taktiež Python na automatické testovanie aj DevOps. Viacej krát som účinkoval ako speaker na PyCon.sk, kde som odovzdával svoje skúsenosti.

Medzi moje obľúbené oblasti teda parí DevOps, Automatizovanie testovania a web development (hlavne backend).

Okrem programovania sa venujem hlavne hudbe 🤘

Mohlo by ťa zaujímať

Java 8
Tipy a triky
19.09.2018
Skillmea

Java 8

Java 8 je veľký balíček novej funkcionality oproti Java 9 a java 10. Tie sú menšie a odzrkadľujú taktiku tvorcov javy vydávať nové verzie častejšie s menším balíkom novej funkcionality. V tomto článku si povieme najvýraznejšie zmeny z Java 8. Funkcionálne rozhraniaRozhranie, ktoré má len jednu abstraktnú metódu je považované za funkcionálne rozhranie. Pre lepšie načrtnutie v kóde môžeme a je odporúčané pridať anotáciu @FunctionalInterface.  import java.lang.FunctionalInterface;   @FunctionalInterface public interface FunctionalInterfaceExample {     void print(); }  Lambda výrazyOk, chceme pridať do javy možnosť definovať funkcionalitu bez toho aby patrila špecificky pod nejakú triedu. Tieto funkcie chceme vkladať do metód ako parametre. Čo spravíme? Tvorcovia javy sa asi takto nezamýšľali, ale my sme sa takto zamysleli a odpoveď je – použijeme lambda výrazy.  Každá lambda má ako typ funkcionálne rozhranie s jej odpovedajúcou metódou. V príklade vyššie máme rozhranie s metódou print. Táto metóda nevracia nič a neprijíma žiadne parametre. Musíme vytvoriť aj taký lambda výraz – teda nebude obsahovať return a nebudeme mať žiadne návratové hodnoty.  () -> System.out.println("Hello Word")Takto vložíš lambda výraz do metódy:  printHelloWord(() -> System.out.println("Hello Word"));Samotná metóda prijíma na vstupe práve dané funkcionálne rozhranie:  public void printHelloWord2(FunctionalInterfaceExample printHello)Viac o lambda výrazoch sa dozvieš z môjho online kurzu Java pre pokročilých, videá o lambda výrazoch sú zadarmo. StreamsStreamy poskytujú zjednodušenú prácu s dátovými kolekciami. Poskytujú viacero užitočných metód ako sú napríklad filter, sorted, map a iné. Ak stream préjde cez všetky elementy v kolekcii, tak bude prázdny. Teda ak chcem znovu použiť ten istý stream, tak zbytočne, lebo tam už nebudú dáta. Musíš si vytvoriť nový stream.  Nový stream vytvoríš napríklad pomocou stream metódy nad kolekciami.  List<Osoba> osoby = Arrays.asList( null, new Osoba("Jaro", "Beno", 30), new Osoba("Peter", "Kridlo", 55), new Osoba("Karol", "Otko", 18), new Osoba("Karol", "Beno", 18), new Osoba("Peter", "Otko", 20), new Osoba("Fedor", "Ronald", 84) ); osoby.stream();Máš zoznam osôb a chceš z daného zoznamu získať všetky veky osôb starších ako 50 rokov a majú byť zoradené podľa veku. Čo spravíš? Použiješ stream a jeho metódy – tieto metódy akceptujú funkcionálne rozhrania, teda tam dáš lambda výrazy.  List<Integer> veky = osoby.stream() .filter(osoba -> osoba.getVek() < 50) .sorted(Comparator.comparing(Osoba::getVek)) .map(Osoba::getVek) .collect(Collectors.toList());Kolekcie, ktoré sú iterovateľné, majú metódu forEach – ktorá získa stream a prejde všetky elementy.  veky.forEach(System.out::println);   Method referencePo slovensky referencia na metódu. Všimni si príklad z hora: Osoba::getVek. Znamená to, že z triedy Osoba použi metódu getVek. To isté sa dá zapísať aj pomocou lambda výrazu takto osoba -> osoba.getVek(). Ale pomocou referencie si to zjednodušíme lebo java vie, aký je vstupný parameter – je to osoba a vie, že voláš getVek. Tak stačí napísať Osoba::getVek. Metódu voláš bez zátvoriek.  Ďalší článok o Jave už čoskor. Prihlás sa k odberu noviniek a nezmeškaj žiadný nový blog.   
Websockety vo Flasku
Tipy a triky
15.08.2018
Miroslav Beka

Websockety vo Flasku

Websockety vo Flasku Ak si sa niekedy stretol s výrazom websocket a chcel by si sa dozvedieť, čo to vlastne je a ako sa to používa v Python aplikácii, tak tento článok je práve pre teba. Štandardne tvoj prehliadač komunikuje na webe pomocou http protokolu. Klasický http protokol ponúka jednoduchú komunikáciu. Pošle sa request a ako odpoveď dostanem response. Tento klasický komunikačný spôsob nebol dostatočný pre dnešné moderné aplikácie. Bola potreba pre komunikačný kanál, ktorý bude slúžiť na obojsmernú komunikáciu. HTTP by mal byť viac-menej bezstavový a klient a server medzi sebou komunikujú iba keď treba, inak je spojenie medzi nimi uzavreté. Navyše, prehliadač (klient) musí požiadať server o komunikáciu a server môže na túto žiadosť odpovedať. Tá žiadosť, to je ten http request. Inak server nevie kontaktovať klienta len tak sám od seba. Pri websocketoch je to inak. Ide o komunikačný kanál, ktorý sa otvorí raz, na začiatku a potom sa používa na komunikáciu klienta a servera v oboch stranách. To znamená, že server môže posielať dáta zároveň čo klient posiela dáta na server. Toto sa odborne volá full-duplex. Web socket má menší overheat prenosu dát, vie byť real-time a hlavne, server môže posielať dáta na klienta bez toho, aby si ich klient musel explicitne vyžiadať requestom. Toto je užitočné napríklad pri aplikáciách, ktoré zobrazujú real time dáta a server posiela tieto dáta klientovi. Takže ak nastane nejaká zmena dát, server ich proste pošle na klienta. Toto predtým nebolo možné spraviť iba pomocou http protokolu. Minimálny príkladNajlepšie je vyskúšať si tieto koncepty v praxi. Dnes budeme pracovať s Flaskom, knižnicou SocketIO a javascript knižnicami socket.io a jQuery. Budem predpokladať, že Flask aplikácie aspoň trochu poznáš. Začneme tým, že si vytvoríme nové virtuálne prostredie: $ mkdir websockets_primer $ cd websockets_primer $ virtualenv venv $ . venv/bin/activate (venv) $Nainštalujeme závislosti, ktoré budeme potrebovať: (venv)$ pip install flask, flask-socketioV čase písania tohto článku som používal verzie Flask==1.0.2 a Flask-SocketIO=3.0.1. Keď už máme pripravené prostredie a nainštalované závislosti, spravíme nový súbor server.py from flask import Flask from flask import render_template from flask_socketio import SocketIO app = Flask(__name__) app.config["SECRET_KEY"] = "secret" socketio = SocketIO(app) @app.route("/") def index(): return render_template("index.jinja") @socketio.on("event") def handle_event(data): print(data) if __name__ == '__main__': socketio.run(app, debug=True)Na začiatku máme importy ako pre každú inú Flask aplikáciu, avšak pribudlo nám tam from flask_socketio import SocketIO. Tento naimportovaný modul je v podstate to isté ako iné Flask rozšírenia. Inicializáciu websocketov vo Flask aplikácií spravíme pomocou riadku socketio = SocketIO(app). Pomocou tohto objektu socketio budeme príjmať a odosielať správy. Minimálna aplikácia by mala mať aspoň jednu stránku. V našom prípade to bude index.jinja. Toto je treba pretože musíme poskytnúť aj klientskú časť našej aplikácie. Tam bude javascript knižnica socketio a nejaké ďalšie funkcie. Websockety vedia prijímať a posielať správy. Spravíme zatiaľ len prijímanie správ. Pomocou riadku socketio.on("event")definujem handler pre udalosť event. V tomto prípade jednoducho vypíšem dáta na konzolu. @socketio.on("event") def handle_event(data): print(data)Posielanie a prijímanie dát na oboch stranách (klient a server) prebieha ako event. Toto je dôležitý fakt, pretože architektúra aplikácie založenej na eventoch (event driven architecture) funguje trošku inak ako klasické volanie funkcie. Nehovorím, aby si mal z toho paniku teraz, ale maj to na pamäti. Ak poznáš Flask aplikácie, tak spustenie appky vyzerá zväčša takto if __name__ == "__main__": app.run("0.0.0.0", debug=True)My ale musíme appku spustiť inak, pretože používame websockety. Spustíme ju pomocou objektu socketio, ktorý sme si vytvorili na začiatku. if __name__ == '__main__': socketio.run(app, debug=True)Teraz musíme ešte vytvoriť 2 súbory. Snažíme sa renderovať index.jinja a taktiež musíme vytvoriť hlavný javascript súbor, do ktorého budeme písať klientskú časť našej websocketovej ukážky. Vytvorím priečinok templates a do neho súbor index.jinja <!DOCTYPE HTML> <html> <head> <title>Websockets test</title> <script type="text/javascript" src="//code.jquery.com/jquery-1.4.2.min.js"></script> <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/socket.io/1.3.5/socket.io.min.js"></script> <script type="text/javascript" src="{{ url_for("static", filename="js/main.js")}}"></script> </head> <body> <form id="emit_event" method="post"> <input type="submit" value="emit"> </form> </body> </html>Dôležité sú 3 importy v hlavičke html dokumentu. Prvý importuje jQuery, druhý importuje knižnicu na prácu so socketmi socketio a posledný import je pre náš main.js súbor, ktorý musíme ešte vytvoriť. Inak, tento html dokument obsahuje len jeden formulár s jedným tlačítkom. To budeme používať na posielanie správy cez websocket. Vytvoríme priečinok static v ňom js a v ňom už konečne súbor main.js Obsah bude vyzerať asi takto: $(document).ready(function() { var url = location.protocol + "//" + document.domain + ":" + location.port var socket = io.connect(url); $("form#emit_event").submit(function(event) { socket.emit("event", "test message"); return false; }); });Toto je hlavná logika klientskej časti. Z tadeto budeme prijímať a posielať správy cez websockety tak isto ako na serverovej časti. Pomocou riadku var socket = io.connect(url); sa pripojím na môj server. Následne pomocou jQuery upravím správanie buttonu, aby pri stlačení poslal správu. Na to slúži funkcia socket.emit() Okej, základ máme hotový a môžeme teraz skúšať posielať správy. Aplikáciu spustím pomocou príkazu: (venv)$ python server.py WebSocket transport not available. Install eventlet or gevent and gevent-websocket for improved performance. * Serving Flask app "server" (lazy loading) * Environment: production WARNING: Do not use the development server in a production environment. Use a production WSGI server instead. * Debug mode: on * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) * Restarting with stat WebSocket transport not available. Install eventlet or gevent and gevent-websocket for improved performance. * Debugger is active! * Debugger PIN: 478-618-530Otvorím prehliadač na http://localhost:5000 a zobrazí sa mi jeden button. Keď ho stlačím na konzole mi vyskočí: test messagePoďme teda preskúmať, aké možnosti nám poskytuje táto knižnica socketio. Príjmanie správAko som už spomínal, prijímanie správ na oboch stranách prebieha ako event. V Pythone musíme pre takýto event definovať handler. V javascripte používame tzv. callbacky. V princípe ide o to isté, ale každý jazyk má svoje vlastné technické riešenie a my si toho musíme byť vedomí. Každý event, ktorý chcem prijať musím mať nejaké meno. V príklade sme mali názov event. Môžem ale použiť čokoľvek @socketio.on("foobar") def handle_data(data): print(type(data)) print(data)Taktiež dáta sa automaticky menia na príslušný typ. Ak v javascripte pošlem string, tak string dostanem aj na serveri. Tak isto to platí pre iné dátové typy ... $("form#emit_event").submit(function(event) { socket.emit("foobar", "message"); socket.emit("foobar", [1,2,3,]); socket.emit("foobar", {data : "message"}); return false; }); ...Po odoslaní udalostí dostanem výpis na serveri <class 'str'> message <class 'list'> [1, 2, 3] <class 'dict'> {'data': 'message'}Handler taktiež môže mať viacero argumentov @socketio.on("sum") def handle_sum(arg1, arg2): print(arg1 + arg2)Upravíme javascriptovú časť a zavoléme event s viacerými argumentami ... $("form#emit_event").submit(function(event) { socket.emit("sum", 23, 47); return false; }); ...Namespace patrí medzi ďalšie funkcie, ktoré mám knižnica SocketIO ponúka. Každý event si môžeme rozdeliť podľa namespaceov. To nám dáva ďalšie možnosti organizácie eventov. @socketio.on("sum", namespace="/math") def handle_sum(arg1, arg2): print(arg1 + arg2)Avšak pozor! Na strane klienta sa musíme teraz pripojiť na inú url $(document).ready(function() { var namespace = "/math"; var url = location.protocol + "//" + document.domain + ":" + location.port; var socket = io.connect(url + namespace); $("form#emit_event").submit(function(event) { socket.emit("sum", 23, 47); return false; }); });Ďalšia vychytávka je to, že každý event, ktorý pošleme, vie zavolať callback potom, čo sa vykonal. Napríklad z javascriptu pošlem nejaké dáta na server a server mi ešte dodatočne potvrdí, že dáta boli spracované. Aha takto ... $("form#emit_event").submit(function(event) { var ack = function(arg){console.log(arg)}; socket.emit("sum", 23, 47, ack); return false; }); ...Ak chcem, aby sa callback zavolal, musím v Pythone vrátiť nejakú hodnotu z vykonaného handlera => return True @socketio.on("sum", namespace="/math") def handle_sum(arg1, arg2): print(arg1 + arg2) return TrueMusím si otvoriť v prehliadači konzolu (ja používam chrome) a keď stlačím tlačítko, dostanem výpis na konzolu [Image] Posielanie správPosielať eventy sme už posielali, ale iba z javascriptu. V Pythone to vyzerá veľmi podobne. Používame 2 funkcie send a emit medzi ktorými je zásadný rozdiel. Najprv musíme importovať z knižnice flask-socketio from flask_socketio import send from flask_socketio import emitupravíme funkciu na sčítavanie @socketio.on("sum", namespace="/math") def handle_sum(arg1, arg2): value = arg1 + arg2 print("{} + {} = {}".format(arg1, arg2, value)) send(value)a pridáme handler v javascripte aby sme mohli tento event zachytiť. ... $("form#emit_event").submit(function(event) { socket.emit("sum", 23, 47); return false; }); socket.on("message", function(data){ console.log("received message: " + data) }); ...Všimni si, že teraz som použil handler, ktorý spracováva event s názvom message. Nie je to náhoda. Ide totiž o to, že funkcia send posiela tzv. unnamed event. Tieto eventy sa vždy posielajú na handler, ktorý spracúva message. Narozdiel od funkcie send, funkcia emit posiela už konkrétny event a musíš mu dať názov. Skúsme teda pozmeniť náš príklad @socketio.on("sum", namespace="/math") def handle_sum(arg1, arg2): value = arg1 + arg2 print("{} + {} = {}".format(arg1, arg2, value)) emit("result", value)... socket.on("result", function(data){ console.log("sum is: " + data) }); ...BroadcastingVeľmi užitočná funkcia je broadcastovanie, čo už z názvu vyplýva, že eventy sa budú vysielať na všetkých pripojených klientov. Dajme tomu, že zmeníme funciu emit na broadcastovanie @socketio.on("sum", namespace="/math") def handle_sum(arg1, arg2): value = arg1 + arg2 print("{} + {} = {}".format(arg1, arg2, value)) emit("result", value, broadcast=False)Teraz, keď si otvoríš 2 prehliadače a v jednom stlačíš button, výsledok súčtu sa ukáže vo všetkých prehliadačoch[Image] note: callbacky sa pri broadcastovaní nebudú vykonávať ZáverWebsockety majú mnoho využití. Tento článok bol len úvod a prehľad niektorých základných funkcií. V budúcom blogu spravíme malú aplikáciu postavenú na websocketoch. Máš nejaké otázky k článku? Napíš ju do komentára.
7 dôvodov, prečo sa ľudia na vašom webe neregistrujú
Tipy a triky
06.06.2018
Skillmea

7 dôvodov, prečo sa ľudia na vašom webe neregistrujú

Weby sú o biznise. A teraz nemám na mysli vývojárske firmy a mladé dynamické “desing studios”. Hovorím o biznise ako takom - o činnosti, pri ktorej sa vymieňajú hodnoty (ako peniaze a tovar) k vzájomnej spokojnosti oboch strán. Vy máte super obsah, návštevníci vašich stránok zase svoje kontaktné údaje. Tak si podáte ruky a dohodnete obchod. Moje články za tvoj e-mail. Jednoduché, nie? Nie. Registračný proces je vcelku zložitá záležitosť a nie vždy funguje tak, ako by sme si predstavovali. Web môže mať veľa návštevníkov, no len veľmi málo sa aj zaregistruje. Prečo? Nuž, keď sme web vymýšľali, mohli sme nevedomky urobiť pár chýb. Tak po prvé… 1. Sme v tom, že registračný proces rovná sa registračný formulár Samozrejme, že je dôležité mať pekný a použiteľný formulár. Názvy nad inputmi pre vertikálne skenovanie, absencia CAPTCHA pre duševný kľud, poznáme. Ale registrácia nezačína vypĺňaním políčok. Ak sme návštevníka dostali až sem, máme už vlastne spolovice vyhraté - registračný proces formulárom nezačína, ale končí. Ďalšou chybou teda môže byť, že… 2. Zabúdame na to, že registračný proces rovná sa motivácia “Ahoj, sme Triad a robíme efektívny digitálny marketing tak, aby to bavilo nás aj našich klientov”. Nejako takto by mohla vyzerať vaša value proposition (nechce sa mi hľadať slovenský preklad), ak by ste boli Triad. Lenže nie ste, tak si vymyslite vlastnú. Pamätajte, že návštevníci stránok sú sebci a zaujíma ich len to, čo im prinesiete. Kašlú na vašu misiu, víziu a počet šálok kávy vypitých od začiatku roku. Prečo by s vami mali robiť biznis, keď im nedáte jasne najavo, akú hodnotu im prinášate? Prečo by sa mali registrovať práve u vás? Povedzte im to. Jednoducho, zrozumiteľne, hlavne nepreháňajte. S tým súvisí ďalší bod. 3. Neosobné a nudné webové copy Ako hovorí Adam Javůrek, vo webovom copy sú sväté 3 slová: vy, váš a zdarma. Schválne, skúste použiť všetky. Buďte priateľskí, s užívateľmi sa rozprávajte. Gombík “Registrujte sa” je prívetivejší ako “Registrovať”, alebo “Registrácia”. Vyznačte dôležité časti, text štruktúrujte. Oh, a skráťte ho. Na polovicu. A potom ešte raz. To, čo je na vás super musíte vedieť povedať počas krátkej cesty výťahom. Je váš produkt alebo obsah zdarma? Povedzte to. A ešte raz - nie ste najlepší, najkrajší a neviete všetko. Svojim návštevníkom dajte jasný a zrozumiteľný dôvod, prečo sa u vás majú registrovať. Bez preháňania. Všetky kecy na svete však nenahradia osobnú skúsenosť - občas na to pozabudneme a potom vzniká… 4. Obsah skrytý pod registračnou hradbou To je tak. Kliknem na nadpis článku alebo odkaz v zmysle “zistiť viac” (moje obľúbené, BTW, keď nejdem ďalej čítať, ale “zisťovať”). Vyskočí na mňa modálne okno s formulárom a vrieska po mne čosi o registrácii. Skúsim ďalší odkaz, situácia sa opakuje. Odchádzam.  Povedzme, že zháňate cukríky a narazíte na dvoch predavačov. Benďo a Jožo. Benďo bude svoje cukríky vynášať do neba a opisovať ich úžasnú chuť. Jožo otvorí balíček a rovno vás ponúkne. Od koho si cukríky kúpite? Jožova prefíkaná obchodná taktika stojí na princípe reciprocity - ak dáš najprv niečo ty mne, ja budem viac ochotný dať niečo tebe. Preto ak máte super obsah, neskrývajte ho pred návštevníkmi. Ukážte im ho. S tým, že keď ho budú chcieť vidieť celý, nech sa zaregistrujú. Svoj e-mail vám poskytnú veľmi radi, ak budú vedieť, do čoho idú. Apropo, do čoho idú… 5. Príliš veľký záväzok Registrácia na webe je veľké rozhodnutie. Nie, vážne. Sú to cudzie stránky, neviem, kto ich vlastní. Komu dávam svoj e-mail? Na čo všetko ho použije? Čaká ma spambox plný super piluliek a členov nigérijskej kráľovskej rodiny? A načo chcú moje telefónne číslo? PSČ? Zbláznili sa? Tak. Buďte transparentní. Ľudia nemajú radi záväzky, ukážte im teda, že sa nemajú čoho báť. Budete im posielať newsletter? Tak im ho ukážte. Ako vyzerá, čo obsahuje a hlavne - ako často sa naňho môžu tešiť. Môžu si účet kedykoľvek zrušiť? Povedzte im to. Pridajte aj vetu o tom, že informácie neposkytnete tretej strane (a skúste to aj dodržať). A uistite ich, že to celé nezaberie viac ako minútu ich času. Ešte jedna vec. Veľa webov prichádza o obrovský zdroj dôvery v očiach ich potencionálnych užívateľov - robia jednu zásadnú chybu… 6. Skrývanie ostatných užívateľov Máte super web plný super obsahu, kde chodí veľa super ľudí? Ukážte ich. Čo všetko tam robia? Jožo práve číta článok o červených pandách? Benďo v špeciálnej aplikácii zistil, kde v jeho okolí sa pandy dajú skvelo pozorovať? Prečo sa nepochváliť? Možno tam dokonca nájdem nejakých kamošov z Facebooku. A prečo by som sa neregistroval, keď tam už sú moji známi, ktorí to preverili za mňa? Volá sa to social proof a viac o ňom zistíte v Googli. Nejaké knihy o tom zoženiete aj na Amazone. A vôbec, keď už o nich hovorím. Títo veľkí hráči míňajú milióny na UX testovanie, musia mať super premakanú registráciu. Spravíme to ako oni. Nemôže to dopadnúť zle. Alebo hej? 7. Opakovanie po ostatných Ak sa nevoláte Bezos (čo určite nie, lebo čítate článok v slovenčine), zapíšte si za uši: nie ste Amazon. Ani Google, ani Alza ani lokálny opravár plynových bojlerov. Vy ste vy. Pri navrhovaní registračného procesu treba vždy vychádzať z vlastnej situácie a vlastných skúseností. Internetoví giganti majú určite veľmi dobre zvládnutý registračný proces, no nikto z nás nevidí do ich cieľov, štatistík a stratégie. To čo funguje pre nich nemusí fungovať pre vás. Na druhej strane, inšpirovať sa niekde treba, takže…  Trochu inšpirácie Tu je niekoľko známych webových produktov, ktoré majú podľa mňa vynikajúco zvládnutý registračný proces. Čo majú spoločné, je predovšetkým jednoduchosť - jasne komunikujú, ako mi pomôžu (či už pomocou textu alebo obrázkov) a okamžite mi ponúknu možnosť ich bezbolestne začať používať. • Intercom • Basecamp • Dropbox Autorom blogu je Roman Pittner, lektor online kurzu Dizajn pre obrazovky. Ak máš k blogu otázky, neváhaj ich napísať do komentárov.

Nezmeškaj info o nových kurzoch a špeciálnych ponukách