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.
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.
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é.
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.
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).
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 :
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é.
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 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 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).
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.
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 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.
J'ai donc choisi PHP pour cette nouvelle version et cela m'amène à un second choix : Laravel ou Symfony.
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 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).
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.
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).
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.
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).
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 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 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.
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.
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.
Même si certaines classes sur le site peuvent faire penser à certains framework CSS, je n'en ai pas utilisé pour les raisons suivantes :
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).
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.
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.
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 ^^.
Vous savez maintenant tout sur les choix que j'ai pu faire pour le site alors voilà un petit résumé de la stack :