- "Ca ne fonctionne pas, mes enfants n'arrivent pas à se connecter à votre service !"
- "Nous sommes désolé notre équipe technique est mobilisée à 100% sur ce problème c'est une situation exceptionnelle… Nous vous tenons au courant."
C'est le genre de tweet ou de message LinkedIn qu'on a pu voir se multiplier avec le confinement récent dû au Covid-19. En effet les services web, et en particulier les outils de communication ou les programmes du secteur de l'EdTech (education technology), ont dû faire face à un usage particulièrement intense et soutenu ces derniers temps. Il n'y en a que peu à ma connaissance qui ont été capables de maintenir une qualité de service acceptable (Slack et Zoom ont particulièrement brillé sur ce sujet). Si la plupart ont mis en place des solutions minutes comme des queues de connexion,d'autres encore ont été forcés de désactiver leur service pour une "maintenance" à durée indéterminée…
Même en étant compréhensifs, face à un service planté, on a vite fait de changer. Après tout, avec le choix plus que fourni de services dont on dispose, pourquoi se priver ? Le problème c'est qu'après la crise, il est fort probable que nous ne reviendrons pas au service qu'on avait pourtant trouvé si cool au premier abord… Aïe, pour une startup c'est beaucoup de clients perdus, peut-être assez pour ne jamais retrouver de la traction. Une mort pénible donc pour un business, victime lui aussi du virus.
Mais 🧐
En premier lieu j'aimerais introduire le concept de scaling maturity . "To scale" c'est l'art d'adapter (automatiquement ou non) sa stack technique afin de répondre à la demande en entrée. Et reconnaissons déjà que Zoom et Slack sont beaucoup plus matures que (par exemple) de jeunes startups de l'EdTech.
Analysons les à l'aide du modèle de scaling maturity .
1 - Volume d'usage nominal : Slack ou Zoom avaient déjà un trafic (très) important, le pic d'activité représente un pourcentage plus petit que pour une startup pour qui c'est peut-être un boost de 100 ou 1000 fois l'activité habituelle.
2 - Maturité du produit : Ils ont eu le temps de connaître les spécificités de leur usage, les caractéristiques d'accès aux données, les points de fragilité de leur système, …3 - Compétences techniques : Ils ont probablement une équipe tech plus grande et plus expérimentée.
Pour synthétiser, ils en savent beaucoup sur la façon dont leur produit est utilisé et quelle est leur roadmap, et donc savent bien quel type d'effort concentrer pour s'adapter à la demande supplémentaire. En plus, leur infrastructure actuelle peut déjà encaisser un volume conséquent.
De l'autre côté, les jeunes services web se sont retrouvés submergés, cherchant de l'aide désespérement et des solutions pour sharder et répliquer leur base de donnée relationnelle existante (plus à ce sujet un peu plus loin).
Je prends à présent l'exemple hypothétique d'une startup EdTech offrant un service de classe en ligne innovant.
1 - Volume d'usage nominal : Quelques clients aiment leur produit, "c'est le futur", ils croient au potentiel de croissance et l'ajout de fonctionnalités avec le temps. Il y a donc un faible volume d'utilisation pour le moment et une croissance mesurée attendue, ils ont opté pour quelques serveurs OVH économiquement intéressants.
2 - Maturité du produit : Leur produit est très jeune, ils misent sur l'innovation et des boucles de feedback rapides pour l'étoffer.
3 - Compétences techniques : Des stagiaires, peut-être de jeunes employés, parfois des fondateurs qui font eux-mêmes les premiers prototypes. A ce niveau les salaires pèsent beaucoup dans la balance.
Je m'autorise ici une conclusion préliminaire à la première question : les entreprises n'auraient pas pu anticiper, et même j'irai plus loin pour les plus petites d'entre elles, elles ne devaient pas le faire… En effet, si on souhaite créer un produit avec du scaling "infini" dès le début, ça implique d'investir beaucoup en temps et en argent . Deux ressources précieuses que l'on préfère rationnellement investir sur d'autres sujets quand on est un business en phase de démarrage (comme trouver sa place sur le marché, ajouter des fonctionnalités, croître, …).
Covid-19 est un très bon exemple de ce qu'on appelle un évènement "cygne noir" Un évènement qui est très rare, a des répercussions massives, que les entreprises n'avaient donc pas prévu.
En effet ce point est assez évident, néanmoins je trouve cet "interlude du cygne" bienvenu 😉
J'ai pu parler récemment avec quelques startups EdTech qui recherchaient des solutions urgemment… Elles en étaient au même point : elles ne pouvaient plus doper les ressources de leur base de donnée relationnelle ( scale up ). Après avoir essayé d'ajouter du cache, déployer de nouveau nœuds, de refactorer leurs applications, le point limitant final restait la base de donnée… La seule solution était donc de sharder ( scale out ) afin de répartir les écritures sur plusieurs instances en parallèle. Ils cherchaient donc des solutions intelligentes (dans le sens autonomes) à ajouter en amont de leur base de donnée afin de pouvoir continuer le scale. Pas si évident, et plutôt cher…Et je ne parlerai même pas de la gestion de la migration dans ce contexte !
Pour avoir une idée de la difficulté de sharder une base de donnée existante, je dirais que plus le requêtage des données est global et complexe (par exemple de l'agrégation cross-compte), le plus intelligent et cher devra être le proxy en amont. Ca peut aller d'une simple hash-distribution à un "query planner" distribué complexe, et difficile à scale lui aussi par ailleurs.
A la lumière du modèle de scaling maturity , il est assez clair qu'on ne peut les blâmer de ne pas avoir eu ces mécanismes de scale déjà en place auparavant, mais elles auraient pu au moins avoir mieux planifié leur prochaine étape de scale .
Déconstruisons déjà ce qui doit être "scaled" :
Note : si votre produit n'a pas encore de traction réelle, se préoccuper de ce sujet est probablement prématuré et inutile, un rapide prototype MVC avec la techno que vous connaissez déjà fera tout à fait l'affaire.
Mais si vous avez de l'usage et une première idée de la direction du produit, alors il y a plusieurs possibilités. Si vous avez avec vous un fondateur au profil technique il sait certainement quoi faire. Sinon, et en particulier si vous avez levé des fonds, c'est une bonne idée de se faire accompagner sur le design de l'infrastructure dès à présent.
Explorons différentes stratégies.
Dissipons immédiatement les nuages de fumée, il est théoriquement possible d' approcher une telle architecture mais ce sera très coûteux (en temps et en argent – encore une fois deux choses précieuses pour une startup), et potentiellement assez rigide.
La clé ici serait d'utiliser au maximum des services gérés de haut niveau qui tournent sur de grosses infrastructures clouds. Les services choisis devraient être 100% dynamiques, c'est à dire scale de manière autogérée : on ne devrait pas avoir à gérer de ressources physiques ou même virtuelles. Idéalement ces services intègreraient de base de la réplication (pour scale en lecture) et du sharding (pour scale en écriture) et pourraient être répartis dans différentes régions sur la planète.
Voici quelques exemples de services gérés de cet ordre :
Hé oui, ça fait beaucoup de services Google, tout simplement parce qu'ils ont un train d'avance !
Ici je me dois de nuancer cette courbe
C'est la stratégie classique et probablement la plus efficace, on fait avec ce qu'on a à disposition au début (compétences, personnes) mais on essaie d'avoir toujours un coup d'avance. On reste conscient des points de fragilité du système, et on sait comment y remédier. On s'attache à planifier les prochaines migrations.
Ca nécessite en particulier :
Par exemple, les prochaines étapes de scale planifiées pourraient être :
Le principal est de rester économique et intelligent : pour scale efficacement il faut scale dans les bonnes proportions et au bon moment. Trop tôt et ça coûtera trop cher, trop tard et la qualité de service tombera… C'est pourquoi avoir un feedback pertinent (automatisé) sur l'architecture est un réel plus.
Les clouds proposent souvent des metrics de monitoring intégrées, par exemple si on utilise les environnements AWS Beanstalk pour des services web il est fourni automatiquement les métriques suivantes :
Il est très facile alors de les visualiser en dashboards, de configurer de l'alerting, ou même d'activer de l'auto-scaling basé sur le pourcentage de CPU utilisé… Autant d'outils que l'on peut utiliser directement pour scaler ses web services correctement. Après tout, le plus d'informations (métriques) on a à la base, le mieux on peut prendre les bonnes décisions au final.
Bien entendu, scaler efficacement c'est scaler de manière appropriée, à chaque situation sa réponse adaptée !
Pour conclure, la bonne solution se situe probablement entre les deux stratégies. Ca dépendra du budget, de la complexité technique du produit, et des compétences à disposition, …Au moins j'espère que vous avez une meilleure idée de comment anticiper le scale !
Scale safe 👋
Comment se gère un projet open-source ? Comment répondre aux demandes, les arbitrer sans crouler sous la pression. On aborde ces problématiques avec :
Je suis toujours très surpris d'entendre parler de la sécurité des applications frontend parce que précisément une application frontend s'exécute sur le périphérique de l'utilisateur et ne peut donc pas être sécurisée. Elle doit même être considérée comme un client potentiellement malveillant .
En effet, le code source de l'application étant à la disposition de l'utilisateur, il est possible de l'étudier et de le modifier à volonté afin d'en comprendre les mécanismes internes ou de récupérer toutes les données stockées sur le périphérique.
Je suis tombé sur de nombreux articles de diverses sources ( Callstack , Jscrambler , Tabris , Nativescript , Reactnativecode ) qui détaillaient les "techniques" pour sécuriser une application frontend en utilisant l'obfuscation, du chiffrement custom (XOR avec réutilisation de clé, etc...), et ainsi de suite.
Ce n'est pas la première fois que j'entends parler de recettes sur "l'écriture d'une application frontend sécurisée": Il peut être dangereux et contre-productif d'essayer de sécuriser une application avec des techniques inefficaces . Voici pourquoi.
TLDR;
L’envoi des informations d’authentification se fait de manière sécurisée au travers d’une connexion SSL, la confidentialité des communications est donc assurée dans la plupart des cas.
Ajouter une couche de chiffrement basique pour le mot de passe avec un simple XOR et une clé réutilisée pour chaque authentification de chaque client est inutile pour plusieurs raisons:
Rajouter une couche de chiffrement peut s’avérer être une bonne idée pour éviter la compromission des données dans le cas d’une attaque Man In The Middle avec un faux certificat SSL .
Pour un chiffrement solide, il sera nécessaire à minima:
Cela revient plus ou moins à implémenter une version custom de SSL dans son application et implémenter son propre système cryptographique n'est jamais une bonne idée .
When you try to reinvent the wheel..
Il est bien plus facile d’utiliser d’autres techniques de sécurisation tel que le SSL Pinning pour se prévenir des attaque MITM.
Dernier point, pour un maximum de sécurité, il est conseillé de générer des tokens ayant une durée de validitée courte pour limiter les dégats causés par une éventuelle compromission.
Lorsqu’il est nécessaire de stocker des données sensibles dans une application frontend, il est préférable d’utiliser les mécanismes mis à disposition par les créateurs de l’environnement de développement.
Par exemple, dans une application mobile avec React Native, nous pouvons utiliser la Keychain d’Apple ou le Keystore d’Android . Ces mécanismes rendent plus difficile l’extraction de données sensibles depuis un device mais ils ne doivent pas non plus être considérés comme inviolables . (Eg: Apple Keychain exploit )
Dans tous les cas, il est inutile de rajouter une couche de chiffrement supplémentaire réalisée avec une clé prédictible car un attaquant peut faire du reverse engineering sur l’application pour retrouver la clé.Ou encore plus facilement, simplement accéder à la clé stockée en mémoire .
Cela est contre-productif va consommer inutilement des ressources CPU pour le chiffrement/déchiffrement et donc drainer la batterie .
Bien que je puisse comprendre que les développeurs puisse vouloir compliquer la tâche de reverse engineering d'une application, l’obfuscation de doit jamais être considérée comme une pratique de sécurisation .
Elle peut au maximum décourager certains attaquants mais quelqu’un de motivé pourra toujours analyser et comprendre le fonctionnement de l'application.
Surtout si un obfuscateur open-source est utilisé car celui-ci est donc connu et des dé-obfuscateurs doivent certainement déjà exister.
De plus, l'obfuscation va rendre le code très difficile à interpréter et optimiser par les différents moteurs Javascript et il en résultera une baisse significative des performances de l’application .
Benchmark réalisé avec l' obfuscateur de react-native-obfuscating-transformer
Comme nous l’avons vu, une application frontend ne peut pas être sécurisée . Comme il est impossible d’avoir le contrôle sur le terminal du client, il est impossible de s’assurer que celui-ci n’est pas compromis.
C’est sur le backend que la majeure partie des éléments de sécurité doivent être mis en place.Il n’y a pas de recette magique pour sécuriser un backend, c’est un ensemble de bonnes pratiques de programmation qui permettra d’arriver à un résultat optimal.
Depuis le corps d’une requête HTTP , en passant par les headers ou encore les cookies , toutes ces informations qui peuvent être manipulées par l’utilisateur doivent être considérées comme potentiellement malicieuses.
L’utilisation naïve des saisies des utilisateurs peut amener à toutes sortes d’attaques:
Il est toujours nécessaire de vérifier et sanitiser ces données avant de les utiliser dans une application.
Les attaques par déni de service tentent de rendre une application indisponible.
Il est possible, par exemple, d'envoyer de très grandes requêtes en JSON , ce qui peut ralentir ou même rendre indisponible votre application.
Atténuer l'attaque : limiter la taille des requêtes dans les couches basses de votre application, de préférence directement dans les couches réseau.
Si votre backend est écrit en Node.js, il est également nécessaire d'être vigilant lors de la création de nouvelles promesses .
En effet, une Promesse est automatiquement envoyée à l'Event Loop et il est alors impossible de la retirer avant sa résolution ou son rejet.
Un attaquant peut alors envoyer beaucoup de requêtes sur une route générant des promesses et donc saturer l'Event Loop .
Atténuer l'attaque : implémenter un système de limite de requêtes simultanées utilisant uniquement des callbacks. Développer avec des callbacks c'est plus compliqué, mais les callbacks ne sont que des pointeurs vers des fonctions n'utilisant aucune ressource jusqu'à ce qu'ils soient invoqués. N'utilisez les promesses qu'après le système de limite de requêtes .
Afin d’éviter un bruteforce de l’authentification d’un utilisateur, il est nécessaire d’introduire une limite au nombre de tentative de connexion .
Cette limite peut prendre la forme d’un blocage après X tentatives rajouté à la route d’authentification.
En 2019, il y avait encore des entreprises qui stockent les mots de passes de leurs utilisateurs en clair .
Cette pratique doit être évitée à tout prix afin de protéger vos utilisateurs en cas de fuite de données de votre application. Pas seulement pour votre propre application : la majorité des utilisateurs réutilisent le même mot de passe pour plusieurs comptes . L'impact d'une fuite de mot de passe peut être catastrophique, tant pour vos utilisateurs que pour l'image de votre entreprise.
Les mots de passe doivent être stockés à l'aide d'une fonction cryptographique unidirectionnelle (ou fonction de hachage).
Le choix de la fonction de hachage doit être basé sur un algorithme robuste tel que bcrypt par exemple. Si vous le pouvez, renforcez les mots de passe faibles en utilisant du key stretching , et pour une couche de sécurité supplémentaire, vous pouvez également utiliser un salt et un pepper sur les mots de passes.
Il y a énormément d’attaques possibles sur un backend, et la plupart sont peu ou pas connues. Certaines sont particulièrement difficiles à détecter et à prévenir :
Une simple comparaison de deux chaînes de caractères peut se donner lieu à une attaque temporelle et permettre à un attaquant de deviner un mot de passe ou un token.
Mais encore l’utilisation d’une librairie d’expression régulières vulnérable à une attaque ReDoS .
C’est pourquoi la sécurisation d’un backend n’est pas une tâche à prendre à la légère et doit être confiée à des experts en sécurité pour former les développeurs mais aussi auditer le code afin de s’assurer d’avoir le minimum de failles possibles car la sécurité parfaite n’existe pas .
Lors du développement d'une application, la sécurité doit être prise en compte du début à la fin et la réflexion doit couvrir l'ensemble du périmètre de l'application , du backend au frontend, y compris les canaux de communication et l'hébergement.
La sécurité coûte cher et est donc souvent négligée. C'est pourquoi il est préférable d'utiliser des frameworks et des backends conçus par des ingénieurs possédant les compétences et connaissances nécessaires pour assurer une sécurité suffisante aux utilisateurs finaux.
J'aimerais remercier toute l'équipe de Kuzzle qui m'a aidé à rédiger cet article et en particulier Sébastien Cottinet et Yannick Combes pour leur expertise en sécurité et cryptographie.
Aujourd'hui, il est assez difficile d'imaginer faire des designs web responsivesans avoir recours aux media queries. Cette idée vieille de 1994, devenuerecommendation du W3C en 2012 (une fois supportée par tous les navigateurs) apris son temps et a su s'imposer comme l'outil de référence pour faire du designadaptatif.
À tel point qu'il parait impossible de faire du responsive sans media querydans l'imaginaire collectif.
Il faut pas se le cacher : travailler avec les media queries n'est pas toujoursévident. Cela implique pour chaque "morceau" de votre site ou appli qui vadevoir s'adapter de prévoir un ou plusieurs breakpoints lié à la tailledisponible de votre viewport. Écrire du code lié au viewport pour un "composant"bas niveau peut paraître clairement étrange.
Ce côté contre intuitif des media queries m'a toujours dérangé : on se retrouve à ciblerune taille d’écran, et non pas de cibler la taille disponible pour un élémentdonné.
Lorsque l'on creuse un peu, on tombe souvent sur le concept de "elementquery". Le rêve de tout intégrateur web : ce serait la solution à tous lesproblèmes posés par les media queries.
Franchement, écrire du code qui permet à un même composant de se retrouver surune même page a 2 endroits mais avec des dimensions différentes ça serait pascool ? Pas qu'un peu.
Alors il y a bien quelques techniques à ce jour notamment "les fab four" ou encore des tricks à base de floats ou d'autres trucs plus exotiques, maismalheureusement ce n'est pas toujours maintenable ou intuitif.
Dans notre monde "moderne" (j'en vois déjà certain cracher sur leur écran),pourquoi ne pas utiliser JavaScript? (voilà vous pouvez essuyer votre écran).Sérieusement, on pourrait se dire que dans notre contexte, il pourrait êtrepertinent de simuler des elements queries à coup de getBoundingClientRect
accompagné d'un observeur.
Certain dirons que encore une fois tout est question de compromis .
Mais si on veut faire du rendu côté serveur… Le JavaScript ne sera pas unebonne solution (oui ça m'arrive de penser à ce concept).
Rentrons dans le vif du sujet pour celles et ceux qui seraient intéressé·e·s parcette opportunité. Voici donc quelques astuces et pratiques que je vais vouslivrer.
Note: afin de mieux profiter du rendu des exemples prévus pour écran large,pensez à consulter les exemples en paysage si vous êtes sur mobile(ou directement sur CodePen qui offre une option de dézoom).Bah oui, on fait un article sur le responsive, donc regarder sur mobile desexemples prévus pour grand écran ça va pas le faire.
Première chose à bien visualiser : nous allons partir du principe que nousvoulons nous contenter de Flexbox. Aujourd'hui supporté par tous lesnavigateurs, Flexbox est le candidat idéal à ce jour pour faire du code propreet maintenable.
Avec Flexbox on peut "juste" faire donc des lignes et des colonnes.
Pour les colonnes, c’est très souvent peu problématique. Tout simplement par ceque l’on scroll le plus souvent verticalement. Je ne vais donc pas spécialementaborder cette axe là et me concentrer sur l’axe horizontal. Mais en changeantd’axe, ces pratiques seront toutes aussi pertinentes selon votre besoin.
Alors que faire ? On commence par quoi ?
On va prendre un exemple très simple où je me retrouve avec une ligne et troisblocs intérieurs. Dès que c’est possible je veux que ces trois blocs soient surune ligne. Par exemple sur un ordinateur de bureau. Ou un iPad. Ou un smartphonesacrément gros. Ou un smartphone en paysage.
<sectionstyle="display: flex;"><articlestyle="flex: 1; height: 50px; background: #fbb;">1</div><articlestyle="flex: 1; height: 50px; background: #bfb;">2</div><articlestyle="flex: 1; height: 50px; background: #bbf;">3</div></section>
Si on réduit notre exemple on va donc se retrouver avec ceci. Pas ouf.
Partant de ceci ça va être assez simple de faire la première étape. On varajouter flex-wrap.
<sectionstyle="display: flex; flex-wrap: wrap;"><articlestyle="flex: 1; height: 50px; background: #fbb;">1</div><articlestyle="flex: 1; height: 50px; background: #bfb;">2</div><articlestyle="flex: 1; height: 50px; background: #bbf;">3</div></section>
C’est plutôt moche et pas très réaliste. Ajoutons donc un petit peu de contenu,et un peu d’espace.
Avant qu'on me crache dessus car j'ai mis des div en guise de gouttière, jesouligne que c'est pour le cas d'école.
<sectionstyle="display: flex; flex-wrap: wrap;"><divstyle="width: 10px;"></div><articlestyle="flex: 1; background: #fbb;"><h2style="margin: 0; padding: 20px; font: 900 64px monospace;">Red</h2></article><divstyle="width: 20px;"></div><articlestyle="flex: 1; background: #bfb;"><h2style="margin: 0; padding: 20px; font: 900 64px monospace;">Green</h2></article><divstyle="width: 20px;"></div><articlestyle="flex: 1; background: #bbf;"><h2style="margin: 0; padding: 20px; font: 900 64px monospace;">Blue</h2></article><divstyle="width: 10px;"></div></section>
Si on rétrécit l’espace disponible, on aura un rendu qui va tenter de s’adaptercomme il peut.
Imaginons que ce rendu ne soit pas forcément souhaitable dans notre contexte.Formulé autrement: ces marges sont sacrément dégueulasses .
Pour être précis, elles ne sont pas adaptées à nos contraintes et au rendu quel’on souhaite avoir : on se retrouve avec un bout de marge perdu à un endroit oùl’on a pas vraiment envie qu’il se trouve.
Du coup comment qu’on fait ?
Petite technique facile à mettre en place et efficace : on va placer desdemi-marges sur le bloc plutôt qu’utiliser le concept de gouttière. Comme ceci :
<sectionstyle="display: flex; flex-wrap: wrap;"><divstyle="flex: 1; padding: 0 10px;"><articlestyle="background: #fbb;"><h2style="margin: 0; padding: 20px; font: 900 64px monospace;">Red</h2></article></div><divstyle="flex: 1; padding: 0 10px;"><articlestyle="background: #bfb;"><h2style="margin: 0; padding: 20px; font: 900 64px monospace;">Green</h2></article></div><divstyle="flex: 1; padding: 0 10px;"><articlestyle="background: #bbf;"><h2style="margin: 0; padding: 20px; font: 900 64px monospace;">Blue</h2></article></div></section>
Ce changement n’a aucun impact sur le rendu grand format, mais cela va nouspermettre en petit format d’obtenir le rendu suivant :
En fonction du contenu intérieur des blocs que vous allez avoir, vous allezpouvoir utiliser plutôt min-width
ou flex-basis
. Je vous laisse jouer un peuavec histoire de vous faire la main.
En fait je n’ai que cette astuce.
Je plaisante à peine. Car si on ajoute à cela le côté malin de overflow: hidden
pour cacher de l'information optionnelle, on peut faire destrucs assez puissant.
Regardons ça avec un exemple plus complexe : on va imaginer le header d'un site.
En appliquant cette technique à l’extrême, (ce qui n’est pas une quantité detravail astronomique, et reste quelque chose de propre et tout à faitmaintenable, surtout avec une approche composant et non document) on peut seretrouver avec un code très simple, sans media query qui donnerait les rendus suivants :
Mettez l'exemple ci-dessus avec un zoom à 0.5× pour mieux visualiser
<style>Header, Header * { box-sizing: border-box; position: relative; display: flex; flex-direction: column;}.Header { flex-grow: 0; flex-shrink: 0; flex-direction: row; background: #333; align-items: center;} .Logo { flex-grow: 0; flex-shrink: 1; min-width: 80px; height: 80px; flex-direction: row; flex-wrap: wrap; overflow: hidden; } .LogoIcon { flex-grow: 1; flex-shrink: 0; font-size: 64px; text-align: center; padding: 010px; } .LogoText { flex-grow: 0; flex-shrink: 1; margin: 0; padding: 20px; font: 90032px sans-serif; color: #fff; } .Center { flex-grow: 1; flex-shrink: 1; flex-basis: 800px; flex-direction: row; flex-wrap: wrap; justify-content: center; align-items: center; overflow-x: hidden; } .Links { flex-direction: row; flew-wrap: wrap; align-items: center; flex-grow: 4; flex-shrink: 1; min-width: 50%; overflow-x: auto; } .Link { margin: 0; padding: 20px; font: 60020px sans-serif; color: #fff; text-decoration: none; } .Search { flex: 1; max-width: 200px; min-width: 100px; border: 0; border-radius: 6px; padding: 10px20px; margin: 10px20px; font-size: 20px; background: #444; } .LinkGradient { content: ""; position: absolute; top: 0; bottom: 0; width: 40px; } .LinkGradient-left { left: 0; background: linear-gradient(to left, rgba(51, 51, 51, 0), rgba(51, 51, 51, 1)); } .LinkGradient-right { right: 0; background: linear-gradient(to right, rgba(51, 51, 51, 0), rgba(51, 51, 51, 1)); } .Networks { flex-grow: 1.5; flex-shrink: 0; min-width: calc(64px * 2 + 40px); flex-direction: row; flex-wrap: wrap; max-height: 64px; } .Network { flex-grow: 1; justify-content: center; align-items: center; padding: 10px6px; }</style><headerclass="Header"><divclass="Logo"><divclass="LogoIcon">♥️</div><divclass="LogoText">Logo</div></div><divclass="Center"><divclass="Links"><ahref="#"class="Link">Lien</a><ahref="#"class="Link">Lideux</a><ahref="#"class="Link">Limoche</a><ahref="#"class="Link">Libeau</a><inputplaceholder="Search"class="Search" /></div><divclass="Networks"><aclass="Network">👴</a><aclass="Network">🐦</a><aclass="Network">📸</a><aclass="Network">📌</a></div><divclass="LinkGradient LinkGradient-left"></div><divclass="LinkGradient LinkGradient-right"></div></div></header>
Vous allez me dire "mais ton exemple il est pas fou là", et vous avez pas tort.Je pense que cela dit, vous avez l'idée en étudiant le code.
Des exemples de ce type-là, surtout en exploitant bien flex-basis
, peuvent serévéler extrêmement puissants. On peut très bien se contenter de ça. Après commesouvent lorsqu’on a des besoins plus complexes, il sera à vous de juger sicontinuer à utiliser cette technique est pertinent, ou s’il est plus judicieuxd’utiliser des media queries afin d’éviter de vous défoncer le cerveau. Ou alors de fairedu rendu conditionnel avec JavaScript si votre platforme vous le permet.
Cette technique est aussi intéressante dans un contexte où les media queries nesont pas accessibles. Ce peut être le cas si vous utilisez un framework ou unelib qui ne propose qu’un sous-ensemble de CSS, comme React Native, qui vouslimitera dans l'ensemble à Flexbox et position absolute pour gérer votrelayout.
On peut aussi se retrouver à utiliser le même moteur que React Native surplusieurs plateformes directement avec Yoga ou Stretch .
On pourrait aussi avoir la même envie si on se retrouve dans un contexte Web oùCSS serait utilisable, mais où l’on se retrouve avec une abstraction qui nepermet pas de les intégrer simplement. Vous allez peut-être répondre : « mais ilest fou ? Il se fait du mal ».
Peut-être un peu. À moins qu’une des contraintes choisies soit de partager ducode entre différentes plates-formes (coucou react-native-web , react-native-windows , react-native-macos …) afind'éviter de faire une grosse app qui te bouffe bien la RAM car basé sur Electron(coucou Slack).
Dans tous les cas, media query disponible ou pas, cette astuce est pour moibien plus que ça puisque c'est devenu ma principale méthode pour faire duresponsive, faisant beaucoup d'appli React Native et/ou React Native Web.
Rien que pouvoir avoir le même composant produisant différents rendus sur unmême écran (en fonction de la taille disponible par son parent), ça devrait vousdonner envie !
[|"Bisous", "À la prochaine"|]|>Js.Array.joinWith(" et ")|>Js.log;
Le type option est vraiment super à utiliser en Reason, je vous conseille la lecture de l'introduction à ReasonML et l'article sur le type option . Mais j’ai un peu buté sur un petit point, c'est le lien entre les types option et les paramètres optionnels d’une fonction ou d’un composant React malgré la doc à ce sujet , alors je résume cela dans cet article.
On va prendre comme exemple un composant User
qui prend en paramètre un name
et une imageSrc
qui peut être facultative.
Une solution peut être d’utiliser le type option explicitement comme type de paramètre.
/* User.re */[@react.component]let make = (~name: string, ~imageSrc: option(string)) => { <div> <div> {imageSrc ->Belt.Option.map(src => <img src />) ->Belt.Option.getWithDefault(React.null)} </div> <div> name->React.string </div> </div>;};
À l’intérieur du composant le type option est parfait pour gérer l’optionalité du paramètre. En l'état, nous sommes obligé de l’utiliser de cette manière :
<User name="Ariel" imageSrc={Some("https://example.com/user.jpg")} /><User name="Manon" imageSrc={None} />
Il est impossible d’omettre le paramètre imageSrc
. Il faudra obligatoirement lui renseigner un option. Pour rendre le renseignement du paramètre vraiment optionnel, on peut déclarer le paramètre comme étant optionnel avec la syntaxe du point d'interrogation.
/* User.re */[@react.component]let make = (~name: string, ~imageSrc: option(string)=?) => { … };
Le point d'interrogation va permettre de renseigner directement le type inclus dans l’option (ici un string) ou d’omettre complètement le paramètre, ce qui donne ceci à l'usage :
<User name="Ariel" imageSrc="https://example.com/user.jpg" /><User name="Manon" />
La valeur sera automatiquement encapsulée dans un option , ce qui fait que l’implémentation ne change pas et on profite toujours des avantages du type option dans le composant.
Là on pourrait se dire que c’est bon c’est fini, mais en fait pas vraiment.
Si on résume, notre component accepte en paramètre que imageSrc
soit absent, ou qu’il soit un string. Mais à première vue, impossible de lui donner un option, et cela peut être embêtant à l’usage.
Par exemple, User
est utilisé dans un autre composant Follower
qui lui prend en paramètre un avatarUrl
, et qui sera un type option. Pour transmettre la valeur de avatarUrl
à imageSrc
nous seront obligé de faire quelque chose comme ça :
/* Follower.re */[@react.component]let make = (~name: string, ~avatarUrl: option(string)=?) => <div> {avatarUrl ->Belt.Option.map(url => <User name imageSrc={url} />) ->Belt.Option.getWithDefault(<User name />)} </div>;
Vu que imageSrc
ne prend pas de type option, on est obligé d'appeler <User>
différement selon que avatarUrl
soit Some()
ou None
.
Heureusement il existe une notation qui va nous permettre de renseigner un option, et on retrouve là encore le point d'interrogation :
<User name imageSrc=?{avatarUrl} />
Le point d'interrogation va permettre de renseigner le paramètre avec un type option, de transmettre la valeur contenue dans le Some()
de avatarUrl
ou d’omettre complètement le paramètre imageSrc
si avatarUrl
est None
.
Si on résume, la première syntaxe qui s'utilise dans la déclaration d'une fonction permet de déclarer un paramètre comme optionnel, la fonction acceptera de recevoir une valeur, comme il acceptera qu'on n'en passe aucune .
Ensuite, la seconde syntaxe qui s'utilise à l'appel d'une fonction permet de transformer une valeur de type option en paramètre optionnel .
Dis comme ça, cela peut sembler inutilement complexe, sauf que la complexité est gérée par le langage , et qu’à l’utilisation on y gagne en souplesse :
Pour les besoins de l’exemple j’ai volontairement utilisé du code très explicite, mais il faut savoir qu’on a quelques raccourcis dans la notation.
L’avantage du JSX de ReasonReact par rapport à React.js, c’est le punning. Le raccourci lorsqu'on a une prop ayant le même nom que la variable qu'on y passe. On a déjà l’habitude en JS avec les objets, au lieu de return {name: name}
, on peut faire return {name}
.
On peut aussi l’utiliser avec le point d'interrogation, par exemple :
[@react.component]let make = (~name: string, ~avatarUrl as imageSrc:option(string)=?) => <div> <User name imageSrc=?{imageSrc} /> </div>;
devient
[@react.component]let make = (~name: string, ~avatarUrl as imageSrc:option(string)=?) => <div> <User name ?imageSrc /> </div>;
(d’ailleurs, si vous écrivez le code sans punning, le Reformat vous réécrit automatiquement votre code avec).
C’est aussi l’occasion d’aborder le fait de pouvoir renommer un argument avec as
. Il ne faut vraiment pas s’en priver. En fait il faut savoir que (~name)
est lui même un raccourci pour (~name as name)
.
L’inférence de type est vraiment très bonne en Reason, si j’ai explicité le type option et leur contenu string, je peux très bien m’en passer. Ce qui nous donne un exemple final moins verbeux :
/* User.re */[@react.component]let make = (~name, ~imageSrc=?) => <div> <div> {imageSrc ->Belt.Option.map(src => <img src />) ->Belt.Option.getWithDefault(React.null)} </div> <div> name->React.string </div> </div>;/* Follower.re */[@react.component]let make = (~name, ~avatarUrl as imageSrc=?) => <div> <User name ?imageSrc /> </div>;
Le type option se révèle très pratique lors de l'écriture d'un fonction ou d'un component. Les paramètres optionnels, avec les deux syntaxes utilisant le point d'interrogation, permettent de ne pas complexifier l'usage de la fonction ou du component.
Dans l'exemple final, l'implémentation de User
n'a pas changé depuis le début car nous voulons gérer l'aspect facultatif du paramètre. En revanche Follower
n'avait absoluement pas besoin de le gérer dans son implémentation, le code s'en serait trouvé inutilement alourdi.Nous gardons ainsi une écriture fluide tout en permettant de gérer l'optionnalité d'une valeur lorsque cela est nécessaire.
Notre métier implique d'arbitrer ce qu'on appelle des tradeoffs. Il s'agit de définir les points positifs et négatifs d'une solution et d'en estimer la balance. On choisit ainsi la solution correspondant le mieux (du moins à nos yeux) à nos besoins de cette façon.
Il existe cependant dans notre industrie un système de dogme. On ne cherche alors plus à mettre en perspective des tradeoffs et à les comparer, mais à faire se battre des "écoles de pensées", chacune ayant développé des axiomes (principe non démontré mais utilisé comme base d'un raisonnement).
Suite à un énième débat sur les technologies modernes utilisées en front-end, réfutées par les défenseurs de certains de ces axiomes, je vais tenter de rationaliser notre approche et d'expliquer ses tradeoffs.
L'idée est ici de faire comprendre pourquoi on utilise ces approches et technologies, dans quel contexte , et non de les imposer à qui que ce soit. Avec un peu de chance, cet article fera passer certains discours de "nimportawak (sic)" à "ce n'est pas pour ma typologie de projet".
Au départ, le Web est conçu comme un ensemble de documents : chaque page en est un. À chaque navigation, on déclenche un nouveau cycle de vie de page : on termine la page courante, on initialise la suivante.
Ce modèle est très simple et permet une expérience très correcte pour des pages majoritairement statiques.
On a une page servie en HTML, une feuille de style servie en CSS. Et that's it . On apprend qu'il faut bien les séparer (au nom du principe de separation of concerns , au cas où il y'en ait un qui foire, une expérience dégradée doit être proposée.
Avec les années, les exigences des utilisateurs sont devenues plus hautes : il a fallu y répondre par des pages plus interactives et des techniques de rechargements partiel de page (le cycle de vie d'une page étant plutôt coûteux). On a donc commencé à ajouter une intelligence limitée avec un peu de JS, notamment quelques briques interactives, charger des bouts de page avec AJAX.
Ces techniques ont permis de drastiquement améliorer l'expérience de navigation des utilisateurs : on a moins de données à charger, on affiche ce que les gens veulent voir plus vite (ce serait quand même un poil relou de charger une nouvelle page dès que vous zoomez sur Google Maps). Et c'est ici le premier tradeoff :
Puis arrive la multiplication des plateformes mobiles. Pour toucher les utilisateurs, le Web n'est plus LA plateforme, mais UNE plateforme parmi d'autres. Il devient alors stratégiquement intéressant pour les entreprises de commencer à développer des socles communs sous la forme d' APIs auxquelles de multiples clients sous différentes plateformes souscriront .
À cette époque, le front-end connait une mutation sans précédent : on commence à créer de véritables applications , non plus des documents auxquels on greffe hasardeusement quelques fonctionnalités. On se dote alors d'outillages plus avancés, issus de patterns déjà éprouvés dans d'autres domaines du software (comme le MVC). L'ère du fichier JS fourre tout qui initialise 3 plugins jQuery pour faire des carousels est révolue.
On commence alors à réfléchir en termes de vues . On gagne également une certaine indépendance vis à vis du back-end, on peut générer notre interface directement depuis le JS.
On n'a plus à nécessairement apprendre le fonctionnement de stack back-end, son organisation, son langage de templating : on devient maîtres de nos stacks.
On s'approprie de nouvelles problématiques comme le routage, le data-fetching et la création de caches clients intelligents. Des frameworks proposant des solutions à celles-ci émergent alors (Angular, Ember et Backbone pour ne citer qu'eux).
Puis débarque React avec une approche unique face à ses concurrents: les composants. On en crée un pour chaque bloc réutilisable de l'application.
Un composant, c'est une boite noire qui prend des paramètres ( props
), peut avoir un état local ( state
) et qui va décrire l'interface à n'importe quel point dans le temps.
React arrive également avec JSX, une extension de JS, qui permet de décrire son interface sous une forme ressemblant à HTML (du XML), mais s'affranchissant de ses limitations (comme la nécessité de serialiser les attributs). Tout en conservant la familiarité d'HTML (et la pertinence d'une telle syntaxe pour représenter un arbre d'éléments), JSX répond à une frustration grandissante face aux templates "logic-less" qui forçaient la création de helpers et la transformation de donnée en amont.
En nous abstrayant complètement du DOM et en nous offrant un modèle conceptuel simple ( (props, state) => UI
), React permet de créer des interfaces plus riches, plus simplement et surtout d'une manière maintenable : le comportement d'un composant étant couplé à son markup, on n'a plus à naviguer entre un fichier HTML et un JS pour les synchroniser. L'isolation des composants permet d'éviter les effets de bords indésirables.
HTML et JS sont donc colocalisés, leur édition est mise en commun. Surprise : on s'est rendu compte que c'était une façon de faire plus productive et qu'on avait moins tendance à laisser pourrir du vieux code dans son coin.
Ce problème subsiste avec CSS : il est toujours possible d'écrire du code CSS ayant un impact non désiré sur un composant autre que celui que l'on visait . On constate des guerres de spécificités, des régressions visuelles et un manque de visibilité sur l'impact d'un changement. Si vous héritez de code avec lequel vous n'êtes pas ou plus familier, le risque de casser quelque chose est grand.
Les techniques d' isolation "manuelles" telles que BEM prennent de la popularité. On évite alors les sélecteurs ésotériques, et on fait au plus simple, avec une méthodologie de découpage faite en parallèle de nos composants (les classNames de mon composant Button
vont être préfixées par Button
), plus maintenable. Étant à la discrétion des devs, cette méthodologie reste sujette à l'erreur, il faut vérifier que l'on n'utilise et ne casse pas un namespace existant.
Puis arrivent les solutions automatisant cette isolation, délégant la tâche à la machine plutôt qu'à l'humain : CSS Modules et CSS-in-JS. Avec ces techniques, une erreur ne peut plus dépasser le scope de son composant . Le CSS non utilisé sur une route donnée n'est jamais injecté : le CSS mort est éliminé par défaut (un problème virtuellement impossible, et pour le moins non automatisable, avec une feuille de style traditionnelle).
CSS-in-JS ramène le style au sein du composant , dans son scope. Notre composant contient désormais son markup, son style et son comportement.
Il a été dit que cette approche rompt la separation of concerns , mais cette vision part du postulat que l'on doit impérativement coder des documents et oublier l'approche composant. Un postulat qu'on a oublié de réévaluer avec la perspective du développement tel qu'il est fait. Dans un contexte applicatif, séparer markup, style et comportement revient à s'imposer une séparation technologique non nécessaire et pouvant au nom d'une "bonne pratique" impacter négativement l'expérience des devs et des users.
Il n'existe plus de raison autre que la "nostalgie du bon vieux temps" de le faire, il s'agit de reflexes acquis à l'époque mais jamais remis en perspective. Demandez à quelqu'un pourquoi c'est mal, il vous répondra "SEPARATION OF CONCERNS!". Demandez-lui pourquoi, il y a peu de chances qu'il vous sorte quoique ce soit de tangible.
L'approche CSS-in-JS ne pose pas de problème lorsque l'application est entièrement gérée côté client. Mais elle peut-être embêtante pour des applications rendues côté serveur : le CSS sera absent de la page HTML chargée initialement et vous aurez un FOUC (Flash Of Unstyled Content). Heureusement, la grande majorité des solutions de CSS-in-JS proposent l'extraction des styles lors du rendu serveur. Il extrait les styles critiques de la page et les accole au rendu de l'application générée. Vous chargez moins de CSS et l'application côté client prendra le relai pour charger et injecter les règles au besoin.
Chaque solution possède ses tradeoffs. Prenons pour exemples les temps de chargement des différentes approches et notons les avec des lettres de A à F (A étant le plus rapide, F le moins):
Chacune de ces solutions peut correspondre à vos besoins. Un document privilégiera le chargement initial et une application les navigations en son sein. Rien n'est parfait, il s'agit (et s'agira encore probablement pour longtemps) de décider du tradeoff que vous êtes prêt à faire.
On écrit aujourd'hui des applications avec une technologie en pleine évolution, initialement prévue uniquement pour faire des documents.
On se heurte au conservatisme de certains que refusent de voir le modèle "détourné" d'une utilisation telle qu'elle a été prévue il y a 20 ans. Mais il faut leur rappeler qu'on veut faire des applications utiles à nos utilisateurs, plus légères, plus rapides, qui dépassent le cadre prévu initialement par une approche document, et qu'on veut pouvoir les faire maintenant , parce que nos users n'ont pas grand chose à carrer du fait qu'une fonctionnalité doive exister dans les standards pour que les devs puissent l'utiliser. Si Dulux Valentine ne vendait que des couleurs primaires, ça vous viendrait à l'idée d'aller gueuler sur les gens qui font leur mur en vert en mélangeant du jaune et du bleu ?
Alors on expérimente, on détourne des usages, on crée des choses, on tire profit d'APIs pas prévues pour ça à la base, on délivre des applications capables de choses qu'on n'imaginait pas possible sur le Web il y a quelques années. Grâce à toutes ces approches, et en s'enlevant le poids de règles obsolètes, on le fait plus vite, on le fait mieux, on le fait plus proprement.
Et vous savez quoi ? Ça ne casse pas le Web. Ça ne casse pas vos pages. Ça n'est pas moins accessible. Ça ne fait qu'utiliser les outils standards Web qu'on a disposition pour faire plus. Et ça permet en plus de guider le W3C en leur montrant que certaines solutions sont utilisées pour résoudre certaines problématiques qui ne sont pas directement adressées par les standards.
Vous n'avez personnellement pas besoin de ces approches ? Le jour où ces besoins émergent, vous saurez qu'elles existent, il suffit de comprendre pourquoi elles sont mises en place, quelles problématiques elles adressent, quels sont leur tradeoffs . Vous pourrez toujours ne pas les apprécier, mais au moins vous les aurez comprises, et elles constitueront une alternative supplémentaire pour le jour fatidique. Quand on a suffisamment de cordes à son arc, on peut jouer de la harpe.
Un épisode avec :
Liens:
Un épisode avec :
Les liens:
La track du générique:
La plupart des langages populaires aujourd'hui ont une valeur particulière appelée null
. Elle représente l'absence délibérée de valeur. JavaScript possède aussi undefined
, qui fonctionne à peu près de la même façon mais pour d'autres significations.
Un des problèmes souvent rencontré dans ces langages est que null
est implicitement accepté comme valeur possible de n'importe quelle variable. Il est donc assez facile de se trouver avec un null is not an object
ou une célèbre NullPointerException
avec une stacktrace qui ne vous dira pas d'où est sorti ce null
.
null
est une valeur importante pour la conception de programme : on n'a pas toujours de valeur, et il faut être en mesure de l'exprimer dans notre code. Pourtant, la plupart des langage fonctionnels statiquement typés n'ont pas de concept de null
.
Comment gèrent-ils ça ? Avec un type option (aussi appelé type maybe dans certains langages), qui est un petit conteneur qui englobe la valeur (ou la non-valeur). Puisqu'il nous faut un langage statiquement typé pour nos exemples, nous allons utiliser ReasonML dont je vous ai déjà parlé et avec lequel on a construit ce site .
Le type option est un variant , qui peut de loin s'apparenter à un type d'union. Par exemple:
type status = | Inactive | Active;
Une valeur de type status
pourra être soit Inactive
, soit Active
et elle ne pourra être qu'une seule de ces valeurs à la fois.
Maintenant, voyons la définition du type option
:
type option('value) = /* on définit les différentes valeurs possibles, une valeur du type option sera forcément d'une des deux listées ci-dessous */ | None/* pas de valeur */ | Some('value); /* une valeur du type `'value`*/
'value
est ici ce qu'on appelle un paramètre de type , ça permet au type d'être «génerique»: il se fout du type de la valeur contenue, et vous laisse le spécifier à l'usage ou laisse l'inférence de type le deviner.
let isMyself = fun | Some("Matthias") => true | Some(_) | None => false;
Ici, la fonction aura la signature suivante :
let isMyself: option(string) => bool; /* ^ le compiler a compris qu'il s'agissait d'une chaîne de caractères! */
Cette généricité fait de l'option une abstraction générale pour représenter la présence ou l'absence de n'importe quel type de valeur. Cela nous permet par exemple de créer une fonction map
:
let map = (opt, f) => switch (opt) { | Some(x) => Some(f(x)) | None => None };
Et cette fonction pourra être utilisée pour n'importe quelle option . Jetons un œil à sa signature :
let map: (option('a), 'a => 'b) => option('b);
On peut lire cette signature de cette façon :
a
a
et retourne une valeur de type b
b
Some(2)->map(x => x *3) //Some(6)None->map(x => x *3) //None
Un autre exemple de fonction utile est flatMap
:
let flatMap = (opt, f) => switch (opt) { | Some(x) => f(x) | None => None };/* let flatMap: (option('a), 'a => option('b)) => option('b); *//* `get` retourne une option */let zipCode = get("profile") ->flatMap(profile => profile->get("address")) ->flatMap(address => address->get("zipCode"));/* zipCode est un `option(string)` */
Prenons pour exemple la fonction Array.prototype.find
de JavaScript :
let result = array.find(item => item === undefined || item.active);
result
sera:
active
ayant une valeur évaluée comme vraie undefined
si un item de array
est undefined
undefined
si rien n'est trouvé Avec cette implémentation naïve, on est incapable de savoir dans quel cas on se trouve : soit on a trouvé un item undefined
, soit on a rien trouvé.
Notez que le problème se pose ici avec undefined
mais qu'il en serait de même un tableau contenant des null
et une fonction find
d'une bibliothèque retournant null
dans le cas où elle ne trouve rien
Si l'on veut être capable de faire la différence entre les deux derniers cas, on doit utiliser une autre fonction: findIndex
:
let index = array.findIndex(item => item === undefined || item.active);if (index == -1) { // not found} else { // foundlet result = array[index];}
Le code est plus lourd, moins lisible, et manque d'expressivité. find
ne nous donne pas assez d'information au travers de la valeur retournée: undefined
est "aplati", et requiert une logique supplémentaire (ici index
, si un item est trouvé, il sera supérieur à -1
, sinon il sera égal à -1
)
Le problème ne vient pas de la fonction find
elle même mais de la façon dont null
et undefined
sont traités. null
est la valeur, il la remplace . option
l'englobe : c'est un conteneur.
openBelt; /* la stdlib *//* `getBy` est l'equivalent de `find` */let result = array->Array.getBy( fun | None => true | Some({active}) => active);
D'abord, array
a le type suivant:
let array: array(option(value));
Et getBy
celui-ci:
let getBy: (array('a), 'a => bool) => option('a);
Si on remplace les paramètres de type par le type vraiment utilisé dans notre cas précis, on se retrouve avec ça :
let getBy: ( array(option(value)), option(value) => bool ) => option(option(value));
result
aura donc le type suivant :
let result: option(option(value));
C'est une option
d' option
de value
. Et ça signifie qu' on peut extraire l'information qui nous intéresse de la valeur de retour:
Some(Some(value))
: on a trouvé une valeur true
pour le champ active
Some(None)
: on a trouvé une valeur None
None
: on n'a rien trouvé dans le tableau Le type option
a éliminé par design certains problèmes inhérents à null
et undefined
en se comportant comme un conteneur plutôt qu'un substitut.
La nature des option
dans les langages statiquement typés permet d'éviter de nombreuses erreurs de conception. Les fonctions ne sont plus juste autorisées à retourner null
et à vous laisser la responsabilité implicite de le gérer à grand coup de if(value == null) { a } else { b }
, elles retournent un type option
qui vous force à prendre en compte l'optionalité de la valeur .
Avant de pratiquer un langage fonctionnel typé, je n'arrivais pas à piger comment ces langages pouvaient de débrouiller sans valeur null
. J'espère que si vous êtes dans le même cas, ce petit post vous aidera à mettre ces deux approches en perspective.
Sir Tony Hoare, l'inventeur de la référence null
l'appelle aujourd'hui sa billion dollar mistake . Le gars l'a inventé en 1965, on va pas lui en vouloir, mais plus de 50 ans après il serait peut-être temps de reconsidérer le bien fondé du truc et se pencher sur les alternatives qui éliminent le problème plutôt que de continuer à mettre des if
partout et de continuer à détricoter des stacktraces ne contenant même pas la source du bug.
Bisous.
Vous venez de coder un composant TwitterButton
(avec React, Vue, en suivant une méthodo BEM, OOCSS, ou autre: c'est comme vous voulez) et franchement c'est du beau boulot: le rendu est vraiment très joli, kudos au designer.
Seulement très vite, ce dernier jette un coup d'oeil à la recette et vous fait un petit retour parce qu'il :
Vous ajoutez donc quelques media queries pour adapter le style de ce bouton en fonction de la largeur du viewport. Ses dimensions changent maintenant lorsque la page fait plus de 768px
de large, puis lorsqu'elle fait plus de 968px
et enfin plus de 1200px
. Un chouia fastidieux.
Vous pestez un peu sur votre collègue qui aurait dû vous fournir toutes les maquettes (alors qu'il n'a pas forcément eu le temps de les créer) et lui peste car vous l'avez dérangé toutes les 2 minutes pour obtenir ces mesures intermédiaires.
Il vous reste 72 composants à coder. Super ambiance dans les bureaux 👏🏼
Plutôt que de demander à votre supérieur Jean-Michel de prendre parti pour résoudre ce problème, nous allons faire appel aux MATHS .
Des termes foutrement complexes pour définir quelque chose de très simple: il s'agit de faire transiter une valeur γ de α à β de façon linéaire et dans notre cas borné dans un intervalle donnée.
En partant de ça, nous allons définir une UI fluide à l'aide de 3 variables :
baseFontSize: number (px value)
scaleRatio: number (abs value)
fluidRange: [number (px value), number (px value)]
Prenons l'exemple d'un site web où, en mobile-first, la taille de police par défaut ( baseFontSize
) est de 16px
. On souhaiterait que celle-ci soit de 20px
lorsque le viewport fait plus de 1600px
de large (donc que le coefficient d'agrandissement - scaleRatio
- soit de 20 / 16 = 1.25
) et que la transition pour passer de 16 à 20 ne se déclenche pas avant que le viewport fasse au moins 480px
de large.
La fonction suivante va nous permettre d'obtenir cette fameuse interpolation linéaire sous le forme d'une formule CSS avec calc()
:
// on utilise JS par praticitélet getLinearInterpolation = ( baseFontSize, // number scaleRatio, // number fluidRange // [number, number]) => { let maxFontSize = baseFontSize * scaleRatio; let [rangeStart, rangeEnd] = fluidRange; let multiplier = (baseFontSize - maxFontSize) / (rangeStart - rangeEnd); let fixed = maxFontSize - multiplier * rangeEnd; return`calc(${fixed}px + ${100 * multiplier}vw)`;};
Si vous copiez-collez ça comme un sagouin dans la console devtools de votre navigateur web et tentez un essai avec les valeurs de notre exemple, vous obtiendrez normalement :
Voyons maintenant comment nous servir de ça.
L'intérêt de cette valeur, c'est qu'elle va nous permettre de modifier toutes les dimensions que l'on veut de façon progressive et proportionnelle .
Petit exemple, simple, basique :
<html><body><h1class="title">Hello world</h1><divclass="red-block"></div></body></html>
// exemple avec SCSShtml { font-size: 16px; // baseFontSize}@media (min-width: 480px /* fluidRange start */) { html { // l'interpolation linéairefont-size: calc(14.285714285714285px + 0.35714285714285715vw); }}@media (min-width: 1600px /* fluidRange end */) { html { font-size: 20px; // baseFontSize * scaleRatio }}// par défaut dans les navigateurs 1rem = 16px, cette fonction nous simplifie les divisions// si vous faites du CSS-in-JS, let fluid = v => `${v / 16}rem` fait le job@function fluid($value) { @return $value / 16 + rem;}.title { // si largeur du viewport < 480px -> font-size = 24px// si largeur du viewport > 1600px -> font-size = 24 * 1.25 = 30px// si 480px < largeur du viewport < 1600px -> 24px < font-size < 30pxfont-size: fluid(24);}.red-block { background-color: red; // les dimensions seront contenues entre 100px et 100 * 1.25 = 125pxheight: fluid(100); width: fluid(100);}
(Cliquez sur le gif pour le voir en taille réelle)(On va se mentir et tenter d'ignorer le fait que tout le monde utilise le zoom)
En effet, l'utilisateur peut toujours choisir d'avoir une taille de police plus petite ou plus grande que celle par défaut ( 16px
) et c'est franchement pas très accessible de forcer.
On va donc modifier notre fonction JS et tenir compte de ça.
let getCSSFluidConfig = ( baseFontSize, // number scaleRatio, // number fluidRange // [number, number]) => { let toRem = value => value / 16; let maxFontSize = baseFontSize * scaleRatio; let baseRemFontSize = toRem(baseFontSize); let maxRemFontSize = toRem(maxFontSize); let [rangeStart, rangeEnd] = fluidRange; // on évite rem pour les media queries: merci Safari// pas de soucis pour utiliser toRem malgré tout:// les media queries sont à la racine du documentlet emRangeStart = toRem(rangeStart); let emRangeEnd = toRem(rangeEnd); let multiplier = (baseRemFontSize - maxRemFontSize) / (emRangeStart - emRangeEnd); let fixed = maxRemFontSize - multiplier * emRangeEnd; // on en profite également pour retourner l'intégralité du CSS voulureturn`html { font-size: ${baseRemFontSize}rem }@media (min-width: ${emRangeStart}em) { html { font-size: calc(${fixed}rem + ${100 * multiplier}vw) }}@media (min-width: ${emRangeEnd}em) { html { font-size: ${maxRemFontSize}rem }}`;};
Et voilà ! Ça continue de faire ce que l'on veut, mais en prenant en compte la taille de police par défaut définie par l'utilisateur.
Vous vous êtes empressé d'embêter le designer à nouveau afin de déterminer ces 3 variables ensemble: ça sera donc une font-size
comprise entre 16px
et 18px
(donc une UI qui scale jusqu'à 18 / 16
= 1.125
…vous êtes encore frileux à l'idée) entre 480px
et 1440px
!
Il est maintenant temps de modifier ce fameux bouton.
getCSSFluidConfig(16, 1.125, [480, 1440]);/* -> "html { font-size: 1rem }@media (min-width: 30em) { html { font-size: calc(0.9375rem + 0.20833333333333334vw) }}@media (min-width: 90em) { html { font-size: 1.125rem }}" */
// le code généréhtml { font-size: 1rem;}@media (min-width: 30em) { html { font-size: calc(0.9375rem + 0.20833333333333334vw); }}@media (min-width: 90em) { html { font-size: 1.125rem; }}// fonctions utilitaires@function fluid($value) { @return $value / 16 + rem;}// le reste du CSSbody { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";}.twitter-btn { align-items: center; display: flex; background-color: #1da1f3; border: 1px solid #218de4; // il ne serait pas logique que border-width soit fluideborder-radius: 4px; // idem pour border-radiuspadding: fluid(4); padding-right: fluid(8); box-shadow: 05px5px -5px rgba(0, 0, 0, 0.25);}.twitter-btn svg { height: fluid(24); width: fluid(24);}.twitter-btnspan { color: #fff; font-size: fluid(14); margin-left: fluid(4);}
Ainsi,
font-size
par défaut fera entre 1rem
et 1.125rem
16px
et 18px
si réglage navigateur par défaut) padding
de .twitter-btn
fera entre 0.25rem
et 0.28125rem
4px
et 4.5px
) height
et la width
du svg feront entre 1.5rem
et 1.6875rem
24px
et 27px
) La différence est très subtile (mais vous étiez frileux). L'avantage, c'est que si vous changez d'avis dans 3 semaines pour finalement passer sur un agrandissement de x1.5 à 2560px
de large, il vous suffira générer un nouveau ce petit bout de code, de copier/coller les quelques lignes obtenues au début de votre fichier CSS …et c'est tout ! Inutile de revenir dans les composants ou de refaire un quelconque calcul.
Pour que ce soit encore plus simple, je vous ai concocté un petit générateur :
Il ne vous qu'à profiter de toute ces heures gagnées en invitant votre (maintenant pote) graphiste à boire une bière ! 🍻
DISCLAIMER : Je me suis rendu coupable de tous les crimes contre la lisibilité que je décris ici, de A à Z.Je ne prétends aucunement que faire ces choses fait de vous (ou moi du coup hein, on se protège) de mauvais devs, je dis simplement que peut-être qu'il est temps qu'en tant qu'industrie nous nous posions pour réfléchir sur ce que nous devons défendre. Je pense que le code peut être simple, je pense que connaître les comportements arcanes d'un langage peut être à la fois une force et une faiblesse. Je pense que le développement doit être accessible au plus grand nombre. Je pense qu'il est temps d'en finir avec la notion de vrai ou de faux développeur. Je vous souhaite une bonne lecture, les opinions exprimées ici sont les miennes donc n'hésitez pas à me coller un procès au cul, à moi et moi seul (je partage pas).
Salut les copains, je profite de mon article annuel pour vous parler d'un autre truc qui commence doucement à m'éplucher les mirettes dans vos pratiques actuelles du développement informatique et pour cette fois, je connais un peu plus mon sujet.
Voyez-vous, J'ACCUSE; J'accuse vous, j'accuse moi, j'accuse tout, j'accuse toi. Je vous accuse tous autant que vous êtes de bien trop vous prendre la tête sur le styling de vos pages. Vous vous faites royalement chier à trouver la ligne de code élégante pour régler votre problème sans avoir l'élémentaire courtoisie de vous demander si un manos précédent n'a pas réussi à régler un problème corollaire sinon adjacent via StackOverflow . Que ce soit avec Flex ou sans Flex, Grid ou sans Grid, float:left
ou sans float:left
, vous vous fourvoyez à la race .
Soyez sans craintes très chers amis car joj (c'est moi, je m'appelle plus skinnyfoetusboy, surprise !) a la solution à tous vos petits maux, bobos et tracas. J'ai récemment élaboré une méthodologie CSS qui vous permettra de maintenir votre code plus efficacement, rendra la relecture plus simple et fera même revenir l'être aimé.
La recette miracle tient en trois lettre : RUD .
Rajoutez Une Div.
Rajoutez. Une. Div.
Le postulat de base est simple, il arrive régulièrement que nous cherchions la pureté dans notre code, cet instant où la grâce divine guide nos doigts pour écrire le code à notre place et où la frénésie remplace avantageusement le café dégueulasse à 35¢ de la machine d'à côté afin d'enfin venir à bout du centrage de cette div qui vous gonfle depuis bien trop longtemps, et puis merde quoi, c'est pas à ça que ça devait servir, Flex ?
Vous tapotez ardemment sur votre clavier, Glenn Gould dans les oreilles comme sur les doigts pour trouver la solution à ce problème, putain, pourquoi quand ça marche sur IE ça foire sur Chrome et pourquoi quand ça marche sur Chrome ça foire sur IE ?
Vous cherchez depuis trop longtemps comment régler ça efficacement, depuis tellement longtemps alors que la solution est bien plus simple : RAJOUTEZ. UNE. DIV.Enveloppez votre problème dans une div, foutez un justify-whatever: center
et voilà.
Posez-vous la question : pourquoi est-ce que vous n'avez pas rajouté cette div plus tôt ?Pris dans un orgueil mal-placé et dans la volonté de faire beau/élégant/malin/stylé/wow-je-vais-en-faire-un-codepen plutôt que de faire simplement bien et maintenable, et malgré la meilleure méthodologie BEM du monde, on s'évertue à rentrer du :before
, du :after
et d'autres tas de trucs qui pourraient être vachement mieux branlés en rajoutant simplement une div dans le DOM à l'endroit où elle devrait être : dans le template.
Pourquoi vous créez des éléments dans votre CSS bordel ?
Pourquoi j'en vois des qui gueulent sur le CSS-in-JS et qui font à côté la défense du HTML-in-CSS ?
"Oui mais je vais quand même pas rajouter une div simplement pour afficher un chevron alors que je peux simplement claquer un :before
?" Si, si, vous allez faire ça, ça va vachement simplifier la peer-review de la pauvre personne qui s'en chargera, et ça aidera la personne qui relira votre code dans six mois (dans 90% des cas : vous) à comprendre ce que vous cherchiez à faire dans votre esprit malade.
La pureté du code n'est qu'une pauvre manœuvre de gatekeeping pour vous prouver que, oui mon chéri, maman t'aime fort car tu es très malin, bravo, tu gères des ternaires de huit lignes dans ta tête je suis fière de toi.
En faisant ce genre de choses vous ne faites que perpétuer les croyances selon lesquelles le développement est une pratique arcane réservée à une élite alors que vous êtes en train de développer le Wordpress de tonton Jean-Marc.
L'autre bonne nouvelle vis-à-vis de cette méthodologie tout bonnement novatrice est qu'elle s'applique en réalité à tous vos besoins de développement, non pas en rajoutant des divs autour de vos conteneurs Docker (votre devops vous cassera la gueule si vous faites ça) mais en apprenant simplement à accepter qu'un code explicite est plus lisible, maintenable et propre qu'un code malin.
Je parlais plus haut de ternaires de huit lignes (expérience véridique), et je vous pose la question : quand vous écrivez un truc pareil, à aucun moment vous ne vous dites "euh dis donc, je serais pas un peu en train de me compliquer la vie" ?
Le développement informatique n'est pas un concours de minimalisme, vous n'êtes pas payés à l'absence de lignes de code, vos solutions alambiquées en 30 caractères ne sont que des proofs of concept face à la réalité de l'industrie.
Combien j'en ai vu qui étaient capables de me sortir des RegExp de tête ou de claquer du bitwise à toutes les sauces et de les utiliser, parfois pertinemment ? Plein en fait, des gens très talentueux d'ailleurs.
Mais, les features compliquées du langage ont un sens, une raison d'être, et elles sont là pour vous, pas contre vous. Ainsi, vouloir à tout prix, à tout moment, utiliser les fonctionnalités les plus poussées pour accomplir les besoins les plus triviaux c'est simplement du fayotage à base de "regarde, j'ai bien appris ma leçon" (et on aime pas les fayots, relisez Le Petit Nicolas).
Je ne vous dis pas d'écrire du mauvais code (ni repris ni échangé ni remboursé je pars à Ibiza avec l'argent des abonnés) ni d'écrire le code le plus simple possible (parce que si vous faites ça ça redevient compliqué, faites pas votre app uniquement avec des if-statements, vous y êtes encore dans trente ans) :
Comprenez simplement vos outils et sachez lesquels vous maîtrisez.
Il sera peut-être parfois plus dur d'écrire du code ultra explicite mais croyez-moi bien quand je vous dis que vous serez content de pouvoir vous relire quand votre ternaire-dont-vous-êtes-super-fier-putain-j'ai-bossé pétera inévitablement au lieu d'être seul sur le sable, les yeux dans l'eau à essayer de vous remémorer votre mindset de l'époque en vous flagellant.
Certains me diront peut-être "oui mais c'est une bonne pratique de faire X ou Y" et je vous le dis, du code en prod vaut cent fois mieux que du code dans le manuel des Castors Juniors du web, même si ce dernier est "hyper propre" ou " approuvé par Douglas Crockford ".
Il n'y a de bonnes pratiques que celles qui vous permettent de travailler efficacement en équipe et avec vous-même .
La vérité c'est que le développement web peut être simple et propre, et que le code spaghetti est toujours du code spaghetti même quand vous le comprenez (parce que vous êtes le seul à le comprendre) et qu'il vous fait vous sentir malin.Si vous tenez tant que ça à faire du code illisible, allez donc faire de l'assembleur ou du brainfuck.
Je retourne me coucher.
Puisqu'on vient de sortir une refonte du site, c'est l'occasion de faire un tour sur le processus qui nous y a amené.
Avant ça, le site n'avait pas énormément évolué depuis quelques années. On a commencé à parler d'un redesign complet du site il y a un an et demi, on a maquetté la chose (c'est le design que vous voyez maintenant), mais on n'a rien foutu par manque de temps et par flemme de s'attaquer à un pareil chantier.
On s'est quand même finalement décidé à retaper le site en un weekend, parce qu'on emmerde les proverbes sur les cordonniers.
Alors comment on a fait ?
Comme tout projet, on accumule au gré des années. On avait à l'époque un script JS qui parsait l'historique Git et tapait l'API GitHub pour fournir des données qui se sont avérées être trop granulaires pour nos besoins (et qui foirait une fois sur deux dès qu'une nouvelle personne voulait contribuer au projet).
On ajoutait des petites features trop tôt, dès qu'une demande apparaissait dans les issues. Et ces features nous bloquaient pour en produire de nouvelles qui avaient plus d'intérêt pour la vie de l'organisation et du blog (notamment au vu des podcasts arrivés depuis 2016).
En prenant un peu de recul, on a réalisé que nos besoin se résumaient à trois points importants :
Quand on s'est lancé dans l'ouvrage de refaire le site, il y avait un impératif : préserver les contributions de nos auteurs.
D'un autre côté, partir d'un repository existant peut énormément limiter la création (alors que partir d'un projet tout frais tout neuf ça motive). On a du coup choisi une approche alternative: copier le projet, l'altérer à foison, et coller le résultat dans une énorme PR.
Partir d'un package.json
vierge était essentiel. On avait beaucoup trop de trucs.
On a choisir de partir sur ReasonML , parce que c'est une technologie dans laquelle on croit, que dans l'équipe on est tous familiers avec React et parce qu'avec son type system , elle nous apporte des avantages non négligeables sur un projet pouvant être édité par un grand nombre de personnes: c'est une sorte de garde-fou.
Pour le styling, si vous me suivez depuis longtemps, vous savez que j'adore CSS , je suis donc parti sur bs-css
, un DSL statiquement typé qui utilise emotion , ce qui nous permet d'envoyer au client uniquement les styles requis .
La question qu'on s'est posée : quel est le moyen le plus efficace de livrer une page de blog aux lecteurs ?
Et la réponse qui s'est imposée à nous était dans la veine de ce qu'on avait déjà fait par le passé : une SPA statique .
Qu'est-ce qu'une SPA statique ? C'est un site statique conçu comme une single page application . On conçoit le site comme une application React côté client, et on va pré-rendre les pages non pas côté serveur, mais dans notre système d'intégration continue.
Pourquoi de l'hébergement statique ? Parce que c'est moins cher (voire gratuit), qu'on est principalement front et que la dernière fois que je me suis connecté à la console AWS j'y suis resté coincé pendant 5 jours.
Ce que ça apporte : un chargement initial extrêmement rapide , une navigation optimale et des besoins en serveur réduits au minimum .
Quand un lecteur arrive sur le site, il récupère une version de la page déjà rendue au moment du build, le client ne fait que "hydrater" la page. Lors que le lecteur navigue vers une autre page, le client va uniquement chercher la donnée dont il a besoin pour la page de destination.
En développement local, chaque page démarre avec un data-store vide , et les composants vont demander le chargement des données . En production, chaque page est livrée rendue , avec le data-store contenant les informations dont elle a besoin , toute navigation ultérieure sera comme en mode local: les composants vont demander le chargement de ce qui leur manque.
Ce mode de fonctionnement requiert une certaine discipline concernant les endroits où la donnée peut être stockée. Dans notre cas, on a choisi de la placer dans l'état du composant qui orchestre l'application, et de le faire sous la forme suivante:
type state = { articles: Map.String.t(RequestStatus.t(Result.t(Post.t, Errors.t))), articleList: RequestStatus.t(Result.t(array(PostShallow.t), Errors.t)), podcasts: Map.String.t(RequestStatus.t(Result.t(Podcast.t, Errors.t))), podcastList: RequestStatus.t(Result.t(array(PodcastShallow.t), Errors.t)), home: RequestStatus.t(Result.t(Home.t, Errors.t)),};
Cet état peut stocker l'intégralité des données du blog , comme il peut stocker le minimum possible . Si vous voulez en savoir plus, n'hésitez pas à naviguer dans les sources du site.
Une approche alternative est de stocker la donnée requise par chaque URL, mais cette approche s'avère moins efficace au niveau du cache: si on a déjà toutes les données depuis une autre URL, on va quand même devoir les charger à nouveau.
Le blog était organisé d'une manière … chaotique. L'emplacement des fichiers markdown dans le repository définissait leur URL sur le site (on devait se dire que c'était une bonne idée à l'époque). Pour plus de perennité, on a choisi de passer à un système où le nom ou l'emplacement du fichier n'avaient pas d'impact sur l'endroit où se retrouve sur le site. Ça nous permet de les organiser par date, par sujet ou par contrib dans le futur sans pour autant influer sur les URLs.
Avec jojmaht , on a pris la tâche ingrate de bouger et transformer la centaine d'articles existants, avec leurs médias, vers une organisation à plat , plus simple à gérer.
Une fois cette tâche gérée, on n'avait plus qu'à récupérer les posts et à les générer aux URLs qui vont bien.
On a profité de ce chantier pour réécrire les URLs des articles vers des formats plus lisibles (e.g. /articles/js/react
-> /articles/introduction-a-react
).
Pour preserver les anciennes URLs, on a indiqué l'ancienne URL des articles dans chaque fichier markdown, et on a généré à ces endroits là une page HTML:
<!DOCTYPE html>
<metahttp-equiv="refresh"content="0;URL=NOUVELLE_URL" />
On se sert également de cette ancienne URL comme identifiant de page pour notre système de commentaires qui tourne avec disqus parce que j'ai pas trouvé la page pour migrer les URLs sur leur admin.
Si vous voulez en savoir un peu plus, n'hésitez pas à parcourir la source du projet . Et n'hésitez pas à contribuer au blog si l'envie vous vient.
Bisous.