TP - page d’accueil¶
A présent, vous avez les connaissances de base nécessaires pour comprendre la plupart des fonctionnalités du forum ; nous pouvons effectuer un petit travail pratique ! Le but de ce TP est de réaliser la page suivante :

Cette page est également responsive
. Donc, lorsque nous réduisons la taille de la fenêtre du navigateur web, nous obtenons le résultat suivant :

Il s’agit de créer une page sur laquelle s’afficheront les discussions. Vous allez devoir récupérer les informations suivantes depuis la base de données :
- le sujet de la discussion
- l’id de la discussion (il servira à donner le lien sur lequel la discussion pointera - par exemple :
http://localhost:port/1
renverra vers la première discussion) - l’utilisateur qui a créé la discussion (c’est-à-dire l’auteur du premier message de la discussion)
- la date d’écriture de la discussion, formatée en français
- le nombre de messages de la discussion
Puisque l’application s’actualise en temps réel, lorsqu’une nouvelle discussion est créée, celle-ci s’affiche à la suite des discussions déjà existantes sans que la page aie besoin d’être chargée. Comme nous n’avons pas encore réalisé la création de nouvelles discussions, il faudra juste écrire une fonction anonyme qui sera appelée avec l’événement socket charger_derniere_discussion
.
socket.on('charger_derniere_discussion', function() {
// ...
});
Cette page comprend également un mini moteur de recherche. Si nous tapons du texte dans la barre de recherche, seules les discussions dont le sujet contient partiellement ou entièrement le terme de recherche sont affichées :

Pistes¶
- Afficher une discussion nécessite beaucoup de formattage
HTML
. Il est donc préférable de charger les discussions en utilisantEJS
etAJAX
. En outre, vous pouvez déjà créer une fonction qui chargera la dernière discussion ; nous en aurons besoin par la suite pour recharger les discussions en temps réel. - Le moteur de recherche est uniquement codé en
javascript
. Il n’est donc pas nécessaire d’interroger le serveur pour afficher les résultats d’une recherche.
Astuce
Pour chercher si une chaîne de caractères est présente dans le sujet d’une discussion, vous pouvez construire une expression régulière à l’aide de l’objet RegExp
:
var recherche = byId('recherche_discussion').value;
var regex = new RegExp(recherche, 'i');
// i est l'option pour que l'expression régulière ne soit pas sensible à la casse
- L’image à gauche du texte “créé par ...” peut être téléchargée à partir de ce lien.
- Vous pouvez utiliser le Hello World que nous avons précédemment créé pour avoir la base de votre page d’accueil. Il faut juste modifier la barre de navigation, le titre de la page et le nom de la base de données à laquelle vous voulez accéder.
- Pour le fichier EJS principal, vous pouvez garder
index.ejs
Avertissement
N’oubliez pas de spécifier la nouvelle base de données dans le fichier sql.js
var pool = mysql.createPool({
host : 'localhost',
user : 'root',
password : 'root',
// spécifier la base de données ici :
database : 'forum',
charset : 'UTF8_UNICODE_CI',
multipleStatements: true
});
Bon travail !
Correction¶
Code côté serveur¶
Pour que cette page soit affichée correctement, il faut routes.js
doit transmettre les donnéees des discussions qu’il a obtenues depuis la base de données. Voici la requête SQL
utilisée :
SELECT
COUNT(messages.id_message) AS "nombre_messages",
discussions.sujet,
discussions.id_discussion,
messages.nom_utilisateur AS "utilisateur_createur",
messages.date_ecriture
FROM discussions, messages
WHERE discussions.id_discussion = messages.id_discussion
AND messages.id_discussion = --ici s'ajoutera l'id de la discussion--
ORDER BY messages.date_ecriture
LIMIT 0,1
Important
Cette requête SQL
a besoin d’un élément (id_discussion
) lui permettant de charger toutes les informations nécessaires à l’affichage d’une discussion. Comme nous devons récupérer, au préalable, une liste contenant les identifiants de toutes les discussions, cela engendre un important “dialogue” entre le serveur et la base de données. Pour limiter ce problème, il est possible de réaliser des transactions. Nous n’aborderons pas ce sujet dans la création de cette application, mais notez qu’il serait tout à fait possible d’utiliser cette technique pour améliorer les performances de notre programme.
Pour récupérer toutes les informations dont nous avons besoin, nous devons exécuter cette requête SQL
autant de fois qu’il y a des discussions. Ces nombreuses requêtes pouvant surcharger le serveur, nous emploierons donc le ;
pour séparer les instructions SQL
et nous utiliserons une boucle afin d’ajouter des instructions à la requête principale. Pour créer la boucle, nous avons besoin des id
des discussions que nous souhaitons charger. Par ailleurs, il la requête doit être différente selon si nous voulons charger l’intégralité des discussions ou seulement la dernière. Pour différencier ces deux cas, nous utilisons la variable tout_charger
. Si elle est true
, la requête retourne tous les id
des discussions ; dans le cas contraire, seul le dernier id
de discussion apparaît.
if (tout_charger) {
var requete_sql = '\
SELECT id_discussion\
FROM discussions\
ORDER BY id_discussion';
}
else {
var requete_sql = '\
SELECT id_discussion\
FROM discussions\
ORDER BY id_discussion DESC\
LIMIT 0,1';
}
sql.requete(mysql, sql, requete_sql, function(results) {
// ...
Note
Les antislash qui se trouvent en fin de ligne permettent d’étendre une chaîne de caractères sur plusieurs lignes pour mieux structurer la requête.
Maintenant, nous avons les éléments nécessaires pour la boucle :
sql.requete(mysql, sql, requete_sql, function(results) {
var requete_sql = '';
for (var i = 0; i < results.length; i++) {
var requete_temp = '\
SELECT\
COUNT(messages.id_message) AS "nombre_messages",\
discussions.sujet,\
discussions.id_discussion,\
messages.nom_utilisateur AS "utilisateur_createur",\
messages.date_ecriture\
FROM discussions, messages\
WHERE discussions.id_discussion = messages.id_discussion\
AND messages.id_discussion = ' + results[i].id_discussion + '\
ORDER BY messages.date_ecriture\
LIMIT 0,1;';
requete_sql = requete_sql + requete_temp;
}
// ...
});
Avertissement
Ce code fonctionne, mais il serait vulnérable aux **injections **SQL
si les données à insérer dans la requête venaient de l’utilisateur. Nous étudirons ce que c’est et comment faire pour s’en prémunir au chapitre suivant.
Nous pouvons alors envoyer toute la requête SQL
à la base de données :
sql.requete(mysql, sql, requete_sql, function(results) {
if (tout_charger) {
callback(results);
} else {
// met la variable results dans un array pour que
les informations à traiter ensuite soient similaires
callback([results]);
}
});
Il ne reste plus qu’à mettre ce code dans une fonction qui puisse être appelée facilement depuis routes.js
:
exports.chargement_discussions = function(mysql, sql, tout_charger, callback) {
if (tout_charger) {
var requete_sql = '\
SELECT id_discussion\
FROM discussions\
ORDER BY id_discussion';
}
else {
var requete_sql = '\
SELECT id_discussion\
FROM discussions\
ORDER BY id_discussion DESC\
LIMIT 0,1';
}
sql.requete(mysql, sql, requete_sql, function(results) {
var requete_sql = '';
for (var i = 0; i < results.length; i++) {
var requete_temp = '\
SELECT\
COUNT(messages.id_message) AS "nombre_messages",\
discussions.sujet,\
discussions.id_discussion,\
messages.nom_utilisateur AS "utilisateur_createur",\
messages.date_ecriture\
FROM discussions, messages\
WHERE discussions.id_discussion = messages.id_discussion\
AND messages.id_discussion = ' + results[i].id_discussion + '\
ORDER BY messages.date_ecriture\
LIMIT 0,1;';
requete_sql = requete_sql + requete_temp;
}
sql.requete(mysql, sql, requete_sql, function(results) {
if (tout_charger) {
callback(results);
} else {
/* met la variable results dans un array pour que
les informations à traiter ensuite soient similaires */
callback([results]);
}
});
});
}
Puis, nous appelons cette fonction dans routes.js
:
// la page se charge aussi si nous entrons l'adresse http://localhost:port/home
app.get(/^\/home|\/$/, function(req, res) {
sql.chargement_discussions(mysql, sql, true, function(discussions) {
res.render('index.ejs', {
nom_utilisateur: session.session_active(req.session, req),
discussions: discussions
});
});
});
Code côté client¶
Vous constatez que le fichier principal a peu de lignes de code. Et pour cause, il utilise beaucoup de <% include %>
:
<!DOCTYPE html>
<html>
<head>
<% include balise_head %>
<script src="static/js/index.js"></script>
</head>
<body>
<% include barre_navigation %>
<div class="container">
<div class="row">
<div class="col-lg-offset-2 col-md-offset-1 col-md-10 col-lg-8 blanc">
<p style="padding-top:6px;">
<!-- bouton de recherche qui appelle la fonction rechercher()
lorsqu'on lâche une touche du clavier -->
<input
id="recherche_discussion"
placeholder="rechercher une discussion"
onkeyup="rechercher();"
class="form-control">
</input>
</p>
<p>
<span id="chargement_discussions">
<% include chargement_discussions %>
</span>
<!-- balise <span> qui servira a afficher d'éventuelles
nouvelles discussions en temps réel -->
<span id="retour_ajax"></span>
</p>
<div class="col-lg-offset-2 col-md-offset-1"></div>
</div>
</div>
</body>
</html>
Note
La fonction rechercher()
est appelée avec l’événement onkeyup
et non onkeydown
, car après onkeyup
, la propriété .value
de la balise est mise à jour avec la dernière lettre entrée et la recherche peut ainsi s’effectuer correctement.
Pour que cette page fonctionne, nous avons donc besoin d’un fichier supplémentaire, chargement_discussions.ejs
. Dans ce fichier, nous utiliserons l’array discussions
envoyée par routes.js
pour récupérer toutes les informations nécessaires à l’affichage des discussions.
<% convertir_date = function(date) {
var id_temp = Math.random().toString(36).substring(7); %>
<span id="<%= id_temp %>"></span>
<script>convertir_date("<%= date %>","<%= id_temp %>");</script>
<% }
for (var i = 0; i < discussions.length; i++) { %>
<div class="sujet_discussion" id="<%= discussions[i][0].id_discussion %>">
<table>
<tr>
<td style="width:50px;">
<img src="static/avatar.png" style="max-width:50%;">
</td>
<td>
<a href="<%= discussions[i][0].id_discussion %>" class="sujet_texte">
<%= discussions[i][0].sujet %>
</a><br>
<span class="infos_sujet">
créé par <a href="profil_<%= discussions[i][0].utilisateur_createur %>">
<%= discussions[i][0].utilisateur_createur %>
</a> le
<% convertir_date(discussions[i][0].date_ecriture); %>
</span>
<span class="nombre_messages">
<%= discussions[i][0].nombre_messages %>
<% if (discussions[i][0].nombre_messages === 1) { %> message <% } %>
<% if (discussions[i][0].nombre_messages > 1) { %> messages <% } %>
</span>
</td>
</tr>
</table>
</div>
<% } %>
Ici, nous faisons appel à une fonction, convertir_date()
, qui s’occupe de prendre les dates retournées par la base de données et les convertit en français. Comme cette fonction sera utilisable dans d’autres pages de cette application, nous pouvons créer un fichier qui y est dédié, conversion_dates.js
. En voici le contenu :
convertir_date = function(date, id_html) {
date = new Date(date);
var jour_mois = date.getDate();
var jour = date.getDay();
var mois = date.getMonth();
var annee = date.getFullYear();
var heure = date.getHours();
var minutes = date.getMinutes();
switch(jour) {
case 1: jour = 'lundi'; break;
case 2: jour = 'mardi'; break;
case 3: jour = 'mercredi'; break;
case 4: jour = 'jeudi'; break;
case 5: jour = 'vendredi'; break;
case 6: jour = 'samedi'; break;
case 0: jour = 'dimanche'; break;
}
switch(mois) {
case 0: mois = 'janvier'; break;
case 1: mois = 'février'; break;
case 2: mois = 'mars'; break;
case 3: mois = 'avril'; break;
case 4: mois = 'mai'; break;
case 5: mois = 'juin'; break;
case 6: mois = 'juillet'; break;
case 7: mois = 'août'; break;
case 8: mois = 'septembre'; break;
case 9: mois = 'octobre'; break;
case 10: mois = 'novembre'; break;
case 11: mois = 'décembre'; break;
}
if(/^[0-9]$/.test(minutes)) {
date_francais = jour+' '+jour_mois+' '+mois+' '+annee+' à '+heure+'h0'+minutes;
}
else {
date_francais = jour+' '+jour_mois+' '+mois+' '+annee+' à '+heure+'h'+minutes;
}
if(typeof(id_html) === 'undefined') {
return date_francais;
}
else {
byId(id_html).innerHTML = date_francais;
}
}
Bien entendu, nous ajoutons ce fichier dans la balise head
de index.ejs
pour que convertir_date()
soit disponible lorsque nous l’appelons :
<head>
<% include balise_head %>
<script src="static/js/index.js"></script>
<!-- ligne à ajouter : -->
<script src="static/js/conversion_dates.js"></script>
</head>
Pour afficher la dernière discussion, nous faisons une requête AJAX
. Il faut donc ajouter une route du nom de derniere_discussion
:
app.get('/derniere_discussion', function(req, res) {
/* ici, la variable tout_charger vaut false,
car nous ne voulons que la dernière discussion */
sql.chargement_discussions(mysql, sql, false, function(discussion) {
res.render('chargement_discussions.ejs', {
discussions: discussion
});
});
});
Nous insérons ensuite dans index.js
les fonctions qui servent à rechercher des discussions et faire en sorte que la page se raffraîchisse en temps réel :
socket.on('charger_derniere_discussion', function() {
var xhr = new XMLHttpRequest();
xhr.open('GET', 'derniere_discussion');
xhr.addEventListener('readystatechange', function() {
if (xhr.readyState === 4 && xhr.status === 200) {
byId('retour_ajax').innerHTML += xhr.responseText;
}
});
xhr.send(null);
});
function rechercher() {
byId('chargement_discussions').innerHTML = '';
byId('retour_ajax').innerHTML = '';
var recherche = byId('recherche_discussion').value;
var discussions = byClass('discussion');
for (var i = 0; i < discussions.length; i++) {
discussions[i].style.display = 'none';
}
for (var i = 0; i < discussions.length; i++) {
var sujet = discussions[i].getElementsByClassName('sujet_texte')[0].innerHTML;
var regex = new RegExp(recherche, 'i');
if (regex.test(sujet)) {
discussions[i].style.display = 'block';
}
}
}
Il ne reste plus qu’à régler le design de cette page à l’aide de quelques directives CSS :
body {
background-color: rgb(245, 245, 245);
}
.blanc {
background-color: rgb(255, 255, 255);
margin-top: -4px;
padding-top: 4px;
}
.sujet {
padding-top: 10px;
padding-bottom: 10px;
}
.sujet_texte {
font-size: 18px;
font-weight: bold;
position: relative;
top: -1px;
color:black;
}
.sujet_texte:hover {
text-decoration: underline;
color:black;
}
.infos_sujet {
position: relative;
top: 2px;
}
.nombre_messages {
position: relative;
top: 2px;
font-weight: bold;
}
.row {
position: relative;
top: -15px;
}
.gros_titre {
font-size:30px;
text-decoration:bold;
text-align:center;
margin-bottom: 25px;
}
.sujet_discussion {
font-size:30px;
text-decoration:bold;
text-align:center;
margin-bottom: 12px;
position: relative;
bottom: 5px;
}
Et voilà ! Nous obtenons une page d’accueil fonctionnelle !