Le logiciel projet_a est l'outil de base du projet de sites et de communauté virtuelle d'artistes. Sa réalisation en mode « agile » permet de faire aussi progresser le reste du projet pendant son avancement.

Articles

1. La structure des données

Une base de données enregistre la structure des sites des artistes et celui du projet A ainsi que les commentaires échangés ou publiés. Les images et le code des générateurs sont stockés à part.

2. Interpolation dans les chaines et textes

Comment contrôler le format des chaines de caractères non HTML et intégrer des méta-données dans les textes HTML.

3. Les composants graphiques

Les composants graphiques sont les constituants des générateurs de pages qui leur permettent d'afficher des groupes d'images, mais aussi d'assurer la gestion de leur disposition.

4. Genèse d'un composant

Cet article est une description didactique, phase par phase, de l'écriture du composant p4008_google_search pris comme exemple. Par la même occasion, il permet de revoir quelques bases de la programmation Ruby et HAML ainsi que du passage de paramètres dans le logiciel projet_a.

5. Gestion du cache

Utilité d'un cache. Caractéristiques. Difficultés. Solutions Rails. Cas du projet A : problème et solution.

Démarche du développement

Le développement du projet A est au départ une démarche solitaire, ce qui permet de structurer rapidement le cœur du projet et de fournir en quelques mois une première version opérationnelle pour tester les principaux concepts.

Toutes les compétences d'un projet Web dont le volet graphique est important sont nécessaires. Toute personne intéressée pour participer au projet peut se faire connaitre à la boite Contact : développement Rails, Javascript, CSS, SQL, Linux ou graphisme Web.

Le développement du logiciel s'intègre dans le projet A plus global de communauté virtuelle d'artistes. Il est effectué suivant une méthode « agile » : on développe quelques fonctionnalités ; on fabrique une maquette ; on vérifie auprès des utilisateurs ; on fait quelques retouches ; on ajoute de nouvelles fonctionnalités ; et ainsi, pas à pas, en voyant et en faisant voir littéralement le projet se construire sous toutes ses facettes.

Avancement de la réalisation

  • Version 0.1 (2011-05) : tables de base, génération des premières pages
  • Version 0.2 (2011-07) : ajout des commentaires, composants graphiques
  • Version 0.3 (2011-09) : site communautaire, composants en base
  • Version 0.4 (2011-10) : sites en plusieurs langues
  • Version 0.5 (2011-12) : relation utilisateurs - sites → abonnements, membres, droits multiples
  • Version 0.6 (2012-01) : moules d'aide à la saisie pour guider les webmestres
  • Version 0.7 (2012-02) : objets joints aux œuvres, accès aux informations, profil utilisateur en fonction du site → sites d'association
  • Version 0.8 (2012-09) : groupes d'utilisateurs, périodes de publication des articles et des œuvres
  • Version 0.9 (2014-01, en cours) : gestion unifiée des ingrédients

Le numéro de la version en ligne figure dans le bas de page de l'accueil du site communautaire et de tous les écrans en mode gestion.

Langage et composants

  • Langage : Ruby 2.1.3
  • Plateforme : Ruby on Rails 3.2.21
  • Composants Ruby additionnels :
    • haml (langage des vues)
    • dalli (cache)
    • Kaminari (pagination)
    • RMagick (traitement des images)
    • exception_notification (notification des erreurs par messagerie)
    • byebug (développement)
    • unicorn (interface avec le serveur web nginx)
  • Composants Javascript :
    • JQuery, JQuery-UI,
    • Tiny-MCE (éditeur de texte)
    • JPlayer (son)
    • Treeview (arbre)
    • Fullscreenr (image en plein écran)
  • Base de données : PostgreSQL
  • Composants Linux
    • memcached (serveur de cache)
    • nginx (serveur web)
    • postfix (serveur de messagerie)
    • ImageMagick (traitement des images)
    • Pandoc (conversion Latex → HTML)

Écrans de gestion

Les écrans de gestion du logiciel sont réservés aux gestionnaires de site et aux administrateurs. Ils donnent une vision très concrète du produit.

Voir l'article Copie des écrans de gestion.

Articles en projet

  • Gestion du cache
  • Les composants
  • Recettes de développement

Développement du logiciel

1. La structure des données

Base de données et fichiers divers

Une base de données enregistre la structure des sites des artistes et celui du projet A ainsi que les commentaires échangés ou publiés.
Les images et le code des générateurs sont stockés à part.

Aio projeta bdd schema 1.0 140117

1. La base de données

→ Voir aussi le document PDF détaillé aio-projeta-bdd-0.9.0-140215.pdf (270 ko).

1.1. Cloisonnement des sites

La base de données de l'application est unique ; elle enregistre les informations de tous les sites, hors fichiers externes (décrits au chapitre 2).

Pour garantir un bon niveau de sécurité, les données sont cloisonnées entre les sites : un objet en base ne peut être en relation qu'avec des objets du même site. Des contrôles d'intégrité sont effectués systématiquement dans les modèles au moment de l'enregistrement et dans les contrôleurs, à partir de l'attribut site_id de chaque objet.

Cette règle n'admet que les exceptions suivantes :

  • les utilisateurs (table 'users') sont communs à tous les sites, mais chacun est rattaché à un site ;
  • les composants sont des générateurs (table 'generators') communs ;
  • les collages (table 'stickers') peuvent reliés un espace d'un site à une œuvre d'un autre site en cas d'emprunt entre sites autorisé.

1.2. La description des sites et de leur contenu

La table 'sites' déclare tous les sites Web du projet, y compris le site communautaire, chaque site disposant de sa propre adresse internet (sous-domaine ou domaine spécifique).

Pour chacun d'eux, la table 'spaces' en construit la structure avec un arbre d'espaces de publication : expositions, actualités, etc. Certains espaces ont un usage spécial, soit visible (page d'intro, page d'accueil, page d'actualités), soit invisible (espaces de ressources et de réserve).

Dans chaque espace, les collages (stickers) accrochent des œuvres (works). Ceci permet de référencer plusieurs fois une même œuvre dans plusieurs espaces et si besoin de les déplacer facilement. Une œuvre peut avoir plusieurs œuvres filles pour compléter sa description par celle de ses parties, par exemple une suite musicale et chacun de ses chants.

Au départ, une œuvre décrit complètement une réalisation artistique avec trois images différentes et un texte associé. Cette notion est également utilisée pour tout contenu volumineux : articles, textes de liaison, ressources graphiques (logos, signatures, etc.) ou sonores, voire des documents PDF ou autres. Les objets joints (attachments) à une œuvre suppriment ces limites.

La version 0.9 en cours de développement considère progressivement tous les textes, images, documents et autres fichiers comme des ingrédients (ingredients) et référence leurs utilisations (ingredient_uses). À la fin de cette migration, la notion d'objets joints sera supprimée ainsi que le référencement direct de fichiers par une œuvre.

1.3. La génération des pages

Les générateurs (generators) de page sont utilisés en deux temps : pendant la phase de définition d'un site, ils produisent le code qui sera ensuite lors de sa consultation par les internautes.

Chaque site a un utilisateur titulaire et un arbre d'espaces d'enregistrement, décrivant son contenu, ainsi qu'un espace de ressources communes (logo, fond d'écran, etc.) et un pour stocker les éléments mis en réserve avant leur publication.

Les générateurs sont de quatre types différents :

  1. les vues (views) sont les seules à produire effectivement les pages présentées aux internautes ; pour ce faire, les vues font appel aux générateurs des autres types  ;
  2. les gabarits (layouts) sont des modèles de pages comportant la structure et les parties fixes ou presque ;
  3. les partiels (partials) produisent des morceaux de pages : menus, articles, présentation graphique, etc. ; ils peuvent être réutilisés dans les différents générateurs d'un site ;
  4. les composants (components) sont des partiels prédéfinis, utilisables pour la fabrication de tous les sites.

La composition (compositions) d'un générateur enregistre les autres générateurs qu'il utilise. Exemple : une vue appelle presque toujours des composants et parfois des partiels.

Enfin, tous les textes peuvent être écrits dans plusieurs langues pour permettre alors aux générateurs de produire une page dans sa version originale ou dans sa traduction la plus adaptée en fonction de la demande de l'internaute ou du paramétrage de son navigateur.

L'interactivité des utilisateurs

Les abonnements (subscriptions) définissent la relation entre les utilisateurs et les sites. Ils permettent de définir les abonnements aux informations sur un site, le profil de membre de l'association liée au site et tous les droits des gestionnaires et auteurs. Des envois ciblés de messages pourront ainsi être effectués à partir de ces données ainsi que la génération de pages présentant les membres d'une association... Les groupes (groups) déterminent les différents types d'abonnements de chaque site.

Les utilisateurs déclarés ou non peuvent publier des commentaires (comments) sur une page d'un site qui a choisi d'offrir cette fonctionnalité. Le gestionnaire du site peut les publier ou non et répondre à leurs rédacteurs. Les commentaires peuvent aussi servir pour des échanges de messages entre les membres inscrits du projet A.

2. Les fichiers hors base de données

En dehors de la base de données, l'application enregistrent des informations dans des fichiers indépendants : les images ; les documents ; le code des générateurs ; les moules de saisie ; les journaux.

Jusqu'à trois images peuvent être définies dans une œuvre ; c'est en général la même photo avec des dimensions différentes. Un document au format PDF ou autres peut être enregistré à la place d'une image. Les objets joints (attachments) à une œuvre permettent en outre chacun l'enregistrement structuré et décrit d'un fichier quelconque : fichier son ou autre.

Le code des générateurs est lisible en mode conception sur l'application ; il doit encore être écrit et installé en dehors de l'application. Dans une version ultérieure, il sera généré automatiquement à partir de caractéristiques fonctionnelles que le concepteur aura enregistré dans la base de données.

Associés aux générateurs, les moules, facultatifs, définissent dans chaque cas les possibilités de saisie par les webmestres et les aides dans ce travail : sélection des champs ; valeurs par défaut ; interactions autorisées ; explications.

Les feuilles de styles au format CSS fixent la présentation des pages HTML générées par l'application pour être servies aux internautes.

Les journaux tracent l'activité de l'application pour tracer les incidents et produire des statistiques.

version 0.9.0-0375-140204

Développement du logiciel

2. Interpolation dans les chaines et textes

Comment contrôler le format des chaines de caractères non HTML et intégrer des méta-données dans les textes HTML.

L'introduction des formules mathématiques en Latex dans les textes des pages HTML a occasionné quelques conflits de codification de la syntaxe (version 0.8.15 du logiciel projet_a). Les règles ont été consolidées pour réduire à l'avenir de nouvelles difficultés.

1. Règles communes

1.1. Syntaxe générale

La plupart des commandes d'interpolation se placent dans le texte entre accolades : {k   k} où k est un caractère distinctif de chaque commande.

1.2. Échappement

{/ affiche une accolade ouvrante.

1.3. Langue

{|lg|} indique que le texte qui suit est dans la langue lg.

{|**|} indique que le texte qui suit est pour toutes les langues non précédemment définies.

Ex. : Bonsoir !{|de|}Guten Abend!{|en|}Good evening!

1.4. Visibilité

{:x:} indique que le texte prend la visibilité x.

Voir l'article Visibilité des informations.

2. Interpolation dans les chaines de caractères

2.1. Texte en gras

Placer le texte à afficher en caractères gras entre deux étoiles ( * ) :

Ex. : Ceci est un *texte en gras* dans une phrase.
   →  Ceci est un <strong>texte en gras</strong> dans une phrase.
   →  Ceci est un texte en gras dans une phrase.

2.2. Texte en italiques

Placer le texte à afficher en caractères italiques entre deux tirets bas ( _ ).

Ex. : Ceci est un _texte en italiques_ dans une phrase.
   →  Ceci est un <em>texte en italiques</em> dans une phrase.
   →  Ceci est un texte en italiques dans une phrase.

2.3. Espace insécable

Le caractère tilde ( ~ ) génère une espace insécable.

Ex. : En date du~12~octobre
   →  En date du&nbsp12&nbsp;octobre
   →  En date du 12 octobre

2.4. Exposant

Le caractère chapeau ( ^ ) place les caractères qui suivent en exposant.

Ex. : Le 1^er octobre
   →  Le 1<sup>er</sup> octobre
   →  Le 1er octobre

2.5. Lien automatique

La mention d'une URL absolue, commençant par http:// ou par https://, génère automatiquement un lien vers l'URL mentionnée.

Ex. : Voir l'article http://dominique-moreau.fr/fr/p/3/10#w30
   →  Voir l'article <a href="http://dominique-moreau.fr/fr/p/3/10#w30">http://dominique-moreau.fr/fr/p/3/10#w30</a>
   →  Voir l'article http://dominique-moreau.fr/fr/p/3/10#w30

2.6. Lien explicite

{@texte->url@} génère un lien sur texte vers url.

Ex. : Voir {@La Galerie->/fr/p/3/10#w30@} à Hossegor
   →  Voir <a href="/fr/p/3/10#w30">La Galerie</a> à Hossegor
   →  Voir La Galerie à Hossegor

3. Interpolation dans les textes

Voir l'article Références dans les textes.

3.1. Référencement d'un fichier

{%type:reference%} génère l'information associée au type (file, name ou size) vers le fichier interne reference.

Ex. : {%name:a1%} ({%size:a1%})
   →  {%name:a1=projetA-Ecrans-0.8.12-130708.pdf%} ({%size:a1=3.04 MB%})
   →  projetA-Ecrans-0.8.12-130708.pdf (3.04 MB)

3.2. Référencement d'une image

{%img:reference%} dans l'attribut alt d'une balise <img> génère dans l'attribut src l'URL relative vers l'image interne reference.

3.3. Numérotation des chapitres

{#...:titre#} génère une numérotation du chapitre depuis le début du texte.

Le nombre de dièses en début de la séquence indique le niveau de la numérotation. Il n'est pas corrélé au type de la balise h1, h2, h3... ou autre (p, div...) dans laquelle elle est placée.

Ex. : {#.#. :Numérotation des chapitres#}
   →  {#.#. :<a name='chapter_3_3'></a>3.3. :Num&eacute;rotation des chapitres#}
   →  3.3. Numérotation des chapitres

version 0.8.15-0343-131012

Développement du logiciel

3. Les composants graphiques

Comment afficher artistiquement des images

Les composants graphiques sont les constituants des générateurs de pages qui leur permettent d'afficher des groupes d'images, mais aussi d'assurer la gestion de leur disposition.

1. Les composants graphiques de base

Dans la rubrique Notions de base, l'article Génération des pages explique comment les générateurs utilisent des composants pour produire les pages affichées aux internautes. Très souvent d'ailleurs, le code du générateur se limite à l'appel d'un composant, avec ou sans paramètres d'adaptation, englobé dans un bloc <div> avec un identifiant qui sert de cible aux instructions de la feuille de style CSS.

On rappelle que, d'après le modèle de données, les images à afficher sur un espace (space) sont celles des œuvres (works) visibles qui lui sont rattachées par des collages (stickers). Par visibles, on entend les images dont le niveau de visibilité est compatible avec les droits d'accès de l'utilisateur et aussi dont la période de publication du collage est en cours.

On dénommera mur d'affichage ou tout simplement mur la zone de la page sur laquelle seront placées les images, le plus souvent entre le haut et le bas de page.

Comme pour tous les composants, les données à afficher sont obtenues à partir des variables de classe standard fournies par le contrôleur See (@site, @space...) grâce à des méthodes d'extraction des classes de modèle assossiées comme Space.visible_works_for_purpose, etc. L'adaptation du composant est assurée soit par des paramètres d'appel Ruby, soit par des valeurs de l'attribut spaces.parameters transmis par le contrôleur au moyen de la variable @gen_params (paramètres de génération).

Les composants destinés à afficher les œuvres des artistes peuvent être répartis en plusieurs catégories : les composants unitaires, les composants génériques et les composants positionneurs.

1.1. Les composants unitaires

Un composant unitaire affiche une seule image d'une œuvre. Un composant de ce type est simple de conception et d'utilisation, la mise en page étant essentiellement assurée par la feuille de style CSS.

1.2. Les composants génériques

Un composant générique affiche de façon répétitive un lot d'images. Le positionnement des images est calculé à partir de l'ordre des collages et des caractérisques des images.

1.3. Les composants positionneurs

Un composant positionneur permet la mise en place des images par le webmestre sur le mur d'affichage de l'espace considéré. Le composant doit donc avoir deux modes de fonctionnement : positionnement des images par le webmestre lors de la phase de composition, puis affichage suivant le positionnement demandé lors de l'affichage aux internautes.

L'information de positionnement de chaque image est enregistré dans l'attribut stickers.place du collage correspondant, dans un format propre à chaque composant. Si nécessaire, la hauteur du mur est stockée dans l'attribut spaces.display de l'espace.

1.4. Fonctionnement du composant p5003_free

Le composant p5003_free, ou brièvement le composant free, est le positionneur le plus universel et le plus sophistiqué.

En mettant à vrai l'attribut positionable d'un générateur vue d'espace (space_view), on active le bouton Positionner sur la page de gestion des espaces qui le référencent comme générateur principal. Ce bouton permet l'affichage de la page générée correspondant à l'espace avec comme paramètre ?position=true (en fin d'adresse internet de la page). Ce paramètre déclenche le mode Position du code JQuery embarqué par le composant.

Ce mode affiche une barre de commande et encadre chaque image et chaque texte du mur d'affichage. La bordure de ces cadres peut être étirée pour redimensionner l'image ou la zone de texte (en conservant les proportions initiales pour les images). Cliquer puis tirer sur un cadre le déplace sur le mur, et même au delà. Comme les cadres sont superposables, double-cliquer sur l'un d'eux le passe au premier plan.

Une ligne horizontale trace la limite inférieure du mur dans la page ; la déplacer vers le bas ou vers le haut augmente ou diminue la hauteur du mur.

Une fois le positionnement des objets sur le mur effectué, le bouton Enregistrer de la barre de commande sauvegarde dans l'attribut stickers.place des collages correspondants et dans spaces.display la position et les dimensions des objets ainsi que la hauteur du mur. Le bouton Espace de la barre renvoie vers la page de gestion de l'espace. Si le moule de l'espace l'autorise, le bouton de modification d'un collage permet d'ajuster au pixel le placement de l'objet associé.

L'enregistrement du placement modifie le numéro d'ordre des collages de 1 à N, ce qui précisera l'ordre d'affichage des objets sur le mur. Ceci est utile pour résoudre les cas de superposition, mais aussi pour générer un effet 3D. Celui-ci se manifeste si au moins l'un des paramètres de génération X ou Y est défini : ces valeurs sont des coefficients multiplicateurs appliqués au déplacement de la souris sur la page pour déterminer un mouvement de chaque objet en fonction de son numéro d'ordre.

En mode d'affichage normal, quand la page générée est appelée par un internaute, les fonctions JQuery sont désactivées, sauf, le cas échéant, celles relatives à l'affichage 3D.

2. Les composants graphiques complémentaires

La possibilité d'ajouter des objets joints (attachments) à une œuvre ou des œuvres filles à une œuvre mère multiplie les possibilités de présentation.

2.1. Les composants spéciaux

2.2. Les animations

  • Le bougre de Pétaouchnock
    passe régulièrement sur le bas des pages du site Pétaouchnock (hors page d'accueil).
    Il ne fait pas encore l'objet d'un composant réutilisable.

3. Évolutions possibles

La bibliothèque des composants n'est encore qu'embryonnaire, elle peut donc être encore notablement améliorée et complétée.

3.1. Adaptations générales

  • Utilisation des composants avec les objets joints aux œuvres (attachments)
    Les objets joints à une œuvre font partie d'une classe récemment ajoutée au projet, encore peu outillée. Ils peuvent être différentes représentations (photos ou dessins) de celles-ci dont on peut souhaiter l'affichage sur un mur.
    Dans ce cas, la difficulté provient de l'absence de collage direct entre l'objet joint et le mur, c'est-à-dire l'espace. Si ce point est sans conséquence pour les composants graphiques génériques où les éléments sont placés automatiquement, il est important pour les composants positionneurs et nécessite de définir comment enregistrer la position des images et textes.
  • Structuration et séparation du code JQuery des composants
    Relecture du code JQuery et séparation du code Ruby des composants.

3.2. Les composants unitaires

  • Image de fond
    Formalisation d'un composant pour positionner dynamiquement une image de fond en fonction notamment de la taille de la fenêtre du navigateur.

3.3. Les composants génériques

  • Images flottantes
    Formalisation d'un composant pour positionner dynamiquement une série d'images en fonction notamment de la taille de la fenêtre du navigateur (mode CSS float).
  • Diaporamas
    Formalisation d'un ou de plusieurs composants de présentation d'images successivement par l'utilisation d'un des composants JQuery disponibles sur l'internet.

3.4. Les composants positionneurs

  • Navigation sur un plan
    Définition et réalisation d'un composant disposant les éléments graphiques sur une zone très supérieure à la taille de l'écran, avec gestion du placement des objets, du fond et du déplacement de l'affichage sur la zone.
    Extension à une navigation multi-plans, en 2D ou en 3D.
  • Positionnement libre adaptable
    Le composant p5003_free génère un mur de dimension fixe. Adaptation du composant aux écrans de taille réduite
  • p5003_free : gestion des bordures
    La définition d'une bordure CSS autour des images et textes entraine une bordure de cadre gênante en mode positionnement et un décalage inadapté des coordonnées.

3.5. Les composants spéciaux

  • Animations
    Formalisation d'un ou de plusieurs composants produisant des animations comme le bougre de Pétaouchnock.
  • Autres composants
    ???

Développement du logiciel

4. Genèse d'un composant

Cet article est une description didactique, phase par phase, de l'écriture du composant p4008_google_search pris comme exemple. Par la même occasion, il permet de revoir quelques bases de la programmation Ruby et HAML ainsi que du passage de paramètres dans le logiciel projet_a.

P4008 2 388x44 121014

Pour des raisons pédagogiques, les fonctionnalités du composant seront ajoutées les unes après les autres. À la fin de chaque phase et de chaque sous-phase, la version du composant obtenue sera pleinement opérationnelle et conforme à ses spécifications. De plus, chaque version successive permettra aussi de répondre aux spécifications des versions précédentes et de pouvoir être appelée avec leurs interfaces.

Le composant p4008_google_search sera pris comme exemple. L'objectif initial de celui-ci était de permettre une recherche plein texte sur un site. Pour simplifier la réalisation, cette opération a été déléguée au moteur de recherche Google en exploitant le fait qu'il limite son exploration à un site quand on lui passe parmi les mots-clés cherchés un nom de domaine précédé des caractères "site:" (ex. : site:a-io.eu).

La réalisation du composant doit bien sûr adopter les conventions et utiliser les outils définis dans le logiciel projet_a pour les composants. Le code sera écrit dans le langage de vues HAML, concis et clair, surtout ici pour une présentation élément par élément. L'appel du composant dans un générateur de pages sera lui soit de préférence au format GAL du projet A, soit au formar HAML.

Conventions d'écriture :

Dans un fichier source, les différents langages sont souvent intriqués. Pour faciliter leur compréhension, les lignes de code seront mises en couleurs :

       vert
gris
orange    
rouge
rouge vif
bleu
Commentaires (tous langages)
Code HTML sous HAML
Code CSS
Code Ruby sous HAML
Code GAL
Code Javascript et JQuery

1. Phase 1 : Recherche sur le site courant

1.1. Interface graphique :

La partie visible du composant comportera trois éléments : un bloc englobant, un champ de saisie de texte et un élément d'activation.

Composant p4008 réduit

Le bloc englobant sera un bloc HTML <div> avec comme classe CSS le nom du composant. Cette convention est respectée pour chaque composant pour faciliter le ciblage d'un style CSS. La balise <div> étant par défaut en HAML, le code sera simplement :

.p4008

Le champ de saisie sera défini par une balise <input> de type text :

  %input(type=text)

L'élément d'activation de la commande de recherche peut être soit un bouton, soit un lien sur un texte ou sur une image. Pour obtenir une interface simple, légère et claire, le lien sur un texte a été retenu. Le lien sera produit par une balise <a> sans attributs, car ceux-ci seront ajoutés dynamiquement par du code Javascript. Le texte sera le mot Rechercher déjà défini dans les fichiers d'internationalisation et appelé par la fonction I18n::translate, alias t tout simplement :

  %a= t('general.go_search')

Pour améliorer un peu l'ergonomie, on peut ajouter un texte en infobulle au survol du lien, c'est-à-dire comme attribut title de la balise <a>. Le texte, "Rechercher sur le site" en français, sera défini dans la sous-section du composant dans la section components des fichiers de langue. La valeur d'un attribut en HAML devant être une chaine de caractères, la fonction t sera interpolé sur la ligne de code qui devient :

  %a(title="#{t('components.p4008.site_search')}")= t('general.go_search')

Le code HAML de l'interface :

.p4008
  %input(type=text)
  %a(title="#{t('components.p4008.site_search')}")= t('general.go_search')

Le fichier de styles a_components.css dispose les éléments graphiques alignés et cadré à droite ; il fixe aussi la taille du champ de saisie ; le curseur sur le mot d'activation est forcé, car une balise <a> sans attribut href n'est pas considérée comme un lien actif par la plupart des navigateurs. Cette présentation pourra bien sûr être ajustée par la feuille de styles de chaque site utilisateur du composant :

.p4008 {text-align:right;}
.p4008 input {display:inline; width:200px;}
.p4008 a {cursor:pointer;}

1.2. Code Javascript :

La dynamique du composant est assurée par du code Javascript utilisant la bibliothèque JQuery. Bien qu'il soit conseillé de ne pas placer de code Javascript dans les sources HTML, les instructions du composant seront placées à la fin du fichier HAML, dans un bloc <script> ouvert par la ligne de code :javascript.

Le code à exécuter sur l'activation du lien Rechercher doit à minima récupérer le texte saisi par l'internaute dans le champ, puis ouvrir une nouvelle fenêtre pour l'affichage du résultat de la recherche.

Le texte est la valeur du champ <input> du composant :

var text = $(".p4008 input").val();

Comme cette valeur sera incluse dans la requête passée à Google dans l'URL d'appel, elle doit être encodée pour les cas où l'internaute taperait certains caractères spéciaux. La ligne précédente est donc à remplacer par :

var value = encodeURIComponent($(".p4008 input").val());

L'ouverture de la fenêtre ou de l'onglet pour afficher le résultat est la méthode open de l'objet window du navigateur qui comporte un seul paramètre obligatoire, l'URL de la requête :

open(URL);

L'URL est constituée de l'URL de la page de recherche de Google suivie en paramètre q des critères de recherche : les mots-clés tapés par l'utilisateur et encodés dans la variable value ainsi que le nom de domaine du site courant précédé de "site:". L'instruction devient :

open("http://www.google.com/search?q=" + value + " site:#{@site.domain}");

Vous noterez que les deux valeurs incluses sont traitées différemment :

  • value est une variable Javascript dont le contenu n'est défini qu'au moment de l'activation du lien Rechercher, elle est concaténée au préfixe de l'URL ;
  • @site.domain est la méthode qui fournit le nom de domaine du site courant, défini par la variable Ruby @site lors de la génération de la page, elle est interpolée dans le code du Javascript à ce moment-là.

Ces instructions à exécuter sur l'activation du lien Rechercher sont à placer dans le gestionnaire de l'évènement du clic sur le lien :

$('.p4008 a').click(function(){
  ...
};

Ce gestionnaire d'évènement ne doit être déclaré qu'après la fin du chargement de la page HTML. Son code doit lui-même être inséré dans le gestionnaire de l'évènement load de la fenêtre du navigateur :

$(window).load(function(){
  ...
};

Les lignes à ajouter à la fin du fichier HAML du composant sont donc les suivantes :

:javascript
  $(window).load(function(){
    $(.p4008 a).click(function(){
      var value = encodeURIComponent($(".p4008 input").val());
      open("http://www.google.com/search?q=" + value + " site:#{@site.domain}");
    });
  });

Là aussi, on va améliorer l'ergonomie en refusant de lancer la requête de recherche si l'internaute n'a pas saisi de critères. Il nous faut tester la valeur lue du champ <input> après suppression des espaces d'extrémité par la fonction JQuery $.trim. Si la valeur est vide, alors la saisie obligatoire sera notifiée à l'utilisateur avant de replacer le focus sur le champ <input>.

Par ailleurs, si on lance plusieurs recherches successives depuis le composant, les résultats s'affichent à chaque fois dans une nouvelle fenêtre ou un nouvel onglet. Pour éviter ce comportement envahissant, il suffit d'ajouter un nom de fenêtre constant comme deuxième paramètre de la méthode open :

open("http://www.google.com/search?q=" + value + " site:#{@site.domain}", 'p4008');

1.3. Source 1 :

En ajoutant en ligne de tête un commentaire d'identification, le fichier source du composant devient :

-# _p4008_google_search.html.haml - Phase 1
.p4008
  %input(type=text)
  %a(title="#{t('components.p4008.site_search')}")= t('general.go_search')

:javascript
  $(window).load(function(){
    $(.p4008 a).click(function(){
      var input = $(".p4008 input");
      var value = encodeURIComponent($.trim(input.val()));
      if (value == '') {
        alert("#{t('general.oblig')}");
        input.focus();
      } else {
        open("http://www.google.com/search?q=" + value + " site:#{@site.domain}", 'p4008');
      }
    });
  });

Le composant peut être appelé depuis un générateur de pages au format HAML par l'instruction suivante :

= render 'p4008_google_search'

ou plus simplement, au format GAL du projet A :

=> p4008_google_search

2. Phase 2 : Recherche sur un site donné

La première évolution du composant permettra de lancer une recherche dans les pages d'un site dont le nom de domaine sera fourni soit en paramètre du composant, soit en paramètre de génération de l'espace. Comme plusieurs instances du composant peuvent être déclarées dans le code d'un générateur pour permettre les recherches sur autant de sites différents, notre code devra être adapté en conséquence.

2.1. Site défini comme paramètre du composant

Le nom de domaine pour la recherche pourra être précisé dans la variable locale domain. L'appel HAML au composant sera alors de la forme :

= render :partial => 'p4008_google_search', :locals => { domain: 'a-io.eu' }

soit, en langage GAL :

=> p4008_google_search, domain: 'a-io.eu'

Pour garder la compatibilité avec la version précédente, le code du composant commencera par l'instruction :

- domain ||= @site.domain

qui, en dialecte Ruby, signifie que la variable domain sera initialisée au nom de domaine du site courant si cette variable n'est pas définie (ou contient false ou nil).

Dans le code du composant, seule l'URL de la requête de recherche doit être modifiée. Elle devient :

        open("http://www.google.com/search?q=" + value + " site:#{domain}", 'p4008');

C'est également plus sympathique pour les internautes de préciser le site de recherche dans l'infobulle sur le lien Rechercher, ce qui donne :

  %a(title="#{t('components.p4008.site_search')} #{domain}")= t('general.go_search')

2.2. Source 2.1 :

Le code du composant devient alors :

-# _p4008_google_search.html.haml - Phase 2.1
- domain ||= @site.domain
.p4008
  %input(type=text)
  %a(title="#{t('components.p4008.site_search')} #{domain}")= t('general.go_search')
:javascript
  $(window).load(function(){
    $(.p4008 a).click(function(){
      var input = $(".p4008 input");
      var value = encodeURIComponent($.trim(input.val()));
      if (value == '') {
        alert("#{t('general.oblig')}");
        input.focus();
      } else {
        open("http://www.google.com/search?q=" + value + " site:#{domain}", 'p4008');
      }
    });
  });

2.3. Site défini comme paramètre de génération de l'espace

Pour définir le domaine de recherche comme paramètre de génération search_domain de l'espace courant, on modifiera la première instruction du composant :

- domain ||= @gen_params[:search_domain] || @site.domain

Elle signifie que la valeur de la variable domain est celle du paramètre d'appel domain s'il est précisé, sinon celle du paramètre de génération search_domain de l'espace courant, sinon le nom de domaine du site courant.

2.4. Utilisation multiple du composant sur une page

Tel qu'il est écrit comme ci-dessus, le composant présente deux défauts majeurs quand on le place plusieurs fois sur la même page, pour des sites de recherche différents : le premier champ de saisie est toujours lu et l'exécution est multiple.

2.5. Sélection imprécise

L'instruction JQuery $(".p4008 input") sélectionne en réalité tous les éléments input placés derrière un élément de la classe p4008. Appliquée à un tableau d'éléments JQuery, la méthode val() retourne la valeur du premier élément. Pour résoudre ce problème, la sélection du champ de saisie doit être plus précise pour ne retourner que l'élément situé dans le même bloc que le lien Rechercher cliqué.

Pour cela, partons de cet élément qui est référencé par $(this) dans le gestionnaire d'évènements et recherchons parmi ses frères – les balises de même niveau que la balise <a> dans le bloc <div> – les éléments input. Comme notre composant n'en contient qu'un seul, ce sera celui cherché :

var input = $(this).siblings("input");

2.6. Exécution multiple

Dans le code du générateur, chaque appel au composant génère dans la page HTML un bloc <div class="p4008"> et le bloc Javascript. Chacun déclare un gestionnaire d'évènements sur les clics sur les balises <a> des blocs <div class="p4008">. Comme dans le paragraphe précédent, la sélection $(".p4008 a") est imprécise et englobe à chaque fois toutes les balises correspondantes de la page qui se voient chacune dotées de plusieurs gestionnaires de clics (autant que d'instanciations du composant). Contrairement au cas précédent, il n'est pas ici possible d'assurer une sélection plus précise, car dans le gestionnaire $(windows).load(), $(this) référence la fenêtre toute entière.

La solution est de ne générer qu'une seule fois le code Javascript, lors de la première génération du composant. Dans ce but, l'instruction :javascript et tout le code Javascript qui la suit seront inclus dans une instruction conditionnelle Ruby qui testera l'existence d'une variable spécifique au composant. Si elle n'est pas définie, alors elle le sera immédiatement, puis le Javascript sera généré :

- unless defined?(@p4008_generated)
  - @p4008_generated = true
  :javascript
    ...

La génération unique du code Javascript a un effet de bord malencontreux : dans l'URL d'appel de Google, l'interpolation de la variable Ruby domain se fera à partir de la valeur du domaine de recherche du premier appel du composant. Pour éviter ça, cette interpolation qui permet de passer au Javascript une variable de la génération Ruby doit être déplacée sur l'une des trois lignes du composant générées à chaque instanciation, c'est-à-dire celles qui produisent le bloc <div> et son contenu. Le plus simple et le plus clair est d'ajouter un attribut data à la balise <div> qui devient :

.p4008(data-domain=domain)

NB : Pour la valeur d'un attribut d'une balise HTML, le compilateur HAML effectue automatiquement l'interpolation de la variable Ruby sans devoir l'écrire dans le traditionnel "#{...}". Cette forme a aussi un avantage qui sera exploité ultérieurement lors de la phase 3 de la conception du composant.

Il ne reste plus qu'à récupérer cette valeur lors de l'exécution pour la concaténer lors du calcul de l'URL après le clic sur le lien Rechercher. Ici aussi, il faut faire attention de sélectionner le bon bloc <div> en partant de $(this) et en remontant à son père :

var site = $(this).parent().data('domain');
open("http://www.google.com/search?q=" + value + " site:" + site, 'p4008');

2.7. Source 2.3 :

Le code du composant est le suivant :

-# _p4008_google_search.html.haml - Phase 2.3
- domain ||= @gen_params[:search_domain] || @site.domain
.p4008(data-domain=domain)
  %input(type=text)
  %a(title="#{t('components.p4008.site_search')} #{domain}")= t('general.go_search')
- unless defined?(@p4008_generated)
  - @p4008_generated = true
  :javascript
    $(window).load(function(){
      $(.p4008 a).click(function(){
        var site = $(this).parent().data('domain');
        var input = $(this).siblings("input");
        var value = encodeURIComponent($.trim(input.val()));
        if (value == '') {
          alert("#{t('general.oblig')}");
          input.focus();
        } else {
          open("http://www.google.com/search?q=" + value + " site:" + site, 'p4008');
        }
      });
    })

3. Phase 3 : Recherche sur un site choisi dans une liste

Dans le cas du besoin de proposer la recherche d'informations dans les pages de plusieurs sites, plutôt que d'implémenter le composant plusieurs fois, il est le plus souvent judicieux de donner à choisir un site dans une liste.

3.1. Interface d'appel :

Dans la version précédente, la variable d'appel domain pouvait définir le nom de domaine du site de recherche, ou par défaut le paramètre search_domain de génération de l'espace, ou par défaut encore le site courant. Pour respecter la règle de compatibilité avec les versions antérieures, cette règle est conservée, ce qui revient à maintenir sans changement la première ligne de code :

- domain ||= @gen_params[:search_domain] || @site.domain

L'objectif de la nouvelle version implique d'accepter plusieurs noms de domaine, ce qui peux être réalisé en autorisant un tableau de valeurs en paramètre. Dans ce cas, les noms de paramètres domain et search_domain au singulier sont sémantiquement incorrects, donc sources d'erreurs potentielles. Les formes au pluriel, respectivement domains et search_domains, seront donc aussi acceptées, avec une préférence du pluriel sur le singulier au cas où les deux formes seraient fournies lors d'un appel du composant (il faut bien faire un choix...). Ceci se traduit par la ligne suivante :

- domains ||= @gen_params[:search_domains] || domain

NB : Avec ces deux lignes, il est possible de déclarer plusieurs sites dans domain ou un seul dans domains. Ce n'est pas grave, le résultat défini dans l'une des formes de paramétrage, sinon la valeur par défaut, se retrouvera finalement dans la variable domains.

Ne serait-ce que pour le site portail http://a-io.eu de l'association, ce serait bien aussi d'accepter une valeur spéciale sélectionnant automatiquement tous les sites déclarés, ou plus précisément tous les sites visibles par l'internaute en fonction de ses droits et de son état de connexion. La valeur spéciale d'appel sera la valeur booléenne true ou le symbole :all ou la chaine de carractères 'all'. La méthode all_visible_by de la classe Site sélectionne les sites convenables ; la méthode map du module Ruby Enumerable construit alors un tableau dont chaque valeur est défini par le bloc suivant (current_user désigne l'utilisateur dans le logiciel) :

- if [true, :all, 'all'].include?(domains)
  - domains = Site.all_visible_by(current_user).map {|site| site.domain}

Enfin, pour pouvoir utiliser au mieux le code de la version précédente du composant, nous restaurons la variable domain dans le cas où une simple chaine est fournie, sinon nous l'initialisons à nil :

- domain = domains.is_a?(String) ? domains : nil

Avec la valeur nil dans domain, l'attribut data-domain de la balise <div> n'est pas généré par la ligne ".p4008(data-domain=domain)".

3.2. Interface graphique :

Composant p4008 complet

Dans le cas où un tableau de valeurs est fourni, une liste déroulante qui contiendra les noms de domaine de recherche est simplement ajoutée devant le champ de saisie des critères. Cette liste est produite par un bloc HTML <select>, lui-même généré par les assistants (helpers) Rails select_tag et options_for_select. Dans notre cas, ce dernier ne prend comme paramètre que le tableau des noms de domaines enregistré dans la variable domains.

  = select_tag :domains, options_for_select(domains) if domains.class == Array

3.3. Impact sur le code Javascript :

Visiblement, l'ajout d'une liste de valeur a deux impacts sur la dynamique Javascript du composant : la nécessité du calcul de l'infobulle sur le lien Rechercher et bien sûr un calcul adapté de la requête vers Google.

L'infobulle s'affiche lors du survol du lien Rechercher, un gestionnaire d'évènements mouseover peut calculer le texte de l'infobulle en fonction de l'élément positionné de la liste des sites. Cette action ne doit être effectuée que si cette liste est présente :

      $('.p4008 a').mouseover(function() {
        var select = $(this).siblings("select");
        if (select.size() > 0) {
          var domain = select.val();
          $(this).attr('title', "#{t('components.p4008.site_search')} " + domain);
        }
      });

La sélection de l'élément liste <select> est similaire à celle de l'<input> dans la version précédente. Nous profitons du fait qu'une sélection JQuery retourne toujours un tableau d'éléments, vide dans le cas d'une sélection infructueuse, comme quand le composant ne doit gérer qu'un seul site de recherche.

En ce qui concerne le calcul de l'URL de requête vers Google, remarquons que le gestionnaire d'évènements click déjà écrit extrait le nom de domaine de l'attribut data-domain du bloc <div> du composant. Comme le lien Rechercher doit avoir été survolé avant d'être cliqué, en positionnant data-domain dans le gestionnaire mouseover le gestionnaire click sera capable en l'état de produire la bonne URL. Ajoutons donc la ligne suivante à la fin du if du bloc de code cité précédemment :

          $(this).parent().data('domain', domain);

3.4. Source 3 :

En chainant les deux gestionnaires d'évènements appliqués au(x) lien(s) Rechercher, le code du composant est maintenant le suivant :

-# _p4008_google_search.html.haml - Phase 3
- domain ||= @gen_params[:search_domain] || @site.domain
- domains ||= @gen_params[:search_domains] || domain
- if [true, :all, 'all'].include?(domains)
  - domains = Site.all_visible_by(current_user).map {|site| site.domain}
- domain = domains.is_a?(String) ? domains : nil

.p4008(data-domain=domain)
  = select_tag :domains, options_for_select(domains) if domains.is_a?(Array)
  %input(type=text)
  %a(title="#{t('components.p4008.site_search')} #{domain}")= t('general.go_search')
- unless defined?(@p4008_generated)
  - @p4008_generated = true
  :javascript
    $(window).load(function(){
      $(.p4008 a)
       .mouseover(function() {
        var select = $(this).siblings("select");
        if (select.size() > 0) {
          var domain = select.val();
          $(this).attr('title', "#{t('components.p4008.site_search')} " + domain);
        }
      })

       .click(function(){

        var site = $(this).parent().data('domain');
        var input = $(this).siblings("input");
        var value = encodeURIComponent($.trim(input.val()));
        if (value == '') {
          alert("#{t('general.oblig')}");
          input.focus();
        } else {
          open("http://www.google.com/search?q=" + value
+ " site:" + site, 'p4008');
        }
      });
    })

version 0.9.5-0442-150125

Développement du logiciel

5. Gestion du cache

Utilité d'un cache. Caractéristiques. Difficultés. Solutions Rails. Cas du projet A : problème et solution.

Article en cours d'écriture

Après quelques rappels sur le fonctionnement d'un cache et les possibilités qu'offre Rails pour en mettre un en œuvre, cet article décrira comment cette technique est utilisée dans le cadre de l'application projet A qui génère plusieurs sites indépendants au moyen de générateurs et de composants adaptables.

1. Généralités sur le cache

1.1. Pourquoi un cache ?

Les pages des sites web dynamiques sont produites par interrogation d'une base de données, puis par l'intégration des résultats dans un ou plusieurs modèles (des vues) de code HTML, eux-mêmes ensuite intégrés dans un gabarit de pages. Ce processus est relativement long et il nécessite des ressources du serveur alors que, la plupart du temps, une page doit être servie à de multiples utilisateurs sans que la moindre information qu'elle contient n'ait été modifiée.

Une manière de réduire cette surcharge inutile consiste à enregistrer spécifiquement dans une cache des résultats intermédiaires (retours des requêtes SQL, blocs HTML), voire l'intégralité des pages à servir, puis d'utiliser tels quels ces résultats lors d'une nouvelle requête similaire.

1.2. Caractéristiques d'un cache

Toutes les techniques de cache ont un certain nombre de propriétés en commun, même si leurs réalisations peuvent être très variées : l'accès simple aux données ; le stockage rapide ; la taille limitée ; la durée de validité des données ; la permanence des données ; un partage des données ; la charge de gestion.

L'accès simple aux données est généralement fourni par l'association d'une clé à chaque bloc de données.

La gestion du cache doit être plus rapide que la production des données qu'il doit enregistrer. Une solution simple est l'enregistrement de chaque donnée dans un fichier dont le nom est la clé, dans une arborescence de dossiers optimisée pour un accès rapide même avec de très nombreux fichiers. L'utilisation d'un bloc de mémoire pour stocker la totalité du cache est encore plus rapide, mais volatile.

Sauf dans le cas d'espaces disque de grande capacité, la taille du stockage consacré au cache est limitée. Un algorithme d'effacement automatique des données les plus vieilles ou les moins utilisées est alors mis en œuvre, ce qui est le plus souvent peu gênant quand les données supprimées ont peu ou pas de chances d'être rappelées.

L'application utilisatrice du cache peut connaître à priori la durée de validité des données qu'elle met en cache et la lui communiquer. Certains caches ne savent pas gérer cette information.

Dans le cas où la production des pages HTML est longue est complexe, la permanence des données dans le cache peut garantir un délai rapide de délivrance des pages, car, dans tous les cas, elles proviendront du cache avec un temps de réponse rapide. Cette propriété n'est possible que si la taille du cache est plus grande que celle de la totalité des pages à servir ; elle s'accompagne logiquement d'un processus de production des pages au démarrage du serveur ou de l'application.

Quand le service d'une application est réparti entre plusieurs processus, sur le même serveur ou sur plusieurs, Par exemple au moyen de logiciels tels qu'Unicorn ou Passenger, le partage du cache entre tous les processus mutualise la charge de production des pages.

Enfin, suivant les techniques utilisées, la charge de gestion du cache sera plus ou moins importante et elle peut parfois entrainer des temps de latences très important, notamment au démarrage de l'application ou lors de la suppression d'éléments dans le cache.

1.3. Où se cache les difficultés ?

Les plus grandes difficultés dans la mise en œuvre d'une technique de cache proviennent de l'utilisation de données pourries, c'est-à-dire d'informations qui ont été modifiées depuis leur mise en cache. Si elles ne sont pas effacées du cache, ou tout au moins rendues inaccessibles, des pages non actualisées peuvent être envoyées aux internautes.

Une page contient toujours des données très diverses, en plus de celles objets de la page : le nom de la ou des rubriques englobantes, le menu de navigation, des actualités, un bas de page personnalisé ou non, avec des commentaires associés, etc. Et si, par construction, le programmeur connait de quoi est composé une page, l'inverse est très souvent loin d'être le cas : dans quelles pages chaque information de base est-elle reprise, directement ou indirectement ? Le problème est d'autant plus complexe que le site est lui-même complexe, composé de différents modules indépendants dont la logique échappe parfois au concepteur du site.

2. Gestion du cache avec Rails

On se réfèrera avantageusement au guide Rails Caching with Rails: An overview pour la description détaillée des solutions qu'offrent Rails concernant la mise en œuvre d'un cache, description dont trois volets sont brièvement repris ci-dessous : modes de stockage, portée et calcul des clés.

2.1. Modes de stockage

En résumé, Rails propose trois modes de stockage du cache :

  • FileStore : enregistrement en fichiers, stockage permanent donc, mais qui n'autorise pas la définition d'une durée de validité des éléments enregistrés ;
  • MemoryStore : enregistrement dans un espace mémoire dépendant du processus, donc pas de partage si la charge de l'application est répartie sur plusieurs ;
  • MemCacheStore : enregistrement en mémoire via le service memcached installé indépen­damment de l'application, donc partage possible entre plusieurs instances de l'application, même sur des serveurs différents ; par contre, comme ce mode privilégie les temps de réponse, il n'implémente pas l'effacement d'enregistrements autre que la réinitialisation complète du cache.

 2.2. Techniques de cache

Rails propose trois mécanismes de cache : par page, par action et par fragment. Les deux premiers nécessite l'ajout d'une gem depuis la version 4 de Rails.

2.3. Cache par page

La méthode de cache par page est la plus efficace, car elle enregistre la page HTML générée intégralement et la sert à nouveau sans passer par toute la pile des traitements d'une requête Rails.

Par contre, son activation s'effectue par action d'un contrôleur et, comme elle ne passe pas par le contrôleur quand la page est dans le cache, elle n'est utilisable que pour des pages quasi-statiques, ne dépendant pas du contexte. Les pages ne seront en effet modifiées que sur effacement explicite ou sur limite du délai de validité.

2.4. Cache par action

Comme la précédente, la méthode de cache par action enregistre la page HTML générée, mais elle exécute les filtres (before_filter) du contrôleur avant de chercher à réutiliser le cache. De plus, elle permet une particularisation de la clé de cache, permettant d'y inclure des éléments du contexte. Elle permet d'exclure du cache le gabarit (layout) associée à la vue si celui-ci contient des éléments dynamiques.

2.5. Cache par fragment

La méthode de cache par fragment permet de cacher une ou plusieurs parties de la page à générer. Celle-ci est alors composée à partir des fragments réutilisables du cache et des parties non cachées de la vue .

La clé de chaque fragment peut être adaptée à chaque contexte en fonction de son contenu. Ainsi, si seul un fragment d'une page est devenu caduque, seul celui-ci est régénéré.

La méthode de cache par fragment peut bien sûr être utilisée avec les autres méthodes quand certains fragments sont communs à plusieurs pages.

2.6. Calcul des clés

 La demande de mise en cache d'un fragment s'effectue dans une vue en appelant la méthode cache avec, comme bloc associé, les lignes de code de génération du fragment.

Exemple en ERB :

<% cache [paramètres] do %>
   lignes de code
   de génération
<% end %>

Exemple en HAML :

- cache [paramètres] do
  lignes de code
  de génération
 

La méthode cache admet un paramètre simple ou un tableau de paramètres à partir desquels sera construite la clé d'enregistrement dans le cache. Pour cela, la méthode cache_key, sinon la méthode to_param, est appelée sur chaque objet ; pour un objet du modèle de donnée, cache_key retourne par défaut une chaine contenant le nom de la classe de l'objet, son identifiant et son heure de dernière mise à jour. Exemple : works/2423-20140123173257.

Cette structure de clé facilite une gestion automatique du cache appelée « poupées russes » (Russians dolls) : quand l'objet de la page (works dans l'exemple) est modifié, sa date change et avec elle la clé du cache ; l'ancien enregistrement en cache n'est plus accessible ; il sera automatiquement effacé quand le cache aura besoin d'espace. Si la page inclut des informations de plusieurs objets, la clé de la page est composée à partir de leurs identifiants et de leurs dates de dernière actualisation ou, à minima, de la date la plus récente.

Pour le cache par action, la clé par défaut est construite à partir de la route d'accès de l'action du contrôleur concernés, mais il est possible de la personnaliser de la même façon.

3. Cas du projet A

Examinons maintenant le cas du logiciel projet_a : ses contraintes et les solutions apportées pour l'utilisation d'un cache.

3.1. Caractéristiques de l'application

Le but de l'application est de produire des sites. Ceux-ci sont quasi-indépendants les uns des autres et, pour des questions de cohérence, chaque objet est rattaché à un site.

Une fois construits, les sites sont peu modifiés ; leur optimisation en consultation est donc prioritaire.

La structure des sites est basée sur l'arbre des espaces (spaces), puis sur les œuvres (works) associées. Sauf quelques exceptions, les pages générées sont donc des pages associées chacune à un espace décrivant des œuvres ou à une seule œuvre.

La forte capacité d'adaptation aux besoins du logiciel conduit toutefois à de notables variations et exceptions :

Quelques générateurs de pages prennent en compte les paramètres HTML figurant en fin d'URL.

Toutes les pages comportent un menu de navigation (deux ou trois niveaux d'espaces) ou les niveaux précédents. Quelques sites (AIO ; CG) et certaines pages de gestion des espaces affichent l'arbre global sur chaque page. Un webmestre peut demander à voir toutes les pages qu'il gère à tout niveau de visibilité et avec quelques options de construction (grilles de calage...).

Une très forte majorité de pages sont en accès public. Les autres ont un contenu privé fonction des quatre niveaux de visibilité correspondants.

Presque toutes les pages ont un contenu stables, sauf quelques unes avec effets spéciaux (par exemple, l'accueil des sites CM et MAD ont une image ou une couleur aléatoire).

Les informations des pages d'actualités sont reprises au niveau de la page d'accueil du site et sur la page portail AIO. Il existe quelques autres dépendantes inter-espaces, enregistrées dans leurs paramètres de génération.

Quelques collages (stickers) ont des dates de début et de fin de publication, en particulier ceux des actualités.

Quelques pages affiche des données d'un groupe d'utilisateurs, voire de plusieurs. Sinon, il n'y a pas d'informations utilisateur autre que sur le bas de page en mode privé.

Les modifications des composants ou des générateurs et celles des paramètres de site ou de branches d'espaces sont peu fréquentes, mais peuvent se produire à tout moment.

3.2. Choix du stockage du cache

Le choix d'un stockage de cache conditionne la programmation, car, comme nous l'avons vu ci-dessus, chaque type de stockage n'implémente que certaines fonctionnalités.

3.2.1. FileStore ?

La première idée qui paraît la plus simple est l'utilisation d'un enregistrement sous forme de fichiers : solution incluse dans Rails ; espace disque disponible ; permanence du cache, compatible avec le taux très faible des mises à jour.

Une première tentative avec FileStore a vite montré des limites quasi-rédhibitoires :

Dès que le cache s'est un peu rempli (quelques centaines d'éléments), la méthode d'effacement par expression régulière devient insupportablement longue. Elle doit en effet lire la totalité des répertoires et sous-répertoires du cache pour retrouver les noms de fichiers (égaux aux clés), puis les filtrer en leur appliquant l'expression. Force est donc de se passer de cette méthode et d'utiliser des clés horodatées de la stratégie de poupée russes précédemment décrite.

On peut aussi gérer soi-même l'indexation du cache... en stockant les tables d'index dans le cache par exemple. La structure du contenu de l'application projet_a étant assez simple, essentiellement basée sur les espaces (objet Space), une table d'index à accès aléatoire vers une table d'enregistrements chainés en cas de collision donnait des résultats satisfaisants. Un peu usine à gaz, mais ça tournait bien.

Hélas ! Il y a toujours des cas qui ne rentrent pas bien dans votre système d'indexation ou globalement dans votre gestion du cache. Il vous reste alors quelques pages – pas beaucoup, très peu même – qui s'obstinent à s'afficher à partir d'un élément du cache contenant des données caduques.

Et comme le stockage FileStore du cache ne permet pas de définir de délais de validité, même par défaut, il ne vous reste plus qu'à purger régulièrement la totalité du cache, perdant ensuite pendant de longues dizaines de minutes le bénéfice de cette technique.

3.2.2. MemCacheStore ! (avec memcached)

Le stockage CacheStore ne pouvant être partagé entre plusieurs processus, cette solution n'est pas envisageable, car l'application projet_a est placée derrière un serveur Nginx qui permet de paralléliser des traitements concurrents.

Il ne reste donc plus que MemCacheStore qui enregistre les données au travers d'un service Linux (ou Windows si vous y tenez) memcached.

Les avantages de cette solution est qu'elle est simple à mettre en œuvre, qu'elle est très rapide et peu gourmande en ressources CPU, qu'elle est partageable entre plusieurs processus ou applications, Rails ou non, sur le même serveur physique ou non, qu'elle accepte un délai de validité – par défaut ou au coup par coup – pour chaque élément mis en cache.

Ses inconvénients sont qu'elle ne permet pas l'effacement ciblé d'éléments dans le cache et que, ce dernier résidant en mémoire vive, il est complètement réinitialisé lors d'un redémarrage du serveur.

3.3. Installation et configuration du cache

Sous Linux Debian, pour installer memcached, tapez sous le compte root :

apt-get memcached

Le service correspondant est installé et démarré avec le fichier de configuration sur /etc/memcached.conf. Les valeurs par défaut sont opérationnelles.

La communication avec une application Ruby passe par la gem dalli. Il faut donc ajouter au fichier Gemfile de l'application :

gem dalli

puis lancer un : bundle update

Dans le fichier production.rb, modifier ou ajouter la ligne, pour une durée de validité par défaut de 24 heures :

config.cache_store = :dalli_store, { expires_in: 86400 }

Quand vous voudrez tester le cache en développement, ajouter cette même ligne dans le fichier development.rb et activez temporairement le cache par :

config.action_controller.perform_caching = true

À suivre...

version 0.9.5-0442-150126
↑ Haut