Sam & Max » javascript http://sametmax.com Du code, du cul Sat, 07 Nov 2015 10:56:13 +0000 en-US hourly 1 http://wordpress.org/?v=4.1 Aux couleurs du site 11 http://sametmax.com/aux-couleurs-du-site/ http://sametmax.com/aux-couleurs-du-site/#comments Wed, 22 Jul 2015 10:41:26 +0000 http://sametmax.com/?p=16645 multiboards.net Max et moi, et il a voulu ajouter un feature rigolote : les flux sont affichés avec une palette de couleur proche de celle du site. ]]> On est en train de tweaker un peu multiboards.net Max et moi, et il a voulu ajouter un feature rigolote : les flux sont affichés avec une palette de couleurs proche de celle du site.

Comme récupérer les couleurs d’un site est compliqué, on s’est rabattus sur les couleurs du favicon, qui en théorie doit résumer l’identité d’un site. Ce n’est pas parfait, mais c’est plus simple à implémenter.

Au début, on avait écrit une solution avec Pillow et numpy. Ça marchait très bien, mais des extensions compilées juste pour faire de la déco c’était un peu lourd, surtout si un jour on libère le code source (mais vu comme les sources sont dégueues et qu’on a pas de doc, pour le moment c’est pas gagné).

On a donc changé notre fusil d’épaule, et on a choisit une nouvelle technique : faire la moitié du taff côté client.

Récupérer le favicon côté serveur

On ne peut pas faire des requêtes cross domain en JS, donc on doit toujours récupérer l’image côté serveur. get_favicon_url() parse le site à coup de regex (c’est mal mais ça évite de rajouter BeautifulSoup en dépendance pour une balise) pour récupérer son URL sur la page donnée, ou à défaut, sur la page à la racine du site. On se donne plusieurs essais en cas d’erreurs de réseau :

 
import urllib
import urlparse 
 
def get_favicon_url(url, retry=3, try_home_page=True):
    """ Try to find a favicon url on the given page """
 
    parsed_url = urlparse.urlparse(url)
    url_root = "%s://%s" % (parsed_url.scheme, parsed_url.netloc)
 
    try:
        # try to get it using a regex on the current URL
        html = fetch_url(url, retry=retry)
        html = html.decode('ascii', errors='ignore')
        pattern = r"""
                               href=(?:"|')\s*
                               (?P<favicon>[^\s'"]*favicon.[a-zA-Z]{3,4})
                               \s*(?:"|')
                          """
 
        match = re.search(pattern, html, re.U|re.VERBOSE)
        favicon_url = match.groups()[0]
 
    except IOError:
        # this is a network error so eventually, let it crash
        if not try_home_page:
            raise
        # try with the home page, maybe this one is accessible
        favicon_url = get_favicon_url(url_root, retry=retry,
                                                       try_home_page=False)
    except (IndexError, AttributeError):
        # url is not on this page, try the home page
        if try_home_page:
            return get_favicon_url(url_root, retry=retry, try_home_page=False)
 
        # can't find the favicon url, default to standard url
        favicon_url = '/favicon.ico'
 
    # make sure to have the domain of the original website in the favicon url
    if url_root not in favicon_url:
        favicon_url = "%s/%s" % (url_root, favicon_url.lstrip('/'))
 
    return favicon_url
 
 
def fetch_favicon(url, retry=3):
    """ Returns the bytes of the favicon of this site """
    favicon_url = get_favicon_url(url, retry=retry)
    return fetch_url(favicon_url, retry=retry)

fetch_favicon() télécharge l’image proprement dite et retourne les bits tels quels. Notez que tout ça est synchrone et bloquant, mais heureusement le traffic de multiboards est sans doute trop faible pour poser problème.

Exposer le résultat au client

Ensuite il faut faire le lien entre le client et le serveur. On fait donc un petit routing (c’est du bottle) qui va retourner tout ça en base64 afin qu’on puisse l’utiliser directement dans un objet Image JS :

 
import base64
from bottle import post, HTTPError
 
@post('/favicon')
def fetch_favicon_base64():
    """ Return the favicon URL from a website """
    try:
        url = request.POST['url']
    except KeyError:
        raise HTTPError(400, "You must pass a site URL")
    try:
        # return favicon as base64 url to be easily included in
        # a img tag
        favicon = fetch_favicon(url)
        return b'data:image/x-icon;base64,' + base64.b64encode(favicon)
    except (IOError):
        raise HTTPError(400, "Unable to find any favicon URL")

Récupérer la palette côté client

Avec un peu d’AJAX sur notre vue, on récupère les bits de l’image, et on fout le tout dans un objet Image qu’on passe à la lib ColorThief. Cette dernière nous renvoie un array avec la palette. On convertit tout ça en code hexadécimal, et on diminue la luminosité de certaines couleurs pour rendre le texte plus lisible.

 $.post('/favicon', {url: url}).done(function(data){
 
        // load an image with the base64 data of the favicon
        var image = new Image;
        image.src = data;
        image.onload = function() {
 
            // ask ColorThief for the main favicon colors
            var colorThief = new ColorThief();
            var bc = colorThief.getPalette(image);
 
            // some tools to convert the RGB array into
            // rgb hex string
            function componentToHex(c) {
                var hex = c.toString(16);
                return hex.length == 1 ? "0" + hex : hex;
            }
 
            function rgbToHex(array) {
                return componentToHex(array[0]) + componentToHex(array[1]) + componentToHex(array[2]);
            }
 
            // make the color brigther
            function increase_brightness(hex, percent){
 
                // convert 3 char codes --> 6, e.g. `E0F` --> `EE00FF`
                if(hex.length == 3){
                    hex = hex.replace(/(.)/g, '$1$1');
                }
 
                var r = parseInt(hex.substr(0, 2), 16),
                    g = parseInt(hex.substr(2, 2), 16),
                    b = parseInt(hex.substr(4, 2), 16);
 
                return '' +
                   ((0|(1<<8) + r + (256 - r) * percent / 100).toString(16)).substr(1) +
                   ((0|(1<<8) + g + (256 - g) * percent / 100).toString(16)).substr(1) +
                   ((0|(1<<8) + b + (256 - b) * percent / 100).toString(16)).substr(1);
            }
 
            /* Define board colors */
            var header =  '#' + rgbToHex(bc[0]);
            var odd = '#' + increase_brightness(rgbToHex(bc[1]), 90);
            var even = '#' + increase_brightness(rgbToHex(bc[2]), 90);
 
});

Est-ce que la feature est utile ? Je ne sais pas, c’est un peu gadget, mais c’était marrant à faire, et c’est vrai qu’on identifie bien chaque site sur la page du coup.

L’idée ici est que le serveur et le client travaillent en équipe, et que grâce au navigateur on a une stack de traitement image/audio/video à portée de code. D’ailleurs on a viré le plugin flash des radios pour le remplacer par du chteumeuleu 5.

On peut imaginer la même chose avec tous les outils côtés en JS, ceci incluant les préprocesseurs et minifieurs JS ou CSS : pas besoin d’installer Node pour faire tourner tout ça, il suffit de déléguer le boulot à un tab du browser. Pour améliorer la stack, on pourrait par ailleurs utiliser du RPC avec WAMP et même prévenir le navigateur quand un fichier est prêt avec PUB/SUB afin qu’il le recharge. Vous avez vu comme j’arrive toujours à placer crossbar ?

J’ai un pote qui va venir dans quelques jours pour creuser cette idée qui permettrait d’avoir en pur Python une sorte de livereload sans extension, sans gros GUI, qui transpile tout et rafraichit tout avec un simple fichier de config. Peut être aurons nous le temps de faire un proto.

C’était l’inspiration du jour, passez une excellente canicule !

]]>
http://sametmax.com/aux-couleurs-du-site/feed/ 11
La mort du tuto angular 27 http://sametmax.com/la-mort-du-tuto-angular/ http://sametmax.com/la-mort-du-tuto-angular/#comments Sun, 14 Jun 2015 18:25:29 +0000 http://sametmax.com/?p=16389 J'avais demandé si il y avait encore des gens intéressés par le tuto angularjs que j'avais commencé. En effet les ressources sur Angular sont généralement de mauvaise qualité, et la réponse avait été un gros OUI.]]> J’avais demandé si il y avait encore des gens intéressés par le tuto angularjs que j’avais commencé. En effet les ressources sur Angular sont généralement de mauvaise qualité, et la réponse avait été un gros OUI.

Je m’étais donc mis en tête de continuer. Malgré la mort annoncée du framework. Malgré une V2 qui va tout casser.

Voyez-vous, j’aime Angular. Bien que je pense que pour des grosses apps une approche comme ReactJS + BackboneJS est plus saine, pour une petit app rapide ou du prototypage c’est très productif et pratique.

En plus, je n’aime vraiment pas manquer à mes engagements, et en presque 3 ans de ce blog, je les ai tenu. J’ai toujours publié les articles promis. Toujours répondu aux mails. Parfois avec des mois de retard, certes, mais je n’ai signé aucun contrat moral sur les deadlines :)

Je vais néanmoins faire une exception ici et envoyer le tuto Angular au cimetière. Toute mes excuses à ceux qui l’attendaient.

Bien sûr il y a le fait que j’ai beaucoup de travail. Cependant si c’était la cause principale je n’écrirais plus ici car ça me demande des heures et des heures chaque semaine.

C’est surtout qu’il se trouve que j’avorte régulièrement des tentatives de retravailler dessus. C’est un signe de manque de motivation certain, et je viens d’en comprendre aujourd’hui la raison : c’est du Javascript.

Jusqu’ici le blog s’était autorisé régulièrement des sorties de la ligne éditoriale, mais un tel travail, un guide complet sur Angular, enterine le JS comme un sujet majeur du site.

Or ce n’est pas le cas.

Je n’aime pas Javascript.

Du coup non seulement me motiver à faire le dossier me demande une énorme énergie (qui pour le moment se transforme en procrastination coupable), mais en prime je me dis que c’est dépenser des ressources pour faire le taff d’une communauté qu’au final je ne supporte que par obligation professionnelle.

Le résultat est donc que bien que je vais continuer à parler de JS et à coder en JS (programmation Web oblige), je ne vais pas lui accorder autant de place sur S&M ou dans mes heures de loisir.

Je remercie ceux qui prennent le temps de le faire, après tout je profite de leur travail. De mon côté, je vais me concentrer sur les domaines qui me sont plus chers, et parler d’autre chose uniquement de manière ponctuelle. L’investissement me parait plus judicieux.

]]>
http://sametmax.com/la-mort-du-tuto-angular/feed/ 27
Je me te tâte à arrêter le tuto Angular 35 http://sametmax.com/je-me-te-tate-a-arreter-le-tuto-angular/ http://sametmax.com/je-me-te-tate-a-arreter-le-tuto-angular/#comments Thu, 15 Jan 2015 04:25:58 +0000 http://sametmax.com/?p=15736 critiques, j'aime beaucoup AngularJS. Mais à l'annonce de la version 2 d'Angular complètement incompatible avec la version 1, seulement quelques années après sa sorties, des questions se sont posées sur l'avenir du framework.]]> Malgré les critiques, j’aime beaucoup AngularJS. Mais à l’annonce de la version 2 d’Angular complètement incompatible avec la version 1, seulement quelques années après sa sortie, des questions se sont posées sur l’avenir du framework.

J’utilise AngularJS pour certains de mes projets, je suis payé pour former sur AngularJS et j’ai commencé à écrire un guide sur le sujet.

Mais je me demande franchement si ça vaut le coup de s’investir plus.

Je vais continuer à l’utiliser pour mes projets actuels, tout en regardant du côté de la concurrence ici et .

Si j’écris l’article, ce n’est pas pour vous alarmer. Ni même pour vous dire “arrêtez d’utiliser Angular”. Je pense que la V1 va être maintenue par la communauté pendant des années.

Non, je me demande simplement si ça vaut le coup que je me casse le cul à finir le guide.

J’ai du travail à la pelle, le guide sur les tests en Python, Django une app à la fois, 0bin à porter en Python 3…

Bref, est-ce que vous êtes toujours aussi intéressés par ce guide ?

]]>
http://sametmax.com/je-me-te-tate-a-arreter-le-tuto-angular/feed/ 35
Qu’est-ce que les websockets et à quoi ça sert ? 8 http://sametmax.com/quest-ce-que-les-websockets-et-a-quoi-ca-sert/ http://sametmax.com/quest-ce-que-les-websockets-et-a-quoi-ca-sert/#comments Tue, 30 Dec 2014 04:51:57 +0000 http://sametmax.com/?p=15615 Le protocole WebSocket vise à développer un canal de communication full-duplex sur un socket TCP. LOL. C'est clair non ? Vous inquiétez pas, tonton Sam est là.]]>

Le protocole WebSocket vise à développer un canal de communication full-duplex sur un socket TCP.

LOL. C’est clair non ?

Vous inquiétez pas, tonton Sam est là.

Le Web a évolué. On est passé de Gopher a HTTP 1 puis 1.1. Et on a eu AJAX pour rafraîchir la page sans tout recharger.

Et maintenant on a des apps complètes qui font des centaines de requêtes au serveur alors même que l’utilisateur ne change pas de page. D’ailleurs, je parie que plein de gens ne savent même plus ce qu’est une page…

Le problème c’est qu’AJAX, c’est toujours HTTP, et HTTP est sans état (stateless) : il ne garde aucune information en mémoire d’une requête à l’autre. Ça a des avantages, mais cela implique qu’à chaque requête, il faut ouvrir une connexion et la refermer. Ce qui bouffe quelques ms à chaque fois, et d’autant plus si on utilise SSL.

Une autre limite, c’est que le serveur ne peut pas envoyer de données au client (ici le navigateur) si le client ne fait pas une requête au préalable. Du coup, pour savoir si il y a quelque chose de nouveau, le navigateur doit régulièrement faire des requêtes au serveur ou utiliser des gros hacks comme le long polling.

Les websockets (c’est un abus de langage, on devrait parler du protocole Websocket) ont été créés pour répondre à ces besoins : elles permettent d’ouvrir une connexion permanente entre le navigateur et le serveur. Ainsi, chaque requête est plus rapide, et plus légère. En prime, le serveur peut envoyer des requêtes au navigateur pour le prévenir qu’il y a du nouveau.

Ceci permet de faire tout ce que permettait de faire AJAX mais en plus rapide, et en plus léger. Et également d’envoyer des notifications (ce contenu a changé, un message est arrivé, l’autre joueur a fait cette action…) au navigateur au moment où l’événement se produit.

En gros, de faire des apps Web quasi temps réel.

Il existe d’autre technos pour faire cela : applets Java, flash, comet, server sent events…

Mais aucune n’ont décollé. Websocket est donc aujourd’hui la solution de facto.

Caractéristiques

Le protocole Websocket utilise l’abréviation ws et wss si SSL, les URLs vers des endpoints websocket ressemblent donc à : ws://domaine.tld/chemin/vers/truc/.

Intelligemment, il utilise un handshake compatible avec celui de HTTP, permettant à un serveur de gérer les deux sur les mêmes ports. Donc on peut faire du Websocket sur le port 80 et 443. Néanmoins, certains proxy se gourent quand ils voient du websocket non chiffré et gauffrent votre connexion en la traitant comme du HTTP. Donc si vous voulez une app solide, investissez dans un certif SSL.

Tout ça fonctionne à partir de IE10. Notez comme IE est devenu le standard de ce qui ce fait de moins bien à tel point que je n’ai même pas besoin de vous parler des autres, vous savez que ça marche. Il existe en plus des plugins flash pour simuler des websockets sur les navigateurs anciens, c’est à dire les encore plus vieux IE.

Par défaut, les websockets permettent de faire de requêtes crossdomain, contrairement à AJAX. Avec les nouvelles apps qui utilisent NodeJS en local (comme popcorntime) on peut imaginer une nouvelle type d’attaque : une page web qui se connecte à un serveur websocket local de votre machine. Comme les websockets sont souvent utilisées pour du RPC, il y a du potentiel.

Bon, ta gueule, et montre le code

Vous noterez que ce qui prend du temps dans l’exemple c’est la connexion, qu’on ne fait qu’une fois. Ensuite l’échange de données est super rapide.

Ceci est un exemple Javascript, mais un client websocket n’est pas forcément un navigateur. En fait, c’est très précisément le cas avec WAMP, dont les clients peuvent être des programmes Python, Objective C, Java, C++, etc. L’avantage de WAMP, c’est qu’il automatise toute la machinerie pour découper la logique de son programme en divers fonctions et services, plutôt que d’avoir à tout faire à la main avec send() et onmessage().

Dans tous les cas, il vous faudra un serveur qui supporte les Websockets pour l’utiliser. En Python, c’est Tornado ou Twisted (sur lequel est basé le serveur WAMP crossbar). En Javascript, c’est NodeJS. Quoi qu’il en soit, il vous faut un logiciel qui gère l’IO de manière non bloquante, car il y a de nombreuses connexions ouvertes en simultanées, si on veut que ça soit performant.

]]>
http://sametmax.com/quest-ce-que-les-websockets-et-a-quoi-ca-sert/feed/ 8
Introduction au currying 3 http://sametmax.com/introduction-au-currying/ http://sametmax.com/introduction-au-currying/#comments Fri, 12 Dec 2014 19:37:00 +0000 http://sametmax.com/?p=12693 Le currying (ou Curryfication pour les frencofans) est le nom donné à une technique de programmation qui consiste à créer une fonction à partir d’une autre fonction et d’une liste partielle de paramètres destinés à celle-ci. On retrouve massivement cette technique en programmation fonctionnelle puisqu’elle permet de créer une fonction pure à partir d’une autre fonction pure. C’est une forme de réutilisabilité de code.

La forme la plus simple de currying est de réécrire une fonction appelant l’autre. Par exemple, soit une fonction pour multiplier tous les éléments d’un itérable :

def multiply(iterable, number):
    """ Multiplie tous les éléments d'un itérable par un nombre.
 
        Exemple :
 
            >>> list(multiply([1, 2, 3], 2))
            [2, 4, 6]
    """
    return (x * number for x in iterable)

On peut ensuite créer une fonction qui multipliera par 2 tous les éléments d’un itérable :

def doubled(iterable):
    """ Multiplie tous les éléments d'un itérable par un 2.
 
        Exemple :
 
            >>> list(doubled([1, 2, 3]))
            [2, 4, 6]
    """
    return multiply(iterable, 2)

C’est une forme de currying. On créé une fonction qui fait ce que fait une autre fonction, mais avec des arguments par défaut.

Python possède une fonction pour faire ça automatiquement avec n’importe quelle fonction :

>>> from functools import partial 
>>> tripled = partial(multiply, number=3) # on curryfie ici
>>> list(tripled([1, 2, 3])) # nouvelle fonction avec un seul argument
[3, 6, 9]

Cela marche car, je vous le rappelle, les fonctions sont des objets en Python. On peut mettre une fonction (je ne parle pas de son résultat) dans une variable, passer une fonction en paramètre ou retourner une fonction dans une autre fonction. Les fonctions sont manipulables.

Il n’est pas rare d’utiliser les fonctions anonymes comme outils curryfication. En Python, on ferait ça avec une lambda :

>>> tripled = lambda x: multiple(x, 3) 
>>> list(tripled([1, 2, 3]))
[3, 6, 9]

Certains outils, comme Ramda en Javascript, vont plus loin, et exposent des fonctions qui se curryfient automatiquement.

Pour ce faire, il faut inverser l’ordre qu’on mettrait intuitivement aux arguments dans la déclaration d’une fonction :

# au lieu de multiply(iterable, number), on a :
def multiply(number, iterable=None):
    # Si on a pas d'itérable passé, on curryfie
    if iterable is None:
        return partial(multiply, number=number)
    return (x * number for x in iterable)

Ainsi :

>>> list(multiply(2, [1, 2, 3])) # pas de currying
[2, 4, 6]
>>> quintuple = multiply(5) # currying automatique
>>> list(quintuple([1, 2, 3]))
[5, 10, 15]

L’intérêt de ce style, c’est qu’on peut composer des traitements à partir de plusieurs sous traitements, presque déclarativement :

def remove(filter, iterable=None):
    """ Retire tous les éléments d'un itérable correspondant au filtre.
 
        Exemple :
 
            >>> list(remove(lambda x: x >= 4, [1, 2, 3, 4, 5]))
            [1, 2, 3]
    """
    if iterable is None:
        return partial(remove, filter)
 
    return (x for x in iterable if not filter(x))
 
>>> smalls = remove(lambda x: x >= 4)
>>> list(smalls(tripled([0, 1, 2, 3, 4]))) # le traitement est auto descriptif
[0, 3]

Néanmoins, il faut savoir que ce style n’est pas pythonique. En effet, en Python on préférera généralement utiliser des suites suite de générateurs. Soit par intention, soit via yield.

Notre exemple serait alors :

>>> tripled = (x * 3 for x in [0, 1, 2, 3, 4])
>>> smalls = (x for x in tripled if x <= 4)
>>> list(smalls)
[0, 3]

De plus, cette technique suppose qu’on ne profitera pas de certaines fonctionnalités, comme les paramètres par défaut des fonctions Python.

C’est toutefois une bonne chose à connaître. C’est occasionnellement utile en Python et peut produire des solutions très élégantes. C’est également une bonne chose à comprendre pour aborder d’autres langages plus fonctionnels qui les utilisent bien plus comme le Javascript, le Lisp, ou carrément le Haskell.

]]>
http://sametmax.com/introduction-au-currying/feed/ 3
AngularJS pour les utilisateurs de jQuery : partie 2 6 http://sametmax.com/angularjs-pour-les-utilisateurs-de-jquery-partie-2/ http://sametmax.com/angularjs-pour-les-utilisateurs-de-jquery-partie-2/#comments Mon, 27 Oct 2014 06:53:27 +0000 http://sametmax.com/?p=12561 Après une intro sans trop de code, on va passer à du concret.

Pour apprivoiser la bête, on va comme d’habitude lui faire donner la patte et dire bonjour :

<!doctype html>
<!-- Lien entre l'app et la page -->
<html ng-app="tuto">
  <head>
    <meta charset="utf-8" />
    <title>Hello Angular</title>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.26/angular.js"></script>
    <script type="text/javascript">
 
    // Déclaration de l'app
    var app = angular.module('tuto', [])
 
    // Lancement du code quand angular est prêt
    app.run(function(){
        alert('Hello !')
    })
 
    </script>
  </head>
  <body>
  </body>
</html>

Pour comprendre ce snippet, il faut réaliser qu’Angular est d’abord et avant tout un moyen d’organiser son code, et qu’il vous incite à le séparer en conteneurs :

  • Des modules, qu’on appelle aussi apps. Ce sont les boîtes qui contiennent les autres boîtes. L’équivalent des apps Django, en fait : on peut mettre tout le programme dedans, mais on peut aussi en faire une lib réutilisable.
  • Les providers : des fournisseurs de services. Tout le code métier est dedans.
  • Les directives : tout le code de manipulation du DOM est dedans.
  • Les contrôleurs : fait le lien entre le HTML et les données.

Dans ce code, nous déclarons un module (ou app, c’est kif-kif) nommé “tuto” et n’ayant aucune dépendance (le second paramètre est un array vide) :

var app = angular.module('tuto', [])

Les apps ne font pas grand chose. Ce sont de gros namespaces avec des dépendances, c’est à dire qui déclarent les autres apps nécessaires à leur fonctionnement. Par défaut, aucune autre app n’est obligatoire pour lancer notre code, donc on ne déclare aucune dépendance.

Il y a juste un piège qu’il faut connaître. Si je fais :

var app = angular.module('tuto')

(notez l’absence de second paramètre)

Angular va tenter de me chercher la référence d’app du nom de “tuto” qui existe déjà. L’absence de dépendance se note donc avec un array vide, et non l’absence d’array. Subtilité piégeuse s’il en est. Vu qu’on n’aura pas besoin de déclarer de dépendances avant longtemps, inutile de vous crisper dessus, je vous l’indique juste pour le débuggage qui va immanquablement arriver le jour où vous oublierez l’array vide.

Ensuite, on fait le lien entre notre code HTML et l’app via :

<html ng-app="tuto">

ng-app est ce qu’on appelle une directive. Toutes les directives préfixées ng- sont fournies avec le framework et déjà chargées, prêtes à être utilisées. Une directive est un bout de code Javascript appliqué à du HTML, dans ce cas précis via un attribut.

Vous vous souvenez dans les années 2000 comme on vous faisait chier avec le fait de ne pas mettre de Javascript inline dans du HTML ? Les directives, c’est du Javascript inline qui ne dit pas son nom.

Chaque page doit être liée à une app, et une seule. Si on ne met pas ng-app, notre code ne se chargera pas, si on en met 2, ça foire. C’est le point d’entrée de notre code. Nous déclarons donc que cette page sera gérée par notre app “tuto”.

Puis lance notre hello tonitruant :

    app.run(function(){
        alert('Hello !')
    })

app est la variable contenant la référence à notre app, et en utilisant la méthode .run, on lui passe un callback à appeler quand l’app sera prête.

Il y a plusieurs choses à réaliser ici :

  • C’est un callback, donc le code de la fonction ne s’exécute pas tout de suite. Il sera lancé après l’initialisation de l’app.
  • C’est ce qu’il y a de plus proche du $.ready de jQuery.
  • On n’utilise presque jamais ce code avec Angular.

Vous allez me dire, mais espèce de pignouf, pourquoi tu nous montres un code que personne n’utilise ?

Et bien parce que le titre du dossier est AngularJS pour les utilisateurs de jQuery, du coup je vous mets en terrain connu.

Mais la vérité, c’est qu’un vrai hello d’Angular ressemble à ça :

<!doctype html>
<html ng-app="tuto">
  <head>
    <meta charset="utf-8" />
    <title>Hello Angular</title>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.26/angular.js"></script>
    <script type="text/javascript">
 
    /* La je recréé l'app à chaque fois pour des raisons de praticité, mais
        dans un vrai code vous auriez ça dans un fichier à part, une seule
        fois */
    var app = angular.module('tuto', [])
 
    // Création d'un controller, et attachement d'une variable au scope.
    app.controller('HelloCtrl', function($scope){
        // Attachement d'une donnée au scope
        $scope.hello = "Hello"
    })
 
    </script>
  </head>
  <body ng-controller="HelloCtrl">
    <!-- Affichage du message attaché au scope -->
    <h1>{{ hello }}</h1>
  </body>
</html>

Et là il y a beaucoup à dire.

Premièrement, contrairement à jQuery, il n’y a pas de notion de préparation du DOM. En vérité, on peut mettre son code Angular en vrac, et Angular va initialiser tout son monde dans le bon ordre, et exécuter ce qu’il faut au bon moment. Et ce bon moment est parfois avant que le DOM soit prêt.

Mais vous n’avez pas à vous en soucier.

Car avec Angular, on manipule très rarement le DOM. Très peu de $('selecteur'), $node.bind() et autre $node.append(), à part dans les directives.

La majorité de votre taf va être de définir des données en pur Javascript, de dire où elles vont dans la page, et de les manipuler en Javascript pendant qu’Angular s’occupe de mettre la page à jour automatiquement.

Nous avons pas mal de nouvelles notions ici. D’abord, le contrôleur…

C’est un bout de code qu’on va lier à un bout de HTML via la directive ng-controller :

  <body ng-controller="HelloCtrl">
    ...
  </body>

Le contrôleur va être responsable de mettre à disposition des données pour ce bout de HTML. On peut lier autant de contrôleurs qu’on veut dans une page, et même les imbriquer. Ici, on dit que notre contrôleur HelloCtrl est responsable de mettre des données à disposition pour le tag body et tout ce qu’il contient.

On fait rarement des contrôleurs qui ratissent aussi large dans de vraies apps, mais pour notre exercice, c’est très bien.

Comment met-on à disposition des données ? Qu’est-ce que veut dire “mettre à disposition” ? Quelles données ?

Notre contrôleur ressemble à ceci :

    app.controller('HelloCtrl', function($scope){
        $scope.hello = "Hello"
    })

La fonction de callback sera exécutée quand ng-controller="HelloCtrl" sera activé automatiquement par Angular. Vous n’avez pas à vous souciez de quand, il le fera au moment le plus optimisé.

La seule chose dont vous avez à vous soucier, c’est ce que vous allez mettre dedans.

Et il y a ici deux nouvelles notions :

  • L’injection de dépendances.
  • Le service (je vous explique ce qu’est un service plus loin) $scope.

L’injection de dépendance fait partie de ces trucs qu’Angular fait automatiquement pour vous. Quand vous déclarez une variable dans le callback d’un contrôleur, Angular va chercher ce nom en mémoire, et récupérer le service qui porte ce nom. S’il ne le trouve pas, il plante.

Une fois qu’il a trouvé le service, il va attendre tranquillement le bon moment pour appeler le callback. Ce moment opportun arrivant, il va lui passer le service en paramètre. On dit qu’il lui “injecte” le service.

En relisant ces paragraphes, je réalise qu’il y a un mélange de plein de termes : contrôleurs, services, injection, bla, bla, bla.

Voilà le deal :

  1. Vous déclarez un contrôleur et vous le liez à du HTML via ng-controller.
  2. Dans ce contrôleur vous dites “j’ai besoin du service Machin”
  3. Angular cherche s’il connaît Machin. Si non, il plante.
  4. Angular prépare le HTML, décide qu’il est prêt, appelle votre contrôleur et lui passe Machin en paramètre automatiquement.

Les services sont justes des d’objets javascript ordinaires, qu’on a nommés d’une certaine manière pour qu’Angular puisse les injecter. C’est tout.

En effet, pour résoudre le problèmes d’espace de nom en Javascript et d’organisation d’une grosse application, les créateurs d’Angular on créé un grand registre. On enregistre notre code dedans sous un nom (Angular.module('nom_de_l_app'), app.controller('nom_du_controller'), etc.), et Angular récupère le bon code au bon moment. C’est essentiellement un moyen de pallier au fait que Javascript est dégueulasse.

Donc, plutôt que de faire des imports comme en Python, on va déclarer une variable en paramètre, et Angular injecte le bon objet pour vous. C’est bizarre, mais on s’habitue.

Du coup, là :

app.controller('HelloCtrl', function($scope){

Je dis “crée moi le contrôleur nommé ‘HelloCtrl’, et quand tu vas appeler le callback, passe lui l’objet nommé ‘$scope’ en paramètre. Fais ça au bon moment, je m’en branle, je veux pas le savoir. Démmerde toi, on a tous des problèmes.”

Le résultat, c’est que votre fonction sera exécutée automatiquement au bon moment, et qu’elle aura $scope qui lui sera passé.

Alors à quoi sert ce $scope ?

C’est simple : tout ce qui est attaché en attribut de $scope est accessible dans le HTML géré par ce contrôleur. Quand je fais :

    app.controller('HelloCtrl', function($scope){
        $scope.hello = "Hello"
    })

Et que après je lie mon contrôleur au HTML :

  <body ng-controller="HelloCtrl">
    <!-- Affichage du message attaché au scope -->
    <h1>{{ hello }}</h1>
  </body>

Ma variable hello est disponible ici (via les petites moustaches qu’on verra au prochain chapitre), parce que je suis dans body qui est géré par le contrôleur HelloCtrl qui contient un $scope avec l’attribut hello. Fiou ! Ca en fait des couches. Mais Angular, c’est comme un orgre un onion.

Vous touchez ici du doigt l’essence même du boulot des contrôleurs, et à peu près leur unique but : mettre des données à disposition du HTML. C’est le C de MVC.

C’est pour cette raison que je vous ai répété “Angular va tout faire au bon moment, vous n’avez pas à vous soucier de quand”. Ceci est en effet très frustrant à entendre quand on vient du monde de jQuery puisque qu’on doit savoir exactement quand on fait les choses : il faut que le DOM soit prêt, il faut que les éléments sur lesquels on travaille existent, il faut qu’on puisse récupérer des nodes et en ajouter.

Avec Angular, on ne fait rien de tout ça. On va déclarer ses données dans le contrôleur, et dire où elles doivent être mises dans la page. Angular se charge de faire le lien pour vous quand le HTML est prêt, quand le Javascript est chargé, quand les Dieux sont avec vous…



Télécharger le code de l’article.

]]>
http://sametmax.com/angularjs-pour-les-utilisateurs-de-jquery-partie-2/feed/ 6
Servir des fichiers statiques avec nginx 10 http://sametmax.com/servir-des-fichiers-statiques-avec-nginx/ http://sametmax.com/servir-des-fichiers-statiques-avec-nginx/#comments Thu, 23 Oct 2014 06:20:02 +0000 http://sametmax.com/?p=10490 C’est un truc dont j’ai tout le temps besoin, alors l’article servira de pense bête. Marre de chercher à chaque fois :

        # sur django, on met tout dans /static/, donc par habitude je le fais
        # pour tout
        location /static/ {

            # Le dossier doit contenir le dossier 'static'. Par exemple si votre
            # arbo est /home/sametmax/repo/static, le chemin sera
            # /home/sametmax/repo. Rassurez-vous, personne n'aura accès aux
            # autres sous dossiers de "repo".
            root  /chemin/absolu/vers/dossier/;

            # On active la compression
            gzip  on;
            gzip_http_version 1.0;
            gzip_vary on;
            gzip_comp_level 6;
            gzip_proxied any;
            gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
            gzip_buffers 16 8k;

            # Sauf pour les vieux nav
            gzip_disable ~@~\MSIE [1-6].(?!.*SV1)~@~];

            # On dit au navigateur de le mettre en cache pour 3 mois. Faites gaffe,
            # mettez un param dans les url de vos balises script/link qui change
            # à chaque version du fichier, sinon vous ne pourrez pas mettre à jour
            # vos fichiers.
            expires modified +90d;
        }
]]>
http://sametmax.com/servir-des-fichiers-statiques-avec-nginx/feed/ 10
AngularJS pour les utilisateurs de jQuery : partie 1 31 http://sametmax.com/angularjs-pour-les-utilisateurs-de-jquery-partie-1/ http://sametmax.com/angularjs-pour-les-utilisateurs-de-jquery-partie-1/#comments Tue, 02 Sep 2014 09:55:36 +0000 http://sametmax.com/?p=12170 Après avoir sondé un peu Twitter, le sujet intéresse, et il est très, mais alors, très mal traité ailleurs sur le net. Malgré mon aversion, bien connue, pour javascript, je m’en bouffe beaucoup, prog Web oblige.

Angular rend l’utilisation de JS plus supportable, mais possède une très grosse courbe d’apprentissage bien pentue, avec des ressources vraiment mal foutues.

Et surtout, le problème vient de l’habitude de jQuery.

AngularJS est l’anti-jQuery, ça ne fonctionne pas du tout pareil, mais personne n’est là pour vous montrer comment faire la transition. Ce sera le but de ce dossier, un dossier qui va être long car ce framework est un gros morceau.

Quand utiliser AngularJS ?

Tous les sites ne sont pas faits pour être créés avec Angular. En effet, si votre site est essentiellement composé de contenu statique, avec juste un peu de dynamique pour quelques widgets, Angular n’a aucun intérêt.

Angular est lourd à mettre en œuvre, on va donc se faire chier à sortir le bazooka quand on monte au front, pas quand on fait une garde de nuit en rase campagne.

Malgré le fait que Google interprète maintenant les pages en JS, le texte reste roi, et Angular vous force à faire des pages en pur JS, rendant le référencement aléatoire de votre contenu. Les hacks que l’on voit un peu partout sur la toile pour compenser cela sont très loin d’obtenir l’effet d’un référencement naturel de contenu facilement accessible à l’ancienne.

En fait, le plus gros défaut d’Angular, c’est qu’il ne permet pas, pour le moment, de faire de la dégradation gracieuse. Soit le mec a Javascript et il a accès au site. Soit le site est illisible.

jQuery a donc encore de beaux jours devant lui.

Typiquement, on ne prendra pas Angular pour :

  • La partie publique d’un blog de type WordPress.
  • Un wiki type Wikipédia.
  • Un forum ou site collaboratif.
  • Un tube vidéo.

Ce sont des sites dont le contenu est important. On veut que les moteurs de recherche puisse les indexer de fond en comble. On veut que ce soit lisible sans JS et par les aveugles (sauf le tube vidéo :-D). Les ajouts de JS se feront par petites touches.

On utilisera plutôt Angular pour :

  • Une app grand public.
  • La partie admin d’un blog.
  • Un logiciel d’entreprise.
  • Un réseau social.

Ce sont des sites pour lesquels l’ergonomie est plus importante que le contenu. Le public est captif, et on peut exiger Javascript, prix à payer pour une expérience utilisateur moderne.

Vous noterez qu’il y a une certaine opposition entre vieux format (wiki, forum, blog) et format contemporain (app, réseau social). Ce n’est pas un hasard. Les premiers sont orientés contenu, les seconds orientés usage. C’est ainsi que le Web a évolué, et Angular est là exactement pour répondre à cette évolution.

Il est néanmoins tout à fait possible de faire des choses hybrides. Rien n’empêche de faire des parties du sites statiques, et d’autres très dynamiques, de mélanger les deux sur une même page. Par exemple, sur un blog, vous pouvez mettre l’article en statique pour le ref et la facilité de consultation, et la partie commentaires en Angular, pour améliorer l’expérience utilisateur.

Que va apporter Angular ?

Les avantages sont doubles.

D’abords, il y a les avantages pour vous, le développeur. Angular vous force en effet à utiliser un style Javascript très propre qui tend à découpler les composants, isoler les services, lisser les angles et refaire le papier peint à fleur. L’application en devient plus propre, plus facile à faire évoluer et à maintenir. Toutes les bonnes pratiques d’organisation d’une app JS ont une réponse angularesque. Ce n’est pas forcément celle que vous voulez, mais elle marche.

Ensuite, Angular va vous rendre plus productif. Pas tout de suite, évidement, coincés que vous êtes dans votre logique jQuerinne. Mais une fois le cap passé, coder beaucoup de JS sera immensément plus simple et plus rapide, car Angular vous épargne beaucoup de logique répétitive.

En prime, ce framework est orienté tests. Ce n’est pas obligatoire, mais c’est bien plus facile qu’avec des modules jQuery.

Mais il y a également les avantages pour l’utilisateur. Puisque qu’Angular est complètement dynamique, il bénéficiera automatiquement d’une app plus réactive, avec moins de rechargements, plus de fonctionnalités aussi. En effet, vous rechignerez moins à rajouter cette fonction “delete-row” si vous savez que ça vous prend 3 lignes de code.

Attention cependant, il y a aussi des inconvénients.

Angular est lourd, c’est long à charger, ça bouffe de la ressource pour chaque tab, et si vous avez beaucoup de traitements sur une page, vous avez intérêt à optimiser votre race le bouzin sinon ça va ramer. Tout le monde n’est pas sur une machine de dev propre à 8 cœurs avec une ligne ADSL qui a tout compris.

En fait, si votre site est destiné à être consulté ainsi : je cherche, je consulte du contenu, et je m’en vais, charger Angular est un pur gâchis. Il faut au moins que l’on interagisse un peu avec le site pour que ça vaille le coup : créer du contenu, manipuler des informations, se faire notifier, etc.

Et puis il y a la tentation, la tentation forte de tout faire à la main côté client. Avec son lot de problèmes de sécurité (le delete qui fait pas de vérif), de merdier dans l’histo de navigation (le clic du milieu qui marche pas, le back qui back pas où on veut), les sessions qui font des siennes (je suis déco, mais y mon avartar là, pourquoi ?), etc.

Angular n’est pas une solution pour le bidouilleur Javascript de devenir soudainement grand programmeur Web, et ne dispense pas de coder correctement.

Si je fais de l’Angular, je dois faire du NodeJS ?

Non.

Angular est une techno parfaitement neutre, elle est limitée au client, et par conséquent, vous êtes libres de choisir votre techno côté serveur : .Net, PHP, Ruby, Java, Python…

Par contre, pour les tests, les outils sont très orientés autour de l’écosystème NodeJS : npm, grunt, PhantomJS, etc. Il va falloir mettre les mains dans le cambouis, avec son lot de trucs qui s’installent pas, de dépendances mal branlées, de doc pas à jour, de code JS peu expressif et tout le bordel. C’est tout le problème du JS, la communauté a gardé le côté crade et à l’arrache du langage. Heureusement, ils ont récupéré les designers du monde Ruby, donc vous aurez des tutos obsolètes, mais très jolis.

Ok, mais je peux faire quoi avec ?

Car finalement, quand on n’a pas utilisé Angular, on se demande finalement qu’est-ce que ça apporte par rapport à jQuery.

Essentiellement, c’est que ça divise par 100 les manipulations du DOM. Avec Angular, on met ses données dans un tableau d’objets JS, on indique où les afficher, et on manipule ensuite notre array. Le DOM est mis à jour sans qu’on doive s’en soucier.

Ça à l’air de rien comme ça, mais si vous ouvrez vos fichiers JS avec jQuery, vous verrez qu’ils sont blindés de .click()code et de .append(). C’est la majorité du code ! Code qu’on a pas besoin d’écrire avec Angular.

L’organisation d’Angular invite aussi naturellement à écrire un code linéaire. Pas de blocs dans des blocs dans des blocs, chaînes de callbacks et amas de closures. Il y en a toujours, mais bien moins.

Après, on fait finalement la même chose qu’on ferait avec jQuery, mais plus facilement. Ce qui explique qu’on fait rarement une app full JS avec jQuery, ce qui est infernal à coder, alors qu’on le fait systématiquement avec Angular, car c’est son milieu naturel.

Ce qui va vous surprendre avec cette façon de faire, c’est que tout ce qui s’affiche dans la page passe par Angular. En fait, on a généralement une API REST, et Angular en front pour afficher le site. Pas trop de templating côté serveur.

Et on verra des exemples concrets de tout ça dans les parties suivantes :)

]]>
http://sametmax.com/angularjs-pour-les-utilisateurs-de-jquery-partie-1/feed/ 31
Rendre un élément scrollable avec Angular 8 http://sametmax.com/rendre-un-element-scrollable-avec-angular/ http://sametmax.com/rendre-un-element-scrollable-avec-angular/#comments Tue, 01 Jul 2014 09:30:20 +0000 http://sametmax.com/?p=11240 Petit snippet que j’utilise dans mes apps angular. Ça permet de définir un comportement quand l’utilisateur scrolle au-dessus d’un élément. Typiquement, augmenter la valeur d’un champ, faire défiler un carousel, etc. Il faut, bien entendu, éviter que la page scrolle elle-même.

Implémentation

app.directive('wheelable', function() {
"use strict";
 
  /* On définit sur quels attributs on va mettre les callbacks */
  var directive = {
      scope: {
          'onWheelUp': '&onwheelup',
          'onWheelDown': '&onwheeldown'
      }
  };
 
  /* On limite la directive aux attributs */
  directive.restrict = 'A';
 
  /* Le code qu'active la directive quand on la pose sur l'élément */
  directive.link = function($scope, element, attributes) {
 
      /* On attache un callback à tous les événements de scrolling */
      element.bind('mousewheel wheel', function(e) {
 
        /* On vérifie si l'utilisateur scroll up ou down */
        if (e.originalEvent) {
          e = e.originalEvent;
        }
        var delta = (e.wheelDelta) ? e.wheelDelta : -e.deltaY;
        var isScrollingUp = (e.detail || delta > 0);
 
        /* On appelle le bon callback utilisateur */
        if (isScrollingUp){
          $scope.$apply($scope.onWheelUp());
        } else {
          $scope.$apply($scope.onWheelDown());
        }
 
        /* On évite que la page scrolle */
        e.preventDefault();
      });
  };
 
  return directive;
});

Usage

Comme pour toutes les directives qui impliquent des callbacks, il faut définir des fonctions et les attacher à votre scope dans un controleur (ou un service attaché au controleur) :

app.controller('FooCtrl', function($scope) {
"use strict";
  $scope.votreCallBackPourQuandCaScrollDown = function(){
    // faire un truc par exemple moi je l'utilise pour changer
    // la valeur de l'élément.
  };
  $scope.votreCallBackPourQuandCaScrollDown = function(){
    // faire un autre truc
  };
});

La directive s’utilise en mettant l’attribut wheelable sur l’élément qu’on veut rendre scrollable. Ensuite on déclare dans les attributs onwheeldown et onwheelup le code à exécuter, et zou :

<div ng-controller="FooCtrl">
  ...
  <input type="text" wheelable
         onwheeldown="votreCallBackPourQuandCaScrollDown()"
         onwheelup="votreCallBackPourQuandCaScrollUp()"
         >
  ...
</div>
]]>
http://sametmax.com/rendre-un-element-scrollable-avec-angular/feed/ 8
Petite démo pragmatique d’un usage de WAMP en Python 47 http://sametmax.com/introduction-a-wamp-en-python/ http://sametmax.com/introduction-a-wamp-en-python/#comments Thu, 26 Jun 2014 07:27:03 +0000 http://sametmax.com/?p=11146 L’API a changé depuis, j’ai donc mis à jour l’article pour refléter ces changements

Vu que dernièrement je vous ai bien gavé avec WAMP, ça mérite un tuto non ?

Il se trouve que l’équipe derrière WAMP a publié plus tôt que prévu une version de leurs libs contenant l’API flaskesque sur laquelle on bosse. L’idée est que même si on n’a pas encore les tests unitaires, on peut déjà jouer avec.

Maintenant il me fallait un projet sexy, histoire de donner envie. Donc j’ai fouillé dans ce qui se faisait côté temps réel (essentiellement du NodeJS et du Tornado, mais pas que) pour trouver l’inspiration.

Et j’ai trouvé un truc très sympa : un player vidéo piloté à distance.

En effet, n’est-il pas chiant de regarder une vidéo en ligne sur son ordi posé sur la commode pendant qu’on est enfoncé dans le canap ? Si on veut faire pause ou changer le son, il faut se lever, arg.

Les problèmes du tiers monde, c’est du pipi de chat à côté. Ils ont de la chance, eux, ils ne connaissent pas le streaming.

Voici donc le projet :

Une page avec un player HTML 5 et un QR code.

Capture d'écran de la démo, côté player

Pour simplifier la démo, on peut cliquer sur le QR code et avoir la télécommande dans un autre tab pour ceux qui n’ont pas de smartphone ou d’app de scan de QRCode.

Si on scanne le QR code avec son téléphone, il vous envoie sur une page avec une télécommande pour contrôler le player sans bouger votre cul :

Capture d'écrand de la démo, côté contrôles

Évidement, c’est basique. Je vais pas m’amuser à faire un produit complet juste pour un truc dont le code source ne sera même pas regardé par la plupart d’entre vous. Je vous connais, bandes de feignasses !

Et vous allez voir, c’est même pas dur à faire.

Démo en ligne:

La démo

Vous pouvez télécharger le code ici.

Pour comprendre ce qui va suivre, il va vous falloir les bases en prog Javascript et Python, ainsi que bien comprendre la notion de callback. Être à l’aise avec promises peut aider.

Et pour bien digérer ce paté, rien ne vaut un peu de son :

Le Chteumeuleu

Il va nous falloir deux pages Web, une pour le player vidéo, et une pour la télécommande.

Le player :

 
<!DOCTYPE html>
<html>
<head>
   <title>Video</title>
   <meta charset='utf-8'>
   <!-- Chargement des dépendances : autobahn pour WAMP
   et qrcode pour générer le QR code. Bien entendu, je
   vous invite à ne pas les hotlinker dans vos projets,
   mais pour la démo c'est plus simple. -->
   <script src="https://autobahn.s3.amazonaws.com/autobahnjs/latest/autobahn.min.jgz"
           type="text/javascript"></script>
   <script src="http://davidshimjs.github.com/qrcodejs/qrcode.min.js"
           type="text/javascript"></script>
 
   <style type="text/css">
      #vid {
         /* Taille de la video */
         width:427px;
         height:240px;
      }
      /* Centrage avec la méthode Rache */
      #container {
          width:427px;
          margin:auto;
      }
      #ctrllink {
          display:block;
          width:256px;
          margin:auto;
      }
   </style>
 
</head>
<body>
<div id="container">
  <p>
 
   <!-- Pareil, je hotlink la video, mais ne faites pas ça
   à la maison les enfants. Surtout que les perfs du
   serveur du W3C sont merdiques et ça bufferise à mort. -->
    <video id="vid"
           class="video-js vjs-default-skin"
           controls preload="auto"
           poster="http://media.w3.org/2010/05/sintel/poster.png" >
    <source id='ogv'
      src="http://media.w3.org/2010/05/sintel/trailer.ogv"
      type='video/ogg'>
    <source id='mp4'
      src="http://media.w3.org/2010/05/sintel/trailer.mp4"
      type='video/mp4'>
    <source id='webm'
      src="http://media.w3.org/2010/05/sintel/trailer.webm"
      type='video/webm'>
    </video>
  </p>
  <p>
    <a id="ctrllink" href="#" target="_blank">
      <span id="qrcode"></span>
    </a>
  </p>
 </div>
 
</body>

Et la télécommande :

 
<!DOCTYPE html>
<html>
<head>
  <title>Télécommande</title>
  <meta charset='utf-8'>
  <script src="https://autobahn.s3.amazonaws.com/autobahnjs/latest/autobahn.min.jgz"
         type="text/javascript"></script>
  <!-- Zoom du viewport sur mobile pour éviter d'avoir
       à le faire à la main. -->
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style type="text/css">
    #controls {
      width:350px;
      margin:auto;
    }
    #controls button {
      font-size: 1em;
    }
    #controls input {
      vertical-align:middle;
       width: 200px;
       height:20px;
   }
  </style>
</head>
<body>
  <p id="controls">
    <!-- Marrant de se dire qu'en 2000, le JS inline était
         considéré comme démoniaque, et maintenant avec
         angularjs et cie, c'est exactement ce que tout
         le monde fait...
         Bref, on attache le clic sur nos contrôles à des
         méthodes de notre objet qui va se charger de la
         logique. -->
 
    <button id="play" onclick="control.togglePlay()">Play</button>
    <input id="volume"
                    onchange="control.volume(this.value)"
                    type="range">
  </p>
</body>

Rien d’incroyable. C’est du HTML, un peu de CSS, on charge les dépendances en JS. Classique.

Vu qu’on utilise des ressources hotlinkées par souci de simplicité, il vous faudra être connecté à Internet.

Setup du routeur

On va travailler avec Python 2.7 puisque Crossbar.io est uniquement en 2.7 et que je n’ai pas envie de vous faire installer deux versions de Python juste pour le tuto.

Il nous faut avant tout un serveur HTTP pour servir les fichiers HTML et un routeur WAMP. On installe donc Crossbar.io :

pip install crossbar

Ca va aussi installer autobahn, twisted et tout le bordel.

On va ensuite dans le dossier qui contient ses fichiers HTML, et on créé le fichier de config de Crossbar.io avec un petit :

crossbar init

Vous noterez la création d’un dossier .crossbar qui contient un fichier config.json. C’est la config de crossbar. Videz moi ce fichier, on va le remplir avec notre config :

{
   "workers": [
      {
         "type": "router",
         "realms": [
            {
               "name": "realm1",
               "roles": [
                  {
                     "name": "anonymous",
                     "permissions": [
                        {
                           "uri": "*",
                           "publish": true,
                           "subscribe": true,
                           "call": true,
                           "register": true
                        }
                     ]
                  }
               ]
            }
         ],
         "transports": [
            {
               "type": "web",
               "endpoint": {
                  "type": "tcp",
                  "port": 8080,
                  "interface": "0.0.0.0"
               },
               "paths": {
                  "/": {
                     "type": "static",
                     "directory": ".."
                  },
                  "ws": {
                     "type": "websocket",
                  }
               }
            }
         ]
      }
   ]
}

Crossbar est en effet un gestionnaire de processus : il ne gère vraiment rien lui même. Il démarre d’autres processus, appelés workers, à qui il délègue le travail.

On définit dans ce fichier de config quels processus (les workers) lancer quand Crossbar.io démarre. Les valeurs qu’on utilise disent de créer un seul worker de type “router”, c’est à dire un worker capable de gérer les entrées et les sorties WAMP. Hey oui, le routeur n’est qu’un worker comme les autres :)

Il y a d’autres sortes de workers, mais aujourd’hui on s’en branle.

Dans notre config du worker router, on crée d’abord un realm, qui est juste un namespace avec des permissions. Si un client WAMP se connecte à ce routeur, il doit choisir un realm (qui est juste un nom), et il ne peut parler qu’avec les clients du même realm. C’est une cloture quoi.

Dans un realm, on définit des roles qui déclarent quelles opérations PUB/SUB et RPC on a le droit de faire. Ici on dit que tout le monde (anonymous) a le droit de tout faire sur toutes les urls (“uri”: ‘*”) histoire de pas se faire chier. Si on met en prod, évidement on va se pencher sur la sécurité et faire ça plus proprement.

"realms": [
{
   "name": "realm1",
   "roles": [
      {
         "name": "anonymous",
         "permissions": [
            {
               "uri": "*",
               "publish": true,
               "subscribe": true,
               "call": true,
               "register": true
            }
         ]
      }
   ]
}
],

Puis on définit les transports, c’est à dire sur quoi notre worker va ouvrir ses oreilles pour écouter les messages entrant :

"transports": [
            {
               "type": "web",
               "endpoint": {
                  "type": "tcp",
                  "port": 8080,
                  "interface": "0.0.0.0"
               },
               "paths": {
                  "/": {
                     "type": "static",
                     "directory": ".."
                  },
                  "ws": {
                     "type": "websocket",
                  }
               }
            }
        ]

Encore une fois on en déclare un seul, de type “web”. Ce transport peut écouter HTTP et Websocket sur le même port. On lui dit d’écouter sur “0.0.0.0:8080″ :


“endpoint”: {
“type”: “tcp”,
“port”: 8080,
“interface”: “0.0.0.0”
},

Ensuite on dit que si quelqu’un arrive sur “/”, on sert en HTTP les fichiers statiques histoire que nos pages Web soient servies :

"/": {
 "type": "static",
 "directory": ".."
},

Si on arrive sur “/ws”, on route les requêtes WAMP via Websocket :

"ws": {
 "type": "websocket",
}

Le routeur est prêt, on lance Crossbar.io :

$ crossbar start
2015-01-07 20:02:55+0700 [Controller  26914] Log opened.
2015-01-07 20:02:55+0700 [Controller  26914] ============================== Crossbar.io ==============================
 
2015-01-07 20:02:55+0700 [Controller  26914] Crossbar.io 0.9.12-2 starting
2015-01-07 20:02:55+0700 [Controller  26914] Running on CPython using EPollReactor reactor
2015-01-07 20:02:55+0700 [Controller  26914] Starting from node directory /home/sam/Work/sametmax/code_des_articles/2014/juin/video_remote/.crossbar
2015-01-07 20:02:55+0700 [Controller  26914] Starting from local configuration '/home/sam/Work/sametmax/code_des_articles/2014/juin/video_remote/.crossbar/config.json'
2015-01-07 20:02:55+0700 [Controller  26914] Warning, could not set process title (setproctitle not installed)
2015-01-07 20:02:55+0700 [Controller  26914] Warning: process utilities not available
2015-01-07 20:02:55+0700 [Controller  26914] No WAMPlets detected in enviroment.
2015-01-07 20:02:55+0700 [Controller  26914] Starting Router with ID 'worker1' ..
2015-01-07 20:02:55+0700 [Controller  26914] Entering reactor event loop ...
2015-01-07 20:02:55+0700 [Router      26917] Log opened.
2015-01-07 20:02:55+0700 [Router      26917] Warning: could not set worker process title (setproctitle not installed)
2015-01-07 20:02:55+0700 [Router      26917] Running under CPython using EPollReactor reactor
2015-01-07 20:02:56+0700 [Router      26917] Entering event loop ..
2015-01-07 20:02:56+0700 [Router      26917] Warning: process utilities not available
2015-01-07 20:02:56+0700 [Controller  26914] Router with ID 'worker1' and PID 26917 started
2015-01-07 20:02:56+0700 [Controller  26914] Router 'worker1': realm 'realm1' started
2015-01-07 20:02:56+0700 [Controller  26914] Router 'worker1': role 'role1' started on realm 'realm1'
2015-01-07 20:02:56+0700 [Router      26917] Site starting on 8080
2015-01-07 20:02:56+0700 [Controller  26914] Router 'worker1': transport 'transport1' started

Setup du client

Pour cette démo, le serveur n’a pas grand chose à faire. On pourrait en fait la faire sans aucun code Python, mais ça va nous simplifier la vie et donner un peut de grain à moudre pour le tuto.

En effet, on a deux problématiques que le serveur va résoudre facilement pour nous : créer un ID unique pour le player et récupérer l’IP sur le réseau local.

L’ID, c’est simplement que si plusieurs personnes lancent en même temps un player, on ne veut pas que les télécommandes puissent lancer un ordre à un autre player que le sien. On pourrait utiliser un timestamp, mais ils sont contigus, n’importe quel script kiddies pourrait faire un script pour foutre la merde. On va donc créer un ID unique qui ne soit pas facilement prévisible. Javascript n’a rien pour faire ça en natif, et c’est un peu con de charger une lib de plus pour ça alors que Python peut le faire pour nous.

L’IP, c’est parce qu’il faut donner l’adresse de notre machine contient notre routeur. Et le téléphone qui sert de télécommande doit se connecter à ce routeur. Il faut donc qu’il connaisse l’adresse de celui-ci, donc on va la mettre dans notre QR code.

Cela veut dire aussi que le téléphone doit être sur le même réseau local pour que ça fonctionne. Donc mettez votre téléphone en Wifi, pas en 3G.

Voilà ce que donne notre code WAMP côté serveur :

# -*- coding: utf-8 -*-
 
from autobahn.twisted.wamp import Application
 
import socket
import uuid
 
# Comme pour flask, l'objet app
# est ce qui lie tous les éléments
# de notre code ensemble. On lui donne
# un nom, ici "demo"
app = Application('demo')
# Bien que l'app va démarrer un serveur
# pour nous, l'app est bien un CLIENT
# du serveur WAMP. Le serveur démarré
# automatiquement n'est qu'une facilité
# pour le dev. En prod on utiliserait
# crossbar.
 
# Juste un conteneur pour y mettre notre IP
app._data = {}
 
# On déclare que cette fonction sera appelée
# quand l'app se sera connectée au serveur WAMP.
# Ceci permet de lancer du code juste après
# le app.run() que l'on voit en bas du fichier.
# '_' est une convention en Python pour dire
# "ce nom n'a aucune importance, c'est du code
# jetable qu'on utilisera une seule fois".
@app.signal('onjoined')
def _():
   # On récupère notre adresse IP sur le réseau local
   # C'est une astuce qui demande de se connecter et donc
   #  à une IP externe, on a besoin d'une connexion internet.
   s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
   s.connect(("8.8.8.8", 80))
   # On stocke l'adresse IP locale dans un conteneur
   # qui sera accessible partout ailleur.
   app._data['LOCAL_IP'] = s.getsockname()[0]
   s.close()
 
# On déclare que la fonction "ip()" est appelable
# via RCP. Ce qui veut dire que tout autre client
# WAMP peut obtenir le résultat de cette fonction.
# Donc on va pouvoir l'appeler depuis notre navigateur.
# Comme notre app s'appelle "demo" et notre fonction
# s'appelle "ip", un client pourra l'appeler en faisant
# "demo.ip".
@app.register()
def ip():
   # On ne fait que retourner l'IP locale. Rien de fou.
   return app._data['LOCAL_IP']
 
# Je voulais appeler cette fonction distante "uuid", mais ça
# override le module Python uuid. Ce n'est pas une bonne
# idée. Je l'appelle donc 'get_uuid' mais je déclare le
# namespace complet dans register(). Un client WAMP pourra donc
# bien l'appeler via "demo.uuid".
# Notez que ce namespace doit toujours s'écrire
# truc.machine.bidule. Pas truc/machin ou truc:machin.
# ou truc et bidule.MACHIN.
@app.register('demo.uuid')
def get_uuid():
   # Retourne un UUID, sans les tirets.
   # ex: b27f7e9360c04efabfae5ac21a8f4e3c
   return str(uuid.uuid4()).replace('-', '')
 
# On lance notre client qui va se connecter au
# routeur.
if __name__ == '__main__':
    app.run(url="ws://127.0.0.1:8080/ws")
# On ne peut rien mettre comme code ici, il faut le
# mettre dans @app.signal('onjoined') si on veut
# entrer du code après que l'app soit lancée.

Et on lance notre app dans un autre terminal:

python app.py

Nous avons maintenant Crossbar.io qui tourne d’une console, et le client Python qui tourne dans une seconde console, connecté au routeur.

Le lecteur vidéo

Il nous faut maintenant définir le comportement de notre lecteur vidéo, un client WAMP Javascript. Il s’agit essentiellement de se connecter au serveur WAMP, et d’échanger des messages via RPC ou PUB/SUB :

  var player = {};
  var url;
  /* On va utiliser du pur JS histoire de pas mélanger
    des notions de jQuery dans le tas. Je ne vais
    PAS utiliser les best practices sinon vous allez
    être noyés dans des détails */
 
  /* Lancer le code une fois que la page est chargée */
  window.addEventListener("load", function(){
 
    /* Connexion au serveur WAMP. J'utilise
       les valeurs par défaut du serveur de
       dev. On ouvre explicitement la connection
       à la fin du script. */
    var connection = new autobahn.Connection({
       url: 'ws://' + window.location.hostname + ':8080/ws',
       realm: 'realm1'
    });
 
    /* Lancer ce code une fois que la connexion
       est réussie. Notez que je ne gère pas
       les erreurs dans dans une APP JS, c'est
       un puits sans fond. */
    connection.onopen = function (session) {
 
      /* Appel de la fonction ip() sur le serveur */
      session.call('demo.ip')
 
      /* Une fois qu'on a récupéré l'IP,
         on peut fabriquer l'URL de notre
         projet et on appelle la fonction
         get_uuid() du serveur */
      .then(function(ip){
        url = 'http://' + ip + ':8000';
        return session.call('demo.uuid');
      })
 
      /* Une fois qu'on a l'UUID, on peut commencer
         à gérer la partie télécommande */
      .then(function(uuid){
 
        /* Création du QR code avec le lien pointant
           sur la bonne URL. On met l'ID dans le hash. */
        var controlUrl = url + '/control.html#' + uuid;
        var codeDiv = document.getElementById("qrcode");
        new QRCode(codeDiv, controlUrl);
        var ctrllink = document.getElementById("ctrllink");
        ctrllink.href = controlUrl;
 
        /* Notre travail consiste essentiellement à
           manipuler cet élément */
        var video = document.getElementById("vid");
 
        /* On attache déclare 4 fonctions comme étant
           appelable à distance. Ces fonctions sont
           appelables en utilisant le nom composé
           de notre ID et de l'action qu'on souhaite
           faire. Ex:
           'b27f7e9360c04efabfae5ac21a8f4e3c.play'
           pour appeler "play" sur notre session. */
        session.register(uuid + '.play', function(){
           video.play();
        });
 
        session.register(uuid + '.pause', function(){
           video.pause();
        });
 
        session.register(uuid + '.volume', function(val){
           video.volume = val[0];
        });
 
        session.register(uuid + '.status', function(val){
          return {
            'playing': !video.paused,
            'volume': video.volume
          };
        });
 
 
 
       /* Quelqu'un peut très bien
           appuyer sur play directement sur cette page.
 
          Il faut donc réagir si l'utilisateur le fait,
          publier un événement via WAMP pour permettre
          à notre télécommande de se mettre à jour
          */
       video.addEventListener('play', function(){
         /* On publie un message indiquant que
            le player a recommencé à lire la vidéo.
            */
         session.publish(uuid + '.play');
       });
 
        video.addEventListener('pause', function(){
          session.publish(uuid + '.pause');
        });
 
        video.addEventListener('volumechange', function(){
          session.publish(uuid + '.volume', [video.volume]);
        });
 
     });
    };
 
    /* Ouverture de la connection une fois que tous les
       callbacks sont bien en place.*/
    connection.open();
  });

Code de la télécommande

La télécommande est notre dernier client WAMP (on peut avoir plein de clients WAMP, ne vous inquiétez, ça tient 6000 connections simultanées sur un tout petit Raspberry PI).

Son code a pour but d’envoyer des ordres au player HTML5, mais aussi de mettre à jour son UI si le player change d’état.

/* L'objet qui se charge de la logique de nos
   controles play/pause et changement de
   volume.
   Rien de fou, il change l'affichage
   du bouton et du slider selon qu'on
   est en pause/play et la valeur du
   volume.
   */
var control = {
   playing: false,
   setPlaying: function(val){
      control.playing = val;
      var button = window.document.getElementById('play');
      if (!val){
         button.innerHTML = 'Play'
      } else {
         button.innerHTML = 'Pause';
      }
   },
   setVolume: function(val){
      var slider = window.document.getElementById('volume');
      slider.value = val;
   }
};
window.onload = function(){
  var connection = new autobahn.Connection({
    url: 'ws://' + window.location.hostname + ':8080/ws',
    realm: 'realm1'
  });
 
  connection.onopen = function (session) {
 
    /* Récupération de l'ID dans le hash de l'URL */
    var uuid = window.location.hash.replace('#', '');
 
    /* Mise à jour des controles selon le status actuel
       du player grace à un appel RPC vers notre autre
       page. */
    session.call(uuid + '.status').then(function(status){
 
      control.setPlaying(status['playing']);
      control.setVolume(status['volume'])
 
      /* On attache l'appui sur les contrôles à
         un appel de la fonction play() sur le
         player distant. L'uuid nous permet
         de n'envoyer l'événement que sur le
         bon player. */
      control.togglePlay = function() {
        if (control.playing){
          session.call(uuid + '.pause');
          control.setPlaying(false);
        } else {
          session.call(uuid + '.play');
          control.setPlaying(true);
        }
      };
 
      control.volume = function(val){
        session.call(uuid + '.volume', [val / 100]);
      };
 
      /* On ajoute un callback sur les événements
         de changement de status du player. Si
         quelqu'un fait play/pause ou change le
         volume, on veut mettre à jour la page. */
      session.subscribe(uuid + '.play', function(){
        control.setPlaying(true);
      });
 
      session.subscribe(uuid + '.pause', function(){
        control.setPlaying(false);
      });
 
      session.subscribe(uuid + '.volume', function(val){
        control.setVolume(val[0] * 100);
      });
    });
  };
 
  connection.open();
};

En résumé

Voici à quoi ressemble le projet final :

.
├── app.py
├── control.html
├── .crossbar
│   └── config.json
└── index.html
Schéma de fonctionnement de la démo

Bien que l’app Python lance le serveur automatiquement et de manière invisible, c’est bien un composant à part.

Pour ce projet, on aura utilisé :

  • WAMP: le protocole qui permet de faire communiquer en temps réel des parties d’application via RPC et PUB/SUB.
  • Autobahn.js: une lib pour créer des clients WAMP en javascript.
  • Autobahn.py: une lib pour créer des clients WAMP en Python.
  • Crossbar.io: un routeur WAMP.

Il y a pas mal de notions à prendre en compte.

D’abord, le RPC.

Cela permet à un client de dire “les autres clients peuvent appeler cette fonction à distance”. On l’utilise pour exposer ip() et get_uuid() sur notre serveur et notre Javascript peut donc les appeler. Mais on l’utilise AUSSI pour qu’une des pages (le player) expose play(), pause() et volume() et que l’autre page (notre télécommande) puisse les utiliser.

La grosse différence, c’est que ip() peut être appelé par tous les clients en utilisant “demo.ip” alors que play() ne peut être appelé que par les clients qui connaissent l’ID du player, puisqu’il faut utiliser “<id>.play”.

Ensuite, il y a le PUB/SUB.

Cela permet à un client de dire “j’écoute tous les messages adressés à ce nom”. Et un autre client peut envoyer un message (on appelle ça aussi un événement, c’est pareil) sur ce nom, de telle sorte que tous les clients abonnés le reçoivent.

On l’utilise pour que notre télécommande dise “j’écoute tous les messages qui concernent les changements de status du player.” De l’autre côté, quand on clique sur un contrôle du player, on envoie un message précisant si le volume a changé, ou si on a appuyé sur play/pause. La télécommande peut ainsi mettre son UI à jour et refléter par exemple, la nouvelle valeur du volume.

Cela résume bien les usages principaux de ces deux outils :

  • RPC permet de donner un ordre ou récupérer une information.
  • PUB/SUB permet de (se) tenir au courant d’un événement.

Voici le workflow de notre projet :

  • On lance un serveur WAMP.
  • On connecte des clients dessus (du code Python ou Js dans notre exemple).
  • Les clients déclarent les fonctions qu’ils exposent en RPC et les événements qu’ils écoutent en PUB/SUB.
  • Ensuite on réagit aux actions utilisateurs et on fait les appels RPC et les publications PUB/SUB en conséquence.

Si vous virez tous les commentaires, vous verrez que le code est en fait vraiment court pour une application aussi complexe.

Encore une fois, il est possible de le faire sans WAMP, ce sera juste plus compliqué. Je vous invite à essayer de le faire pour vous rendre compte. Avec PHP, Ruby ou une app WSGI, c’est pas marrant du tout. Avec NodeJs, c’est plus simple, mais il faut quand même se taper la logique de gestion RPC et PUB/SUB à la main ou installer pas mal de libs en plus.

WAMP rend ce genre d’app triviale à écrire. Enfin triviale parce que là j’ignore tous les edge cases, évidemment. Pour un produit solide, il faut toujours suer un peu.

Les limites du truc

C’est du Python 2.7. Bientôt on pourra le faire avec asyncio et donc Python 3.4, mais malheureusement sans le serveur de dev.

Heureusement, Twisted est en cours de portage vers Python 3, et donc tout finira par marcher en 3.2+.

C’est du HTML5, mais bien entendu, rien ne vous empêche de faire ça avec du Flash si ça vous amuse.

C’est du WebSocket, mais on peut utiliser un peu de Flash pour simuler WebSocket pour les vieux navigateurs qui ne le supportent pas.

Non, la vraie limite c’est encore la jeunesse du projet : pas d’autoreload pour le serveur (super chiant de devoir le faire à la main à chaque fois qu’on modifie le code) et les erreurs côté serveur se lisent dans la console JS, et pas dans le terminal depuis lequel on a lancé le serveur. Plein de petits détails comme ça.

]]>
http://sametmax.com/introduction-a-wamp-en-python/feed/ 47