Fabriquer un design graphique, ce n’est souvent pas aussi simple que de prendre une bibliothèque sur étagère comme Bootstrap, Tailwind ou n’importe quel autre framework Front-End, d’autant plus qu’il est intéressant d’avoir rapidement un visuel à montrer. S’il porte les couleurs et transmet les valeurs du produit ou de la société qui porte ce design, c’est encore mieux.
On parle alors d’identité graphique. Pour illustrer, lorsque vous utilisez Bootstrap, vous consommez l’identité graphique de Twitter et lorsque vous utilisez Material, vous mettez des principes provenant de Google.
Nous allons sortir de ces solutions clés en main pour revenir à un style qui ne dépend d’aucun framework.
Parlons alors de votre propre style que vous allez pouvoir réaliser en suivant vos méthodologies de travail préférées. Je vous conseille l’approche Atomic Design lorsque vous souhaitez une forte cohérence graphique dans votre application. Pour que votre design soit documenté et organisé correctement, n’hésitez pas à créer votre propre Pattern Library (avec Tikui, PatternLab, à la main ou avec d’autres solutions si vous le souhaitez).
Revenons aux différentes façons de consommer votre style. Dans cette introduction, nous avons évoqué l’utilisation de frameworks jusqu’à votre propre création graphique, indépendante de ces frameworks. Cependant, un choix s’impose pour consommer votre style au sein de vos applications, faisons un point sur les différents moyens de déployer son identité graphique.
Dans votre application
Avec des frameworks comme Vue ou Angular, la magie de webpack vous permet d’utiliser votre style comme une simple dépendance node, et même de le déclarer directement dans vos composants. On parlera de scoped styles parfois pour que votre style n’impacte pas d’autres composants de votre framework préféré.
Je vous propose de voir plusieurs implémentations d’un style, venant directement des exemples des frameworks eux-même. Commençons par Vue :
<style scoped>
.example {
color: red;
}
</style>
<template>
<div class="example">hi</div>
</template>
Pour Angular, on aura :
@Component({
selector: 'app-root',
template: `
<h1>Tour of Heroes</h1>
<app-hero-main [hero]="hero"></app-hero-main>
`,
styles: ['h1 { font-weight: normal; }']
})
export class HeroAppComponent {
/* . . . */
}
Bien entendu, vous pouvez séparer, dans les deux cas, votre style CSS du reste de votre composant en le mettant dans un fichier à part.
L’avantage considérable de cette méthode, c’est que vous versionnez et déployez votre style directement avec votre application.
En revanche, votre couplage ne vous incite pas à réfléchir au style indépendamment de la mécanique de vos composants, ce qui peut créer plusieurs comportements :
- Du style pour chaque composants ;
- Un style commun à votre application ;
- Des composants avec de la valeur graphique à multiplier (bouton, menu déroulant, lien …).
Et même lorsque vous faites un style commun, si vous souhaitez qu’une autre application ait la même identité graphique, ça peut vite devenir un vrai casse-tête.
Via une Component Library
Ça y est, vous avez trouvé la solution. Extraire vos composants dans une bibliothèque séparée : ils seront consommés par toutes vos applications sur tous vos projets, à l’image d’Angular materialize et Vuetify.
Vous vous empressez de fabriquer votre propre Component Library, une bibliothèque avec tous vos composants à forte valeur graphique.
Tout se passe bien pendant quelques semaines jusqu’à ce que vous remarquiez qu’une équipe a déjà commencé une application sur Vue alors que tous les autres projets sont sur Angular.
Eux aussi souhaitent utiliser des composants graphiques, cependant ils ne peuvent pas facilement utiliser vos composants Angular au sein de leur application Vue.
Vous l’aurez vu, une Component Library fonctionne bien lorsque toutes vos applications utilisent le même framework que celui de vos composants. Cependant, le couplage de votre style graphique à votre Component Library ne permet pas de supporter un framework différent de celui que vous avez utilisé pour la construire.
Un autre effet de ce couplage peut venir de deux équipes différentes qui souhaitent en apparence un même composant mais qui n’ont pas exactement le même fonctionnement. Une des méthodes pour résoudre ce problème est d'adapter le composant pour qu’il prenne en compte les deux situations. Cela peut parfois en complexifier la réalisation mais aussi vous mettre devant des choix pour rendre plus cohérentes vos deux applications. .
Vous pouvez aussi avoir une désynchronisation des versions de votre Component Library au sein des applications. Ainsi, en fonction des mises à jours de vos dépendances, vous risquez que certaines équipes ne mettent pas à jour ses dépendances et qu’il soit difficile de le faire lorsque votre Component Library aura accumulé un grand nombre de changements bloquants.
Pour le déploiement, cela vous ajoute une brique en plus à publier dans un registry Node.js pour qu’elle soit consommée par toutes vos applications Angular (ou autre) à la manière d’un shared kernel, ce qui crée un fort couplage applicatif.
Avec une Pattern Library
Vous vous en doutiez si vous avez lu attentivement mon introduction ainsi que mes articles précédents, je vais vous parler de Pattern Library.
Bien que ce terme soit moins connu, il est souvent utilisé dans le domaine UX/UI.
Une Pattern Library est une collection d’éléments graphiques utilisée sur un site pour définir son visuel.
Concrètement, lorsque vous utilisez Bootstrap, sans sa partie JavaScript, vous consommez une Pattern Library, c’est pareil lorsque vous utilisez Tailwind ou tout autre framework centré sur le CSS.
Ce qui est intéressant avec ces outils, c’est qu’ils présentent une documentation pour être utilisés indépendamment de tout framework Front-End (Vue, React, Angular …). Ainsi vous découplez totalement l’identité visuelle de la mécanique de vos composants.
Même si votre Pattern Library peut, tout comme la Component Library, être consommée comme une dépendance Node.js, ce n’est pas ce que je vous conseille de faire. Un déploiement sur un serveur web (Nginx, Apache ou autre…) semble plus pertinent.
Laissez moi vous présenter, avec un exemple, la manière avec laquelle j’effectue mes déploiements pour que vous puissiez vous aussi l’essayer et vous faire votre propre idée.
Déploiement de sa Pattern Library
Pour déployer ma Pattern Library, je dois d’abord la créer. Je vous propose l’exemple d’un bouton que je crée à l’aide de Tikui, un outil qui m’aide à fabriquer ma Pattern Library. J’utilise Pug, un moteur de template pour fabriquer mon HTML qui fera office de contrat et SCSS pour fabriquer mon style CSS.
$example-button-color-background: #d50;
$example-button-color-text: #fff;
$example-button-hover-color-background: lighten($example-button-color-background, 5%);
$example-button-focus-color-border: darken($example-button-color-background, 5%);
$example-button-focus-color-background: lighten($example-button-color-background, 10%);
$example-button-focus-shadow: 0 0 5px $example-button-focus-color-background;
.example-button {
outline: none;
border: 1px solid $example-button-color-background;
border-radius: 3px;
background-color: $example-button-color-background;
cursor: pointer;
padding: 5px 20px;
text-transform: uppercase;
line-height: 20px;
color: $example-button-color-text;
font-size: 16px;
&:hover {
border-color: $example-button-hover-color-background;
background-color: $example-button-hover-color-background;
}
&:focus {
border-color: $example-button-focus-color-border;
box-shadow: $example-button-focus-shadow;
background-color: $example-button-focus-color-background;
}
}
button.example-button Button
Ensuite, je fabrique un Dockerfile avec une base Nginx :
FROM nginxinc/nginx-unprivileged
COPY ./nginx.default.d/. /etc/nginx/conf.d/.
COPY dist /usr/share/nginx/html
Et bien entendu la configuration associée :
server {
listen 8080;
server_name localhost;
root /usr/share/nginx/html;
location / {
index index.html index.htm;
}
location ~ \.(css|html)$ {
expires epoch;
}
location ~ \.(eot|otf|ttf|woff|woff2)$ {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
#
# Custom headers and headers various browsers *should* be OK with but aren't
#
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
#
# Tell client that this pre-flight info is valid for 20 days
#
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
if ($request_method = 'POST') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
}
if ($request_method = 'GET') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
}
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
location /healthcheck {
default_type 'text/plain';
return 200 'OK';
}
}
La configuration avec
expires epoch
me permet de redemander à chaque fois ma page HTML et CSS au serveur, ce qui me permet d’avoir un style évolutif. Par défaut Nginx gère les ETag comme je le souhaite, ce qui permet de ne renvoyer que les fichiers qui ont changé depuis la dernière mise en cache de mes styles dans le navigateur. Ainsi, à la première sollicitation, je reçois un 200 (OK) avec le contenu de mon fichier CSS que mon navigateur met en cache. Ensuite si je recontacte le serveur sans que mon fichier ait changé, il me renverra seulement un 304 (not modified) et non son contenu. Si le fichier change, l’ETag étant différent, le serveur me renverra une 200 avec le nouveau contenu du fichier.
Suite à ça, je publie mon image sur Docker Hub (à faire sur n’importe quel registry Docker).
Tout est maintenant prêt pour déployer mon image. Je choisis de la déployer dans un orchestrateur de conteneurs.
Vous pouvez vous aussi voir le résultat via
docker run -p 8080:8080 gnukde/deploy-pl
puis en vous rendant sur http://localhost:8080/atom/atom.html#button
Je peux maintenant consommer mon style depuis mon application Angular mais aussi Vue ou React., voici les exemples de code utilisant les boutons pour incrémenter ou décrémenter un score initial de 0.
Dans tous les cas, vous devez ajouter le lien vers le fichier CSS de la Pattern Library dans le fichier index.html
de votre solution préférée :
<!-- … -->
<head>
<!-- … -->
<link rel="stylesheet" href="http://localhost:8080/tikui.css" />
<!-- … -->
</head>
<!-- … -->
J’ai pris soin de mettre le lien local vers la Pattern Library, vous pouvez évidemment remplacer le lien vers http://localhost:8080 vers celui de votre choix.
Avec Angular :
Création d’un projet Angular à l’aide d’Angular CLI.
Module :
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { ExampleComponent } from './example.component';
@NgModule({
declarations: [
ExampleComponent
],
imports: [
BrowserModule
],
providers: [],
bootstrap: [ExampleComponent]
})
export class ExampleModule { }
Composant :
import { Component } from '@angular/core';
@Component({
selector: 'app-example',
templateUrl: './example.component.html'
})
export class ExampleComponent {
score = 0;
increment() {
this.score++;
}
decrement() {
this.score--;
}
}
Template :
<h1>Score: {{ score }}</h1>
<button class="example-button" data-selector="increment" (click)="increment()">
Add 1
</button>
<button class="example-button" data-selector="decrement" (click)="decrement()">
Remove 1
</button>
Avec Vue :
Un composant utilisant le bouton :
<template>
<div>
<h1>Score: {{ score }}</h1>
<button class="example-button" data-selector="increment" @click="increment">
Add 1
</button>
<button class="example-button" data-selector="decrement" @click="decrement">
Remove 1
</button>
</div>
</template>
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
@Component
export default class ExampleComponent extends Vue {
score = 0;
increment() {
this.score++;
}
decrement() {
this.score--;
}
}
</script>
Avec React :
Création d’un projet React à l’aide de create-react-app avec le template typescript
:
import React from 'react';
interface ExampleState {
score: number;
}
export default class Example extends React.Component<{}, ExampleState> {
constructor(props: {}) {
super(props);
this.state = {
score: 0,
}
}
get score(): number {
return this.state.score;
}
increment(): void {
this.setState(state => ({score: state.score + 1}));
}
decrement() {
this.setState(state => ({score: state.score - 1}));
}
render() {
return (
<div>
<h1>Score: {this.score}</h1>
<button className="example-button" data-selector="increment" onClick={() => this.increment()}>
Add 1
</button>
<button className="example-button" data-selector="decrement" onClick={() => this.decrement()}>
Remove 1
</button>
</div>
);
}
}
Pour récapituler à l’aide d’un schéma, voici ce qu’il se passe lorsque tout est déployé :
Et lorsque votre navigateur appelle une des trois applications front :
La stratégie d’utilisation de la Pattern Library correspond ainsi à la notion de Published Language telle que vous pouvez la retrouver dans le livre Domain-Driven Design (page 375) de Eric Evans. La Pattern Library utilise le langage HTML comme langage commun avec les applications qui la consomme.
Conclusion
Quand une volonté de capitaliser un style émerge, je retrouve souvent les deux premières stratégies : avoir le code CSS directement au sein de l’application ou utiliser une Component Library.
J’ai rarement vu des équipes choisir une Pattern Library malgré ses avantages qui me semblent moins contraignants qu’une Component Library. La Pattern Library est plus centrée sur une idée d’identité graphique étant donné que seuls les états sont représentés. Ainsi, la mécanique peut être implémentée à part, ce qui permet de s’affranchir de tout framework JavaScript (ou langage pour le web).
Depuis plusieurs années je déploie des Pattern Libraries, parfois en utilisant un conteneur avec un serveur web sur Docker, parfois en le publiant dans un CDN. Cependant, j’insiste sur son déploiement sur un serveur autonome plutôt que de l’intégrer directement dans l’application via une dépendance Node.js par exemple.
En effet,le cycle de vie de la Pattern library devient plus simple puisque les applications la consommant n’ont pas besoin de se mettre à jour pour profiter de la dernière version. Bien entendu, lorsqu’un composant évolue (structure ou nom de classe), vous devez le rendre rétro compatible le temps que tous les consommateurs aient implémenté votre nouvelle version.
J’espère vous avoir fait découvrir ou redécouvrir une autre façon de fabriquer une application Front End avec la notion de Pattern Library pour que vous puissiez mesurer son intérêt lorsque vous souhaitez fabriquer et déployer votre identité graphique.