Zpět na blog
Tipy a triky

Websockety - message board

Miroslav Beka
04.10.2018
23 minut čtení
Websockety - message board
 Ahoj, naposledy jsme mluvili o websocketech ve flasku. Používali jsme knihovnu flask-socketio a prošli jsme základní funkcionalitu. Tato knihovna používá koncept místností nebo rooms, který slouží k tomu, abychom uměli adresovat klienty v nějakých skupinách.
Tento koncept se používá v chatových aplikacích, kde uživatelé vidí zprávy jen v místnosti, ve které se nacházejí. Nedostanou zprávy z žádné jiné.

Podíváme se tedy na tento koncept a abychom udělali i nějaký reálný příklad, uděláme vlastní chatovací appku. Uživatelé se budou moci přidat do stávající místnosti, chatovat s ostatními, vytvářet nové místnosti a podobně. Bude to velice 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
Instalujeme závislosti. Budeme používat totéž, co v předchozím článku.
(venv)$ pip install flask, flask-socketio
Jedeme na boilerplatě pro naši appku. Struktura vypadá asi takto:
▾ websockets_message_board/
  ▾ static/
    ▾ css/
        main.css
    ▾ js/
        main.js
  ▾ templates/
      board.jinja
  ▸ venv/
    server.py
Soubory main.css a main.js jsou zatím prázdné, slouží pouze jako placeholder. Pokračujeme tedy se souborem server.py a lze jej naplnit kódem.
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)
Rozdíl oproti minimální flask appke je ten, že ji jinak spouštíme. Nepoužijeme
if __name__ == '__main__':
    app.run()
ale budeme ji spouštět přes socketIO.
if __name__ == '__main__':
    socketio.run(app, debug=True)
To proto, aby aplikace uměla spustit více vláken pro každého uživatele. Stejně tak je dobré vědět, že deployment na produkční server takové aplikace je trošku komplikovanější než když máme klasickou flask appku.

Obsah základního templejtu board.jinja (i jediného, který budeme používat) je následující:
<!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 pár důležitých importů jako socket.io, jquery a také css a js soubory naší appky.
Takový jednoduchý základ můžeme spustit a uvidíme, jestli všechno šlape jak 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 není vůbec potřebný, ale jelikož všichni mají rádi hezké věci, nainstalujeme si css framework zvaný semantic-ui. Je to fajn framework, mám s ním dobré zkušenosti. Dokumentace je možná trošku těžší na pochopení, ale kromě toho to funguje a hlavně vypadá moc hezky.
Stačí stáhnout toto zipko a integrovat do svého projektu. Je to velmi jednoduché. Zip rozbalíme a překopírujeme následující soubory
  • themes -> websockets_message_board/static/css/
  • semantic.min.css -> websockets_message_board/static/css/
  • semantic.min.js -> websockets_message_board/static/js/
Soubory semantic.min.js a semantic.min.css musím includnout na svou stránku, takže běžím do board.jinja a přihodím do hlavičky další řádky:
<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é dát si pozor, abychom nejprve přidali jquery a až pak semantic.min.js, jinak se mi semantic-ui bude stěžovat, že neví najít jquery knihovnu. Ve složce themes jsou hlavně ikony a nějaké obrázky, které semantic-ui poskytuje.

Po instalaci css frameworku můžu hned vidět změnu v podobě jiného fontu na mé smutné stránce. Nic jiného tam ještě není.

UI

Uděláme nyní přibližný náčrt UI, abych věděl, jak appka asi bude vypadat a jaké funkce jí vlastně uděláme. Nebude to nic světoborného. Budeme mít jednu stránku kterou rozdělím na 3 sekce. Hlavní bude obsahovat zprávy, takže to bude můj message board. Boční panel bude obsahovat seznam místností, do kterých se budu umět přepínat. No a na spodní liště bude input pro moji zprávu.

Zhmotním tuto svou představu do kódu. Otevřu board.jinja a naházím tam nějaké <div> elementy. Jelikož používáme semnatic-ui jako náš css framework, budu rovnou používat třídy v html. Použijeme grid systém, který nám usnadní práci při ukládání ui elementů.
<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ůžu zkusit naplnit tyto části i nějakým obsahem. Jen tak ze zvědavosti, jak to bude vypadat. Všechno bude zatím jen tak naoko (prototypování).
Začneme tím nejhlavnějším: message boardem
<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šechny zprávy jsem obalil do div s id msg_board, abych pak jednoduše uměl přidávat nové zprávy do tohoto elementu.

Uděláme totéž pro seznam místností. Rozhodl jsem se, že do tohoto postranního panelu strčíme i formulář pro změnu jména uživatele. Ten by měl mít možnost změnit své jméno. Bude to vypadat 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é jsem přidal <input /> na vytváření nových místností. Myslím, že takovou možnost by uživatel mohl mít.
Poslední skládačkou bude input pro naše zprá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álně mi nebudou fungovat radio buttony, protože semantic-ui potřebuje tyto inicializovat v javascriptu. Pome tedy na to. Otevřeme main.js a píšeme
$(document).ready(function(){
    // UI HANDLERS
    $('.ui.radio.checkbox').checkbox();
});
Stejně tak můžeme rovnou vybavit iniciální spojení přes websockety mezi klientem a serverem.
$(document).ready(function(){
    var url = location.protocol + "//" + document.domain + ":" + location.port;
    socket = io.connect(url);
    // UI HANDLERS
    $('.ui.radio.checkbox').checkbox();
});
Posílání zpráv mohu rovnou i vyzkoušet v konzoli prohlížeče. Stačí otevřít developer tools, přejít na záložku console a tam už můžeme psát
socket.emit("test", "hello there")

Nicméně, nic se neděje, protože můj backend dosud není vůbec připraven. Vrhneme se tedy na server side a implementujeme místnosti – room.

Rooms

Přesuneme se do souboru server.py a přidáme handler pro základní eventy které budeme používat: 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í velmi jednoduše. Pokaždé se podívám na data, která mi přišla (proměnná data) a vytvořím jednoduchou zprávu, kterou pak broadcastuji na všechny uživatele v té dané místnosti.

Pokud si už pořádně nepamatuješ, co dělal ten broadcast, vzpomínej z minulého blogu.

Důležité je použití funkcí join_room a leave_room. Tyto pocházejí z knihovny flask-socketio, kterou jsme instalovali na začátku. Slouží k tomu, abychom přiřadili danou session do nějaké místnosti. Potom, když pošlu zprávu do místnosti, dostanou ji všichni v té místnosti. Je to fajn mechanismus jak kontaktovat jiné klienty a uspořádat si je do nějakých kategorií.

rooms nemusím nutně používat jen na chatovou funkcionalitu. Mohu to použít k tomu, abych si seřadil uživatele do nějaké společné skupiny, které posílám barsjaká data.

Dejme tomu, že bych měl appku o počasí, a nějaká skupina uživatelů by měla zájem o notifikace, jestli bude pršet. Tak tyto bych hodil do společné skupiny - místnosti - a notifikace bych posílal jen jim. Využití je tedy všelijaké.

JavaScript

Backend byl v tomto případě docela jednoduchý a nepotřebovali jsme toho mnoho implementovat. Zprávy se od našeho backendu jen odrážejí jako od relátka, který je dále rozesílá klientům.
Na straně klienta toho bude trošku více. Pokračujeme v souboru main.js. Nyní se pokusíme implementovat posílání zprávy a zobrazení příchozí zprá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čátek vytvoříme nějaké random uživatelské jméno a zvolíme default místnost "Lobby". To abychom s tímto neměli starosti zatím. Používáme k tomu pomocné funkce, které si implementujeme stranou, aby nám nezavazovaly.

Jméno uživatele a název aktuální místnosti si udržuji v sessionStorage, což je fajn dočasné úložiště v prohlížeči. Přežije také reload stránky a navíc se mi tento způsob více líbí jak udržovat informaci v cookies.
Když máme potřebná data, můžeme se hned na začátku bouchnout do nějaké místnosti. V javascriptu používáme knihovnu socket.io, která ale žádný koncept místností nezná.

Pokud se podíváš do dokumentace(pozor! otevři si client api), zjistíš, že nic takového jako rooms se tam nezmiňuje. Takže to je věcička knihovny flask-socketio. Použijeme tedy klasický emit na handler join, který existuje na serveru.

Tento řádek $("form#send_msg_to_room").submit( se pomocí jquery napíchne na formulář a zachytí odeslání formuláře. Pak můžu dělat co se mi zachce a nakonec vrátím false, takže formulář se reálně ani neodešle.
Odeslání zprávy je přímočaré. Zjistím UserName, zjistím RoomName, vytáhnu si text zprávy a vše pošlu do funkce sendMessage.

Tato již zajistí zabalení informací do jsonu a posílám pomocí funkce emit. Posílám na handler msg_board, který jsem si udělal před chvilkou.
Zbývá mi vyřešit přijetí zprávy. To dělám pomocí funkce socket.on, kde dám kód, který bude proveden při přijetí zprávy. Tady si jednoduše (ale zato strašně ošklivě) slepím kus HTML, které pak strčím na konec elementu s id msg_board.
Než to budeš zkoušet, je fajn si ještě vymazat ty fejkové zprávy, které jsme tam dali natvrdo do HTML. Takže mažeme tyto řádky
        <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 tedy jako další věc vybavit změnu uživatelského jména.
$(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 jako při posílání zprávy, napíchnu se na HTML formulář a zpracuji ho ještě před odesláním. Změny uložím do sessionStorage.
Přidal jsem ještě 2 vychytávky.
  • funkce updateHeading nastaví aktuální název místnosti a uživatele jako hlavičku stránky,
  • notifyNameChange dá všem uživatelům vědět, že si někdo změnil jméno.
Jméno si už můžu měnit, ale oznámení o změně jsem nedostal. Na to ještě musíme doplnit 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);
    });
});
...
Nyní se nám začnou zobrazovat i systémové notifikace o tom, co se děje. Kdo vešel do místnosti, kdo ji opustil nebo kdo si změnil jméno.
Poslední věcí, kterou musíme udělat, je selekce místností. Toto bude vyžadovat trošku více práce. Seznam stávajících místností si musíme udržovat na backendu. Ani na klientské části ani na backendu z knihovny flask-socketio neumím získat seznam všech místností. Musím si ho tedy udržovat 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 jsem si začal posílat nějaká data. Vyhodím tedy ty fejkové, které jsou tam natvrdo, a uděláme loop, ve kterém přidám všechny stávající místnosti.
<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čuji v souboru main.js, kde si vytvořím funkce, které se postarají o změnu místnosti + pokud byla vytvořena nová, tak ji přidám do seznamu.
$(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 zde několik pomocných funkcí, které mi pomáhají při výběru místnosti nebo při vytváření nové. Problematické části nastávají právě tehdy, když chci místnost i vytvářet. V podstatě ale nejde o žádné komplikované věci.

Funkce selectCurrentRoom mi pomůže přehodit radio button při změně místnosti. Tím, že používáme semantic-ui, tak se nám to také trošku zkomplikovalo, ale výsledek stojí za to.

Závěr

Postavili jsme takzvaný proof of concept, udělali jsme chatovací appku jen pomocí websocketů. Není to dokonalé a určitě je tam spousta much, to nám však nebránilo pochopit jak fungují websockety. Všechny zprávy žijí pouze v prohlížeči uživatele a nejsou uloženy na žádném serveru. Někdo to může považovat za chybu, někdo za fičúru. To už nechám na vás.

Celý projekt se dá stáhnout zde.

Zanedlouho se opět vrhneme na nějaké zajímavé téma ;)
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 tě zajímat

Java 8
Tipy a triky
19.09.2018
Skillmea

Java 8

Java 8 je velký balíček nové funkcionality oproti Java 9 a java 10. Ty jsou menší a odrážejí taktiku tvůrců jevy vydávat nové verze častěji s menším balíkem nové funkcionality. V tomto článku si řekneme nejvýraznější změny z Java 8. Funkcionální rozhraníRozhraní, které má jen jednu abstraktní metodu je považováno za funkcionální rozhraní. Pro lepší nastínění v kódu můžeme a je doporučeno přidat anotaci @FunctionalInterface. import java.lang.FunctionalInterface;   @FunctionalInterface public interface FunctionalInterfaceExample { void print(); } Lambda výrazyOk, chceme přidat do jevy možnost definovat funkcionalitu, aniž by patřila specificky pod nějakou třídu. Tyto funkce chceme vkládat do metod jako parametry. Co uděláme? Tvůrci jevy se asi takhle nezamýšleli, ale my jsme se takto zamysleli a odpověď je – použijeme lambda výrazy. Každá lambda má jako typ funkcionální rozhraní s její odpovídající metodou. V příkladu výše máme rozhraní s metodou print. Tato metoda nevrací nic a nepřijímá žádné parametry. Musíme vytvořit i takový lambda výraz – tedy nebude obsahovat return a nebudeme mít žádné návratové hodnoty. () -> System.out.println("Hello Word")Takto vložíš lambda výraz do metody: printHelloWord(() -> System.out.println("Hello Word"));Samotná metoda přijímá na vstupu právě dané funkcionální rozhraní: public void printHelloWord2(FunctionalInterfaceExample printHello)Více o lambda výrazech se dozvíš z mého online kurzu Java pro pokročilé, videa o lambda výrazech jsou zdarma. StreamsStreamy poskytují zjednodušenou práci s datovými kolekcemi. Poskytují několik užitečných metod jako jsou filtr, sorted, map a jiné. Pokud stream přejede přes všechny elementy v kolekci, tak bude prázdný. Tedy pokud chci znovu použít tentýž stream, tak zbytečně, neboť tam už nebudou data. Musíš si vytvořit nový stream. Nový stream vytvoříš například pomocí stream metody nad kolekcemi.  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áš seznam osob a chceš z daného seznamu získat všechny věky osob starších 50 let a mají být seřazeny podle věku. Co uděláš? Použiješ stream a jeho metody – tyto metody akceptují funkcionální rozhraní, tedy 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());Kolekce, které jsou iterovatelné, mají metodu forEach – která získá stream a projde všechny elementy. veky.forEach(System.out::println);   Method referenceČesky reference na metodu. Všimni si příkladu shora: Osoba::getVěk. Znamená to, že ze třídy Osoba použij metodu getVek. Totéž lze zapsat i pomocí lambda výrazu takto osoba -> osoba.getVek(). Ale pomocí reference si to zjednodušíme neboť java ví, jaký je vstupní parametr – je to osoba a ví, že voláš getVek. Tak stačí napsat Osoba::getVek. Metodu voláš bez závorek. Další článek o Javě již brzy. Přihlas se k odběru novinek a nezmeškej žádný nový blog.
Websockety ve Flasku
Tipy a triky
15.08.2018
Miroslav Beka

Websockety ve Flasku

Websockety ve Flasku Pokud ses někdy setkal s výrazem websocket a chtěl by ses dozvědět, co to vlastně je a jak se to používá v Python aplikaci, tak tento článek je právě pro tebe. Standardně tvůj prohlížeč komunikuje na webu pomocí http protokolu. Klasický http protokol nabízí jednoduchou komunikaci. Pošle se request a jako odpověď dostanu response. Tento klasický komunikační způsob nebyl dostačující pro dnešní moderní aplikace. Byla potřeba pro komunikační kanál, který bude sloužit k obousměrné komunikaci. HTTP by měl být víceméně bezstavový a klient a server mezi sebou komunikují jen když je třeba, jinak je spojení mezi nimi uzavřeno. Navíc, prohlížeč (klient) musí požádat server o komunikaci a server může na tuto žádost odpovědět. Ta žádost, to je ten http request. Jinak server neumí kontaktovat klienta jen tak sám od sebe. U websocketů je tomu jinak. Jedná se o komunikační kanál, který se otevře jednou, na začátku a poté se používá ke komunikaci klienta a serveru v obou stranách. To znamená, že server může posílat data zároveň co klient posílá data na server. Toto se odborně jmenuje full-duplex. Web socket má menší overheat přenosu dat, umí být real-time a hlavně, server může posílat data na klienta, aniž by si je klient musel explicitně vyžádat requestem. Toto je užitečné například u aplikací, které zobrazují real time data a server posílá tato data klientovi. Takže pokud nastane nějaká změna dat, server je prostě pošle na klienta. Toto dříve nebylo možné provést pouze pomocí http protokolu. Minimální pří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: Nejlepší je vyzkoušet si tyto koncepty v praxi. Dnes budeme pracovat s Flaskem, knihovnou SocketIO a javascript knihovnami socket.io a jQuery. Budu předpokládat, že Flask aplikace alespoň trochu znáš. Začneme tím, že si vytvoříme nové virtuální prostředí: $ mkdir websockets_primer $ cd websockets_primer $ virtualenv venv $ . venv/bin/activate (venv) $Nainstalujeme závislosti, které budeme potřebovat: (venv)$ pip install flask, flask-socketioV době psaní tohoto článku jsem používal verze Flask==1.0.2 a Flask-SocketIO=3.0.1. Když už máme připravené prostředí a nainstalované závislosti, uděláme nový soubor 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čátku máme importy jako pro každou jinou Flask aplikaci, avšak přibylo nám tam  from flask_socketio import SocketIO. Tento naimportovaný modul je v podstatě totéž jako jiné  Flask rozšíření . Inicializaci websocketů ve Flask aplikací provedeme pomocí řádku  socketio = SocketIO(app). Pomocí tohoto objektu  socketio budeme přijímat a odesílat zprávy. Minimální aplikace by měla mít alespoň jednu stránku. V našem případě to bude  index.jinja. Toto je třeba, protože musíme poskytnout i klientskou část naší aplikace. Tam bude javascript knihovna  socketio  a nějaké další funkce. Websockety umí přijímat a posílat zprávy. Provedeme zatím jen přijímání zpráv. Pomocí řádku  socketio.on("event")definuji handler pro událost  event. V tomto případě jednoduše vypíšu data na konzoli. @socketio.on("event") def handle_event(data): print(data) Posílání a přijímání dat na obou stranách (klient a server) probíhá jako event. Toto je důležitý fakt, protože architektura aplikace založené na eventech ( event driven architecture ) funguje trošku jinak než klasické volání funkce. Neříkám, abys měl z toho paniku teď, ale měj to na paměti. Pokud znáš Flask aplikace, tak spuštění appky vypadá většinou takto if __name__ == "__main__": app.run("0.0.0.0", debug=True) My ale musíme appku spustit jinak, jelikož používáme websockety. Spustíme ji pomocí objektu socketio, který jsme si vytvořili na začátku. if __name__ == '__main__': socketio.run(app, debug=True) Nyní musíme ještě vytvořit 2 soubory. Snažíme se renderovat  index.jinja a také musíme vytvořit hlavní javascript soubor, do kterého budeme psát klientskou část naší websocketové ukázky. Vytvořím složku  templates a do ní soubor 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é jsou 3 importy v hlavičce html dokumentu. První importuje  jQuery , druhý importuje knihovnu pro práci se sockety  socketio  a poslední import je pro náš  main.js  soubor, který musíme ještě vytvořit. Jinak tento html dokument obsahuje pouze jeden formulář s jedním tlačítkem. To budeme používat k posílání zprávy přes websocket. Vytvoříme složku  static v ní  js a v ní už konečně soubor main.js Obsah bude vypadat 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 klientské části. Z tadeto budeme přijímat a posílat zprávy přes websockety stejně jako na serverové části. Pomocí řádku  var socket = io.connect(url); se připojím na můj server. Následně pomocí  jQuery  upravím chování buttonu, aby při stisku poslal zprávu. K tomu slouží funkce socket.emit() Okej, základ máme hotový a můžeme nyní zkoušet posílat zprávy. Aplikaci spustím pomocí pří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-530Otevřu prohlížeč na  http://localhost:5000  a zobrazí se mi jeden button. Když ho zmáčknu na konzole mi vyskočí: test messagePojďme tedy prozkoumat, jaké možnosti nám poskytuje tato knihovna  socketio. Přijímání zprávJak jsem již zmiňoval, přijímání zpráv na obou stranách probíhá jako event. V Pythonu musíme pro takovýto event definovat handler. V javascriptu používáme tvz. callbacky. V principu jde o totéž, ale každý jazyk má své vlastní technické řešení a my si toho musíme být vědomi. Každý event, který chci přijmout musím mít nějaké jméno. V příkladu jsme měli název  event. Mohu ale použít cokoli @socketio.on("foobar") def handle_data(data): print(type(data)) print(data)Také data se automaticky mění na příslušný typ. Pokud v javascriptu pošlu string, tak string dostanu i na serveru. Totéž platí pro jiné datové 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 odeslání událostí dostanu výpis na serveru <class 'str'> message <class 'list'> [1, 2, 3] <class 'dict'> {'data': 'message'}Handler také může mít několik argumentů @socketio.on("sum") def handle_sum(arg1, arg2): print(arg1 + arg2)Upravíme javascriptovou část a zavoleme event s více argumenty ... $("form#emit_event").submit(function(event) { socket.emit("sum", 23, 47); return false; }); ...Namespace patří mezi další funkce, které mám knihovna SocketIO nabízí. Každý event si můžeme rozdělit podle namespace. To nám dává další možnosti organizace eventov. @socketio.on("sum", namespace="/math") def handle_sum(arg1, arg2): print(arg1 + arg2)Ovšem pozor! Na straně klienta se musíme nyní připojit na jinou 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; }); });Další vychytávka je to, že každý event, který pošleme, umí zavolat callback poté, co byl proveden. Například z javascriptu pošlu nějaká data na server a server mi ještě dodatečně potvrdí, že data byla zpracována. Aha takhle ... $("form#emit_event").submit(function(event) { var ack = function(arg){console.log(arg)}; socket.emit("sum", 23, 47, ack); return false; }); ...Pokud chci, aby se callback zavolal, musím v Pythonu vrátit nějakou hodnotu z provedeného handleru => return True @socketio.on("sum", namespace="/math") def handle_sum(arg1, arg2): print(arg1 + arg2) return TrueMusím si otevřít v prohlížeči konzoli (já používám chrome) a když zmáčknu tlačítko, dostanu výpis na konzoli[Image] Posílání zprávZasílat eventy jsme již posílali, ale pouze z javascriptu. V Pythonu to vypadá velmi podobně. Používáme 2 funkce  send a  emit mezi nimiž je zásadní rozdíl. Nejprve musíme importovat z knihovny flask-socketio from flask_socketio import send from flask_socketio import emitupravíme funkci na sčítání @socketio.on("sum", namespace="/math") def handle_sum(arg1, arg2): value = arg1 + arg2 print("{} + {} = {}".format(arg1, arg2, value)) send(value)a přidáme handler v javascriptu abychom mohli tento event zachytit. ... $("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 teď jsem použil handler, který zpracovává event s názvem  message. Není to náhoda. Jde totiž o to, že funkce  send posílá tvz. unnamed event . Tyto eventy se vždy posílají na handler, který zpracovává  message. Narozdíl od funkce  send, funkce  emit posílá již konkrétní event a musíš mu dát název. Zkusme tedy pozměnit náš pří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) }); ...BroadcastingVelmi užitečná funkce je broadcastování, což už z názvu vyplývá, že eventy se budou vysílat na všechny připojené klienty. Dejme tomu, že změníme funkci  emit na broadcastování @socketio.on("sum", namespace="/math") def handle_sum(arg1, arg2): value = arg1 + arg2 print("{} + {} = {}".format(arg1, arg2, value)) emit("result", value, broadcast=False)Nyní, když si otevřeš 2 prohlížeče a v jednom zmáčkneš button, výsledek součtu se ukáže ve všech prohlížečích[Image] note: callbacky se při broadcastování nebudou provádět ZávěrWebsockety mají mnoho využití. Tento článek byl jen úvod a přehled některých základních funkcí. V příštím blogu uděláme malou aplikaci postavenou na websocketech. Máš nějaké dotazy k článku? Napiš ji do komentáře.
7 důvodů, proč se lidé na vašem webu neregistrují
Tipy a triky
06.06.2018
Skillmea

7 důvodů, proč se lidé na vašem webu neregistrují

Weby jsou o byznysu. A teď nemám na mysli vývojářské firmy a mladé dynamické „desing studios“. Mluvím o byznysu jako takovém - o činnosti, při které se vyměňují hodnoty (jako peníze a zboží) ke vzájemné spokojenosti obou stran. Vy máte super obsah, návštěvníci vašich stránek zase své kontaktní údaje. Tak si podáte ruce a domluvíte obchod. Mé články za tvůj e-mail. Jednoduché, ne? Ne. Registrační proces je vcelku složitá záležitost a ne vždy funguje tak, jak bychom si představovali. Web může mít mnoho návštěvníků, ale jen velmi málo se i zaregistruje. Proč? Inu, když jsme web vymýšleli, mohli jsme nevědomky udělat pár chyb. Tak zaprvé… 1. Jsme v tom, že registrační proces rovná se registrační formulář Samozřejmě, že je důležité mít pěkný a použitelný formulář. Názvy nad inputy pro vertikální skenování, absence CAPTCHA pro duševní klid, známe. Ale registrace nezačíná vyplňováním políček. Pokud jsme návštěvníka dostali až sem, máme už vlastně zpola vyhráno - registrační proces formulářem nezačíná, ale končí. Další chybou tedy může být, že… 2. Zapomínáme na to, že registrační proces rovná se motivace „Ahoj, jsme Triad a děláme efektivní digitální marketing tak, aby to bavilo nás i naše klienty“. Nějak takhle by mohla vypadat vaše value proposition (nechce se mi hledat český překlad), kdybyste byli Triad. Jenže nejste, tak si vymyslete vlastní. Pamatujte, že návštěvníci stránek jsou sobci a zajímá je jen to, co jim přinesete. Kašlou na vaši misi, vizi a počet šálků kávy vypitých od začátku roku. Proč by s vámi měli dělat byznys, když jim nedáte jasně najevo, jakou hodnotu jim přinášíte? Proč by se měli registrovat právě u vás? Řekněte jim to. Jednoduše, srozumitelně, hlavně nepřehánějte. S tím souvisí další bod. 3. Neosobní a nudné webové copy Jak říká Adam Javůrek, ve webovém copy jsou svatá 3 slova: vy, váš a zdarma. Schválně, zkuste použít všechny. Buďte přátelští, s uživateli si povídejte. Knoflík “Registrujte se” je přívětivější než “Registrovat”, nebo “Registrace”. Vyznačte důležité části, text strukturujte. Oh, a zkraťte ho. Na polovinu. A pak ještě jednou. To, co je na vás super musíte umět sdělit během krátké cesty výtahem. Je váš produkt nebo obsah zdarma? Řekněte to. A ještě jednou - nejste nejlepší, nejkrásnější a nevíte všechno. Svým návštěvníkům dejte jasný a srozumitelný důvod, proč se u vás mají registrovat. Bez nadsázky. Všechny kecy na světě však nenahradí osobní zkušenost - občas na to pozapomeneme a pak vzniká… 4. Obsah skrytý pod registrační hradbou To je tak. Kliknu na nadpis článku nebo odkaz ve smyslu “zjistit více” (moje oblíbené, BTW, když nejdu dále číst, ale “zjišťovat”). Vyskočí na mě modální okno s formulářem a vříská po mně cosi o registraci. Zkusím další odkaz, situace se opakuje. Odcházím. Řekněme, že sháníte bonbóny a narazíte na dva prodavače. Benďo a Jožo. Benďo bude své bonbóny vynášet do nebe a popisovat jejich úžasnou chuť. Jožo otevře balíček a rovnou vás nabídne. Od koho si bonbóny koupíte? Jožova mazaná obchodní taktika stojí na principu reciprocity - pokud dáš nejprve něco ty mně, já budu víc ochoten dát něco tobě. Proto pokud máte super obsah, neskrývejte jej před návštěvníky. Ukažte jim ho. S tím, že když ho budou chtít vidět celý, ať se zaregistrují. Svůj e-mail vám poskytnou velmi rádi, budou-li vědět, do čeho jdou. Apropo, do čeho jdou… 5. Příliš velký závazek Registrace na webu je velké rozhodnutí. Ne, vážně. Jsou to cizí stránky, nevím, kdo je vlastní. Komu dávám svůj e-mail? Na co všechno jej použije? Čeká mě spambox plný super pilulek a členů nigerijské královské rodiny? A k čemu chtějí mé telefonní číslo? PSČ? Zbláznili se? Tak. Buďte transparentní. Lidé nemají rádi závazky, ukažte jim tedy, že se nemají čeho bát. Budete jim posílat newsletter? Tak jim ho ukažte. Jak vypadá, co obsahuje a hlavně – jak často se na něj mohou těšit. Mohou si účet kdykoli zrušit? Řekněte jim to. Přidejte také větu o tom, že informace neposkytnete třetí straně (a zkuste to i dodržet). A ujistěte je, že to celé nezabere více než minutu jejich času. Ještě jedna věc. Spousta webů přichází o obrovský zdroj důvěry v očích jejich potencionálních uživatelů - dělají jednu zásadní chybu… 6. Skrývání ostatních uživatelů Máte super web plný super obsahu, kde chodí spousta super lidí? Ukažte je. Co všechno tam dělají? Jožo právě čte článek o červených pandách? Benďo ve speciální aplikaci zjistil, kde v jeho okolí se pandy dají skvěle pozorovat? Proč se nepochlubit? Možná tam dokonce najdu nějaké kámoše z Facebooku. A proč bych se neregistroval, když tam už jsou moji známí, kteří to prověřili za mě? Jmenuje se to social proof a více o něm zjistíte v Googlu. Nějaké knihy o tom seženete i na Amazonu. A vůbec, když už o nich mluvím. Tito velcí hráči utrácejí miliony na UX testování, musí mít super promakanou registraci. Uděláme to jako oni. Nemůže to dopadnout špatně. Nebo hej? 7. Opakování po ostatních Pokud se nevoláte Bezos (což určitě ne, neboť čtete článek v češtině), zapište si za uši: nejste Amazon. Ani Google, ani Alza ani lokální opravář plynových bojlerů. Vy jste vy. Při navrhování registračního procesu je třeba vždy vycházet z vlastní situace a vlastních zkušeností. Internetoví giganti mají určitě velmi dobře zvládnutý registrační proces, ale nikdo z nás nevidí do jejich cílů, statistik a strategie. To co funguje pro ně nemusí fungovat pro vás. Na druhé straně, inspirovat se někde třeba, takže… Trochu inspirace Zde je několik známých webových produktů, které mají podle mě skvěle zvládnutý registrační proces. Co mají společného, ​​je především jednoduchost – jasně komunikují, jak mi pomohou (ať už pomocí textu nebo obrázků) a okamžitě mi nabídnou možnost je bezbolestně začít používat. • Intercom • Basecamp • Dropbox Autorem blogu je Roman Pittner , lektor online kurzu Design pro obrazovky . Pokud máš k blogu dotazy, neváhej je napsat do komentářů.

Nezmeškej info o nových kurzech a speciálních nabídkách