Quelles technologies pour le site ?

Posté le 3 février 2021 - A Propos - Par Grafikart - Proposer une correction

Dans cet article je vous propose de partager avec vous les choix techniques que j'ai pu faire pour Grafikart.fr et les raisons derrière ces choix.

Architecture générale

Il existe aujourd'hui différentes techniques pour créer un site web et avant de commencer à parler de langage / technologies, il faut faire un choix sur l'approche à adopter.

SPA, Single Page Application

L'approche SPA est assez inadaptée pour Grafikart.fr car il existe de nombreuses pages d'entrées et beaucoup d'utilisateurs ne visitent finalement qu'une seule page. Aussi, une grosse partie du traffic vient des moteurs de recherche et le référencement ne peut pas être négligé.

Rendu Statique

Le principe du rendu statique est de rendre des fichiers HTML pour les différentes pages du site. Le rendu peut être fait automatiquement à partir de données provenant d'une API ou d'une base de données. Ce type de rendu aurait pu être possible sur certaines pages (comme les pages "tutoriels" par exemple) mais la fréquence des mises à jour, et le nombre de pages à générer, aurait rendu le processus trop complexe.

SSR, rendu côté serveur

Pour ce type de rendu, le serveur va générer dynamiquement les pages HTML en fonction de la requête utilisateur. Le serveur va pouvoir communiquer avec différents services privés (base de données, API internes & externes...), ou publics, afin de récupérer les données nécessaires à l'affichage de la page.

Le problème de ce type de rendu est que chaque page doit être rechargée totalement lorsque l'utilisateur clique sur un lien. Cela peut être problématique lorsque l'on souhaite faire persister des choses entres les pages (comme une connexion au WebSockets par exemple).

Hybride, SPA + SSR

Une dernière approche est possible en mélangeant le rendu côté serveur (ou statique) avec le rendu côté client d'une SPA. L'objectif est d'utiliser le même code côté client et côté serveur pour avoir un rendu isomorphique. Lors d'une requête utilisateur la page est générée et le rendu HTML est renvoyé par le serveur. Le JavaScript est ensuite chargé par dessus et vient remplacer la structure de la page (sans changement visuel pour l'utilisateur). Le reste de la navigation se fait alors côté client et permet d'avoir les bénéfices du rendu côté client (rapidité et fluidité) sans les inconvénients (chargement initial plus long et mauvais référencement).

Cependant, pour moi, cette approche possède quelques inconvénients :

  • L'utilisation d'une technologie commune entre le back-end et le front-end rend le système plus sensible aux changements (le front-end a tendance à évoluer plus rapidement que le back-end).
  • La complexité du projet est plus importante car il faut développer une API d'un côté et le rendu client de l'autre (cela peut aussi être vu comme un avantage car cela apporte une meilleure séparation du code et on peut maintenir les 2 parties séparément).

Mon choix : SSR

J'ai donc choisi de faire un rendu côté serveur classique car c'est un système avec lequel je suis à l'aise, mais aussi car le site n'a qu'un format possible (rendu HTML) et je n'ai donc pas forcément de bénéfice à concevoir une API. Le côté hybride, bien qu'intéréssant, ne m'a pas convaincu car impose une certaine technologie et les options disponibles manquent de flexibilité.

Quel langage pour le back-end ?

Maintenant que le choix est fait pour l'approche, il faut choisir le langage qui sera utilisé pour générer les pages côté serveur. Je n'ai pas choisi parmis l'ensemble des langages disponibles mais seulement parmis ceux que je connaissais (donc si vous ne voyez pas votre langage préféré dans la liste, ce n'est pas parce que c'est un mauvais langage).

Golang

Golang est un langage intéréssant car il peut être compilé pour fonctionner sur différents systèmes, il possède un système de typage bien structuré et possède une librairie standard importante. En revanche, c'est un langage qui est assez technique et qui, à mon sens, est plus interéssant pour des systèmes plus bas niveau qu'un simple site interne avec du rendu HTML.

Elixir (avec phoenix)

Elixir est un langage fonctionnel dynamique basé sur la machine virtuelle Erlang. Il dispose aussi d'un framework web, Phoenix, qui est très intéréssant avec un système de "Live View" qui permet de pousser des mises à jour depuis le serveur vers le client.

Malheureusemnt, l'écosystème est encore relativement jeune (pour la partie Elixir) et il faut aussi rapidement mettre les doigts sur le système sur lequel repose le langage (comprendre le langage Erlang et sa machine virtuelle).

Ruby (avec Ruby on Rails)

La version précédente du site avait été développée en utilisant Ruby et le framework Ruby on Rails (j'étais un peu lassé par PHP à l'époque et je voulais voir si l'herbe était plus verte ailleurs). Avec le temps, et la pratique, le côté dynamique du langage s'est avéré problématique.

Par exemple, il est possible en Ruby de déclarer plusieurs fois la même classe, ce qui a pour effet de rajouter des méthodes.

class Integer

    # On rajoute une méthode double à tous les entiers
    def double
        self * 2
    end

end

puts 3.double # 6

Cette approche m'avait séduite à l'origine mais à l'usage il devient assez rapidement difficile de connaitres les méthodes qui sont disponibles sur un objet particulier et cela ne me correspond plus (des outils comme Sorbet peuvent cependant mitiger le problème.)

En dehors de cet aspect là (qui est une préférence personnelle) le langage reste très agréable et je perds définitivement en confort d'écriture en choisissant un autre langage.

NodeJS

Le choix de NodeJS ne m'attirait pas trop car je n'ai pas forcément une très grande affinité avec le langage JavaScript. Au delà de ça, la technologie subit de nombreuses mutations et je ne trouve pas que cela en fasse une base solide pour une application qui dois durer dans le temps.

L'écosystème évolue aussi très rapidement et je trouve que l'exploration du code source des librairies est rendu trop difficile par la multitude d'outils qui servent à générer le code (le code source en sortie est difficile à explorer et à modifier via npm link ).

PHP

PHP est le langage avec lequel j'ai le plus d'expérience mais que j'avais un peu abandonné avant la sortie de la version 7. Depuis, l'apparition du typage (même si il est plutôt pauvre comparé à d'autres langages) m'a reconcilié avec le langage car cela apporte plus de structure et améliore la lisibilité du code. Cette nouvelle fonctionnalité a d'ailleurs été adoptée rapidement par la communauté (la plupart des méthodes des librairies tiers sont typées et il est facile de prévoir les méthodes disponibles et les types de retours).

Le langage est aussi bien installé et dispose de frameworks monolithiques stables qui correspondent à mon besoin de génération de pages HTML.

Quel framework ?

J'ai donc choisi PHP pour cette nouvelle version et cela m'amène à un second choix : Laravel ou Symfony.

Laravel

Laravel est un framework que j'apprécie pour les mêmes raisons que Ruby on Rails mais son côté dynamique rend plus difficile l'analyse du code. Il n'est pas possible par exemple de connaitre la forme d'un objet provenant de la base de données.

class Post extends Eloquent
{
    // ...
}

$post = Post::find(1);
$post->id; // 3

Laravel repose sur des méthodes magiques pour accéder aux données, ce qui est beaucoup moins explicite que des propriétés définies dans le code directement.

class Post
{

    public int $id;

}

Aussi, le framework évolue assez vite avec des breaking changes assez fréquents (sans forcément une phase de dépréciation) ce qui peut rendre la maintenance assez complexe (un changement de numéro majeur tous les 6 mois).

Symfony

Symfony est à l'opposé, avec une approche basée sur la configuration, et a tendance à être très verbeux (voici un exemple de model). Cette verbosité permet cependant une meilleure transparence du fonctionnement des différentes classes qui composent notre application.

En revanche, les composant Symfony sont moins uniformisés que ceux de Laravel et leur configuration peut s'avérer assez difficile car il n'est pas forcément évident de connaitre l'ensemble des options disponibles et l'effet que cela peut avoir sur le fonctionnement du bundle (on sera rapidement obligé de fouiller dans le code source souvent peu commenté pour comprendre le rôle d'une option).

Laravel ou Symfony ?

Finalement, j'ai fait le choix de la stabilité pour ce projet et, vu que je voulais mettre en place une organisation spécifique, je me suis dit que Symfony serait plus adapté (à voir avec le temps si ce choix a été le bon ^^). Vous pouvez retrouver la discussion qui a mené à ce choix sur les issues GitHub.

La base de données

L'ancien site fonctionnait avec une base MySQL et une base Neo4J. Le choix de Neo4J avait été fait pour me permettre de mieux définir la relation entre un cours et une technologie. L'idée était à terme de pouvoir créer un système de recommandation intelligent basé sur les vidéos marquées comme lues par les utilisateurs. Cependant, à l'usage, le système n'a pas réellement fonctionné et j'ai préféré m'orienter vers un système plus manuel, avec des cours organisés sous forme de cursus (rien ne vaut la curation humaine).

Pas de NoSQL pour la base principale

Comme pour le choix des langages, l'absence de rigueur me mène systématiquement à des problèmes d'organisation. Les bases de données relationnelles me forcent à penser ma structure en amont et sont souvent moins spécialisées dans le type de données qu'elles peuvent gérer. En revanche, cela ne m'empèche pas d'avoir recours à Redis pour le stockage de certaines informations comme les sessions utilisateurs, le cache et les notifications instantanées.

Pourquoi PostgreSQL ?

Sur mes projets, je me retrouve un peu perdu sur les différents moteurs de stockage de MySQL et MariaDB, et suivre les évolutions de ces 2 bases est devenu un peu trop complexe à mon goût. Aussi, je me suis dit qu'avec PostgreSQL il serait plus simple de prévoir les fonctionnalités qui sont disponibles sur le système (seul le numéro de version compte et un seul système de stockage). Je voulais aussi utiliser PostgreSQL pour gérer la partie recherche du forum et les types de recherche semblaient plus adaptés à ma problématique (gestion du langage et des poids à appliquer aux différentes colonnes).

La recherche

Pour la recherche, j'utilisais ElasticSearch sur l'ancien site, mais sa complexité et mon incapacité à le configurer correctement, a fait que la recherche n'était pas forcément pertinente. Aussi, pour cette nouvelle version j'ai décidé d'essayer d'autres outils.

PostgreSQL

PostgreSQL est doté d'un système de recherche full text qui peut être utilisé pour effectuer une recherche avec un classement par pertinence.

Voila par exemple ce que j'utilise pour indexer les sujets sur le forum.

ALTER TABLE forum_topic ADD search_vector tsvector DEFAULT NULL;
CREATE INDEX search_idx ON forum_topic USING GIN(search_vector);
-- Quand un topic est mis à jour, on met à jour le champs full text
-- La fonction
CREATE FUNCTION update_forum_document() RETURNS trigger AS $$
begin
    new.search_vector :=
    setweight(to_tsvector('french', coalesce(new.name, '')), 'A')
    || setweight(to_tsvector('french', coalesce(new.content, '')), 'B');
    return new;
end
$$ LANGUAGE plpgsql;
-- Le trigger
CREATE TRIGGER update_forum_document_trigger 
BEFORE INSERT OR UPDATE ON forum_topic FOR EACH ROW EXECUTE PROCEDURE update_forum_document()
-- On met à jour les données initiale
UPDATE forum_topic 
SET search_vector = setweight(to_tsvector('french', name), 'A') || setweight(to_tsvector('french', content), 'B')
WHERE 1 = 1;

Concrètement, cette approche fonctionne mais je trouve cela assez verbeux et complexe à mettre en place (je ne suis pas très à l'aise avec les fonctions et les triggers). L'autre problème, par rapport à ElasticSearch, est que le poids des champs doit être défini en amont (à l'insertion) plutôt qu'au moment de la recherche.

Typesense

Typesense est un outil, plus simple, avec lequel il est possible d'intéragir avec l'index au travers d'une API HTTP. Il est beaucoup plus limité en terme de fonctionnalité mais correspond bien à mon cas d'utilisation.

// On crée l'index en HTTP (POST)
$this->client->post('collections', [
    'name' => 'content',
    'fields' => [
        ['name' => 'title', 'type' => 'string'],
        ['name' => 'content', 'type' => 'string'],
        ['name' => 'category', 'type' => 'string[]'],
        ['name' => 'type', 'type' => 'string', 'facet' => true],
        ['name' => 'created_at', 'type' => 'int32'],
        ['name' => 'url', 'type' => 'string'],
    ],
    'default_sorting_field' => 'created_at',
]);

// On peut ensuite indexer un nouveau contenu
$this->client->post('collections/content/documents', $data);
// Et ensuite chercher
$this->client->get('collections/content/documents/search?q=php&per_page=10&query_by=title,category,content')

Le système fonctionne bien et est surtout extrèmement rapide. En revanche, il donne parfois des résultats un peu trop larges à cause de sa correction des erreurs typographiques. Par exemple une recherche "event" va faire ressortir "revenir" ou "évènement" avant d'autres résultats qui contiendraient event directement.

Les notifications instantanées

Pour le système de notifications instantanées sur le site j'ai choisi d'utiliser le protocole Mercure. J'ai eu l'occasion de l'utiliser à plusieurs reprises et je trouve son fonctionnement relativement simple et il s'adapte avec de nombreuses solutions grâce à son API HTTP.

Le front-end

Maintenant que l'on a fait le tour des technologies qui permettent de faire fonctionner les pages côté serveur, nous allons voir les technologies qui sont utilisées côté front-end.

CSS maison

Même si certaines classes sur le site peuvent faire penser à certains framework CSS, je n'en ai pas utilisé pour les raisons suivantes :

  • Les framework comme bootstrap offrent une collection d'élément pre-stylisés pour construire rapidement un prototype d'application. Dans mon cas, j'ai fait une maquette en amont et il aurait été contreproductif d'utiliser un framework de ce type pour ensuite devoir écraser les styles proposés par défauts.
  • Les frameworks utilitaires comme TailwindCSS offrent des milliers de classes qui doivent être combinées pour styliser les éléments. Personnellement, je n'aime pas cette approche car elle rend le code HTML peu lisible et il faut derrière utiliser des outils comme purgeCSS pour ne garder que le CSS réellement utilisé.
  • Je fais du CSS depuis longtemps et je sais comment organiser mon code pour arriver à m'y retrouver convenablement. La cascade ne me fait pas peur ^^

L'approche utilisée par le site est un mix entre de l'utilitaire (en regroupant pour ne pas avoir 10 classes par éléments et en utilisant des variables CSS pour gérer les variantes) et du sémantique. Vous pouvez d'ailleurs avoir un aperçu des classes utilitaires sur cette page).

CustomElements & Preact

Le serveur est responsable de renvoyer la majorité du code HTML mais certains éléments ont besoin d'être dynamiques et il est nécessaire dans ce cas d'avoir recours à du JavaScript. La principale problématique est alors de savoir comment rattacher le comportement à un élément tout en étant capable de le faire pour les nouveaux éléments entrant (dans le cas d'un ajout suite à une requête AJAX par exemple).

Par rapport à cette problématique j'ai décidé d'utiliser les custom-elements qui permettent de déclarer des éléments HTML personnalisés. J'utilise ces composants web pour créer des éléments interactifs au sein d'une page générée par le serveur.

<youtube-player
    class="course__player"
    id="course-1201"
    video="lKm22TA5pzw"
    poster="image.jpg"
    duration="33 min"
    class="shadow"
>
    <a href="https://www.youtube.com/watch?v=lKm22TA5pzw" target="_blank" rel="noopener" class="course__placeholder">
        <span>Voir la vidéo</span>
        <img src="image.jpg" width="1330" height="750" />
    </a>
</youtube-player>

Cette approche permet aussi d'afficher des éléments par défaut pendant que le JavaScript se charge et d'éviter au maximum les changements de structure de la page.

Pour créer certains de ces composants j'ai eu recours à Preact. Cela me permet de simplifier la logique et d'éviter un trop grand nombre de manipulations au niveau du DOM. J'ai fait le choix de Preact car il est léger et utilise la syntaxe jsx qui est largement supportée.

Enfin, pour la navigation j'utilise Turbolinks qui va transformer tous les liens du site en lien Ajax. Cela me permet de maintenir la connexion au serveur de notification pendant la navigation de l'utilisateur mais aussi de ne pas avoir à rééxécuter toute l'étape d'initialisation du JavaScript à chaque page.

L'hébergement

Enfin, le dernier choix à faire se situe au niveau de l'hébergement : Web service ou Serveur dédié ?

Dès le début, j'ai écarté la solution des web services en raison d'un coût important de fonctionnement (notamment pour la partie diffusion de vidéo) mais aussi par peur d'être dépendant d'un service particulier. En effet, l'un des problèmes que j'ai avec beaucoup de solutions cloud actuelles est que chaque fournisseur dispose de ses propres produits (venant chacun avec une terminologie bien spécifique) qui nécessitent un apprentissage préalable important. Et les connaissances acquises sur le fonctionnement d'un service ne sont pas forcément transférable vers un autre système.

Aussi, la taille du projet fait qu'il est possible de le gérer assez facilement sur une seule machine et j'ai préféré choisir une machine sur laquelle j'ai le contrôle sur la configuration.

Un serveur virtuel cloud

J'ai donc choisi d'héberger cette nouvelle version sur un serveur virtuel. L'objectif étant de pouvoir faire évoluer la configuration en fonction des besoins, en terme de RAM et de stockage.

Pour l'hébergeur, j'ai hésité parmis 2 hébergeurs que je connais bien : Scaleway (chez qui j'utilise des intances virtuelles pendant le développement et de l'object storage) et Infomaniak (que j'utilise pour héberger certains WordPress et pour gérer mon nom de domaine). J'ai finalement tranché pour Infomaniak car ils proposaient une tarification en amont plus prévisible.

Après les avoir contacté, ils ont accepté de sponsoriser cette nouvelle version en m'offrant son hébergement ^^.

Pour résumer

Vous savez maintenant tout sur les choix que j'ai pu faire pour le site alors voilà un petit résumé de la stack :

  • Serveur dédié virtuel avec Debian,
  • Nginx pour le serveur web,
  • PHP avec Symfony pour le BackEnd,
  • PostgreSQL pour la base de données,
  • Redis pour le cache, les sessions et le système de fil d'attente,
  • Mercure pour les notifications instantanées,
  • Messenger pour les tâches asynchrones,
  • Typesense pour la recherche,
  • NodeJS pour le bot Discord.