Fermer

mars 23, 2020

Comment créer et structurer une application MVC Node.js –33 minutes de lecture

ES6 en action: let et const


Dans une application non triviale, l'architecture est aussi importante que la qualité du code lui-même. Nous pouvons avoir des morceaux de code bien écrits, mais si nous n'avons pas une bonne organisation, nous aurons du mal à mesure que la complexité augmente. Il n'est pas nécessaire d'attendre que le projet soit à mi-chemin pour commencer à penser à l'architecture; le meilleur moment est avant de commencer, en utilisant nos objectifs comme balises pour nos choix.

Node.js n'a pas de framework de facto avec des opinions fortes sur l'architecture et l'organisation du code de la même manière que Ruby a le framework Rails, pour exemple. En tant que tel, il peut être difficile de commencer à créer des applications Web complètes avec Node.

Dans ce didacticiel, nous allons créer les fonctionnalités de base d'une application de prise de notes en utilisant l'architecture MVC. Pour ce faire, nous allons utiliser le cadre Hapi.js pour Node.js et SQLite comme base de données, en utilisant Sequelize.js ainsi que d'autres petits utilitaires, pour accélérer notre développement. Nous allons construire les vues en utilisant Pug le langage de création de modèles.

Qu'est-ce que MVC?

Model-View-Controller (ou MVC) est probablement l'une des architectures les plus populaires pour les applications . Comme avec beaucoup d'autres choses intéressantes dans l'histoire de l'ordinateur le modèle MVC a été conçu au PARC pour le langage Smalltalk comme une solution au problème de l'organisation des applications avec des interfaces utilisateur graphiques. Il a été créé pour les applications de bureau, mais depuis lors, l'idée a été adaptée à d'autres supports, y compris le Web.

Nous pouvons décrire l'architecture MVC en termes simples:

  • Modèle : la partie de notre application qui traitera de la base de données ou de toute fonctionnalité liée aux données.
  • Voir : tout ce que l'utilisateur verra – essentiellement, les pages que nous allons envoyer au client.
  • Contrôleur : le la logique de notre site et la colle entre modèles et vues. Ici, nous appelons nos modèles pour obtenir les données, puis nous mettons ces données sur nos vues à envoyer aux utilisateurs.

Notre application nous permettra de créer, visualiser, éditer et supprimer des notes en texte brut. Il n'aura pas d'autres fonctionnalités, mais comme nous avons déjà une architecture solide définie, nous n'aurons pas beaucoup de mal à ajouter des choses plus tard.

Ce didacticiel suppose que vous avez une version récente de Node installée sur votre machine. Si ce n'est pas le cas, veuillez consulter notre tutoriel sur la mise en service avec Node .

Vous pouvez consulter l'application finale dans le accompagnant le référentiel GitHub afin que vous

Présentation de la fondation

La première étape lors de la création d'une application Node.js consiste à créer un fichier package.json qui contiendra tous les de nos dépendances et scripts. Au lieu de créer ce fichier manuellement, npm peut faire le travail pour nous en utilisant la commande init :

 mkdir notes-board
cd notes-board
npm init -y

Une fois le processus terminé, nous aurons un fichier package.json prêt à l'emploi.

Remarque: si vous n'êtes pas familier avec ces commandes, consultez notre Guide du débutant pour npm .

Nous allons procéder à l'installation de Hapi.js – le framework de choix pour ce tutoriel. Il offre un bon équilibre entre simplicité, stabilité et fonctionnalités qui fonctionneront bien pour notre cas d'utilisation (bien qu'il existe d'autres options qui fonctionneraient également très bien).

 npm install @ hapi / hapi @ 18.4.0

Cette commande va télécharger Hapi.js et l'ajouter à notre fichier package.json en tant que dépendance.

Remarque: Nous avons spécifié la version 18.4.0 de Hapi.js, comme il est compatible avec les versions 8, 10 et 12 de Node. Si vous utilisez Node 12, vous pouvez choisir d'installer la dernière version (Hapi v19.1.0).

Nous pouvons maintenant créer notre fichier d'entrée – le fichier serveur web qui va tout démarrer. Allez-y et créez un fichier server.js dans votre répertoire d'application et ajoutez-y le code suivant:

 "use strict";

const Hapi = require ("@ hapi / hapi");
const Settings = require ("./ settings");

const init = async () => {
  serveur const = nouveau Hapi.Server ({port: Settings.port});

  server.route ({
    méthode: "GET",
    chemin: "/",
    gestionnaire: (demande, h) => {
      retour "Bonjour, monde!";
    }
  });

  attendre server.start ();
  console.log (`Serveur fonctionnant à: $ {server.info.uri}`);
};

process.on ("unhandledRejection", err => {
  console.log (err);
  process.exit (1);
});

init ();

Ce sera le fondement de notre application.

Premièrement, nous indiquons que nous allons utiliser le mode strict qui est une pratique courante lors de l'utilisation le framework Hapi.js.

Ensuite, nous incluons nos dépendances et instancions un nouvel objet serveur où nous définissons le port de connexion sur 3000 (le port peut être n'importe quel nombre au-dessus de 1023 et au-dessous de 65535 ).

Notre premier itinéraire pour notre serveur fonctionnera comme un test pour voir si tout fonctionne, donc un "Bonjour, monde!" le message nous suffit. Dans chaque itinéraire, nous devons définir la méthode HTTP et le chemin (URL) auxquels il répondra, ainsi qu'un gestionnaire, qui est une fonction qui traitera la demande HTTP. La fonction de gestionnaire peut prendre deux arguments: demande et h . Le premier contient des informations sur l'appel HTTP et le second nous fournira des méthodes pour gérer notre réponse à cet appel.

Enfin, nous démarrons notre serveur avec la méthode server.start () .

Stockage de nos paramètres

Il est recommandé de stocker nos variables de configuration dans un fichier dédié. Ce fichier exporte un objet JSON contenant nos données, où chaque clé est affectée à partir d'une variable d'environnement – mais sans oublier une valeur de secours.

Dans ce fichier, nous pouvons également avoir des paramètres différents en fonction de notre environnement (comme le développement ou la production ). Par exemple, nous pouvons avoir une instance en mémoire de SQLite à des fins de développement, mais un véritable fichier de base de données SQLite en production.

La sélection des paramètres en fonction de l'environnement actuel est assez simple. Comme nous avons également une variable env dans notre fichier qui contiendra soit développement soit production nous pouvons faire quelque chose comme ceci pour obtenir les paramètres de la base de données: [19659023] const dbSettings = Paramètres [Settings.env] .db;

Ainsi dbSettings contiendra le réglage d'une base de données en mémoire lorsque la variable env est en développement ou contiendra le chemin d'un fichier de base de données lorsque la variable env est la production .

De plus, nous pouvons ajouter la prise en charge d'un fichier .env où nous pouvons stocker nos variables d'environnement localement à des fins de développement. . Ceci est accompli en utilisant un package comme dotenv pour Node.js, qui lira un fichier .env à la racine de notre projet et ajoutera automatiquement les valeurs trouvées à l'environnement. [19659003] Remarque: si vous décidez d'utiliser également un fichier .env assurez-vous d'installer le package avec npm install dotenv et de l'ajouter à .gitignore afin de ne pas publier d'informations sensibles.

Notre settings.js fichier ressemblera à ceci:

 // Ceci chargera notre fichier .env et ajoutera les valeurs à process.env,
// IMPORTANT: omettez cette ligne si vous ne souhaitez pas utiliser cette fonctionnalité
require ("dotenv"). config ({silent: true});

module.exports = {
  port: process.env.PORT || 3000,
  env: process.env.NODE_ENV || "développement",

  // Paramètres dépendants de l'environnement
  développement: {
    db: {
      dialecte: "sqlite",
      stockage: ": mémoire:"
    }
  },
  production: {
    db: {
      dialecte: "sqlite",
      stockage: "db / database.sqlite"
    }
  }
};

Nous pouvons maintenant démarrer notre application en exécutant la commande suivante et en naviguant vers http: // localhost: 3000 dans notre navigateur Web:

 node server.js

Remarque: ce projet a été testé sur Node v12.15.0. Si vous obtenez des erreurs, assurez-vous d'avoir une installation mise à jour.

Définition des itinéraires

La définition des itinéraires nous donne un aperçu des fonctionnalités prises en charge par notre application. Pour créer nos routes supplémentaires, il suffit de répliquer la structure de la route que nous avons déjà dans notre fichier server.js en changeant le contenu de chacune d'elles.

Commençons par créer un nouveau répertoire appelé lib dans notre projet. Ici, nous allons inclure tous les composants JS.

Dans lib créons un fichier routes.js et ajoutons le contenu suivant:

 "use strict" ;
const Path = require ("chemin");

module.exports = [
  // we’re going to define our routes here
];

Dans ce fichier, nous allons exporter un tableau d'objets qui contiennent chaque route de notre application. Pour définir la première route, ajoutez l'objet suivant au tableau:

 {
  méthode: "GET",
  chemin: "/",
  gestionnaire: (demande, h) => {
    return "Toutes les notes apparaîtront ici";
  },
  config: {
    description: "Obtient toutes les notes disponibles"
  }
},

Notre premier itinéraire est pour la page d'accueil ( / ), et comme il ne renverra que des informations, nous lui attribuons une méthode GET . Pour l'instant, il ne nous donnera que le message "Toutes les notes apparaîtront ici", que nous allons changer plus tard pour une fonction de contrôleur. Le champ de description dans la section config est uniquement à des fins de documentation.

Ensuite, nous créons les quatre itinéraires pour nos notes sous le chemin / note / . Puisque nous créons une application CRUD nous aurons besoin d'une route pour chaque action avec les méthodes HTTP correspondantes .

Ajoutez les définitions suivantes à côté de la route précédente: [19659023] {
  méthode: "POST",
  chemin: "/ note",
  gestionnaire: (demande, h) => {
    retourner "Nouvelle note";
  },
  config: {
    description: "Ajoute une nouvelle note"
  }
},
{
  méthode: "GET",
  chemin: "/ note / {slug}",
  gestionnaire: (demande, h) => {
    retourner "Ceci est une note";
  },
  config: {
    description: "Obtient le contenu d'une note"
  }
},
{
  méthode: "PUT",
  chemin: "/ note / {slug}",
  gestionnaire: (demande, h) => {
    retourner "Modifier une note";
  },
  config: {
    description: "Met à jour la note sélectionnée"
  }
},
{
  méthode: "GET",
  chemin: "/ note / {slug} / delete",
  gestionnaire: (demande, h) => {
    return "Cette note n'existe plus";
  },
  config: {
    description: "Supprime la note sélectionnée"
  }
}

Nous avons fait la même chose que dans la définition d'itinéraire précédente, mais cette fois, nous avons modifié la méthode pour qu'elle corresponde à l'action que nous voulons exécuter.

La seule exception est la route de suppression. Dans ce cas, nous allons le définir avec la méthode GET plutôt que DELETE et ajouter un / delete supplémentaire dans le chemin. De cette façon, nous pouvons appeler l'action de suppression simplement en visitant l'URL correspondante.

Remarque: si vous prévoyez d'implémenter une interface REST stricte, vous devrez alors utiliser la méthode DELETE et supprimer la partie / delete du chemin.

Nous pouvons nommer les paramètres du chemin en entourant le mot entre accolades. Puisque nous allons identifier les notes par un slug, nous ajoutons {slug} à chaque chemin, à l'exception de la route POST ; nous n'en avons pas besoin car nous n'allons pas interagir avec une note spécifique, mais en créer une.

Vous pouvez en savoir plus sur les itinéraires Hapi.js dans la documentation officielle . [19659003] Maintenant, nous devons ajouter nos nouvelles routes au fichier server.js . Importons le fichier de routes en haut du fichier:

 const Routes = require ("./ lib / routes");

Remplaçons ensuite notre itinéraire de test actuel par ce qui suit:

 server.route (Routes);

Construction des modèles

Les modèles nous permettent de définir la structure des données et toutes les fonctions pour les utiliser.

Dans cet exemple, nous allons utiliser la base de données SQLite avec Sequelize.js qui va nous fournir une meilleure interface en utilisant la technique ORM ( Object-Relational Mapping ). Il nous fournira également une interface indépendante de la base de données.

Configuration de la base de données

Vous pouvez installer SQLite et Sequelize en exécutant la commande suivante:

 npm install sequelize sqlite3

Créez maintenant un répertoire models à l'intérieur de lib / avec un fichier appelé index.js qui va contenir la base de données et la configuration Sequelize.js, et inclure le contenu suivant:

 "use strict";

const Fs = require ("fs");
const Path = require ("chemin");
const Sequelize = require ("sequelize");
const Settings = require ("../../ settings");
const dbSettings = Paramètres [Settings.env] .db;

const sequelize = new Sequelize (
  dbSettings.database,
  dbSettings.user,
  dbSettings.password,
  dbSettings
);
const db = {};

Fs.readdirSync (__ dirname)
  .filter (fichier => fichier.indexOf (".")! == 0 && fichier! == "index.js")
  .forEach (fichier => {
    const model = sequelize.import (Path.join (__ dirname, file));
    db [model.name] = modèle;
  });

db.sequelize = sequelize;
db.Sequelize = Sequelize;

module.exports = db;

Premièrement, nous incluons les modules que nous allons utiliser:

  • Fs pour lire les fichiers à l'intérieur du dossier models qui va contenir tous les modèles [19659072] Chemin pour joindre le chemin de chaque fichier du répertoire actuel
  • Sequelize ce qui nous permettra de créer une nouvelle instance de Sequelize
  • Paramètres qui contient les données de notre fichier settings.js de la racine de notre projet

Ensuite, nous créons une nouvelle variable sequelize qui contiendra une instance Sequelize avec nos paramètres de base de données pour le l'environnement actuel. Nous allons utiliser suite pour importer tous les modèles et les rendre disponibles dans notre objet db .

L'objet db va être exporté et contiendra nos méthodes de base de données pour chaque modèle. Il sera disponible dans notre application lorsque nous aurons besoin de faire quelque chose avec nos données.

Pour charger tous les modèles, au lieu de les définir manuellement, nous recherchons tous les fichiers dans le répertoire models (avec à l'exception du fichier index.js ) et chargez-les à l'aide de la fonction import . L'objet retourné nous fournira les méthodes CRUD, que nous ajouterons ensuite à l'objet db .

À la fin, nous ajouterons sequelize et Sequelize dans le cadre de notre objet db . Le premier va être utilisé dans notre fichier server.js pour se connecter à la base de données avant de démarrer le serveur, et le second est inclus pour plus de commodité si vous en avez également besoin dans d'autres fichiers.

Création de notre modèle de note

Dans cette section, nous allons utiliser le package Moment.js pour aider au formatage de la date. Vous pouvez l'installer et l'inclure comme dépendance avec la commande suivante:

 npm install moment

Nous allons créer un fichier note.js dans le répertoire models qui sera le seul modèle de notre application. Il nous fournira toutes les fonctionnalités dont nous avons besoin.

Ajoutez le contenu suivant à ce fichier:

 "use strict";

const Moment = require ("moment");

module.exports = (sequelize, DataTypes) => {
  const Note = sequelize.define ("Remarque", {
    Date: {
      type: DataTypes.DATE,
      get: function () {
        renvoie Moment (this.getDataValue ("date")). format ("MMMM Do, YYYY");
      }
    },
    title: DataTypes.STRING,
    slug: DataTypes.STRING,
    description: DataTypes.STRING,
    contenu: DataTypes.STRING
  });

  retourner la note;
};

Nous exportons une fonction qui accepte une instance séquentielle pour définir le modèle et un objet DataTypes avec tous les types disponibles dans notre base de données.

Ensuite, nous définissons la structure de nos données à l'aide d'un objet où chaque clé correspond à une colonne de base de données et la valeur de la clé définit le type de données que nous allons stocker. Vous pouvez voir la liste des types de données dans la documentation Sequelize.js . Les tables de la base de données vont être créées automatiquement sur la base de ces informations.

Dans le cas de la colonne date, nous définissons également comment Sequelize doit renvoyer la valeur à l'aide d'une fonction getter ( ]obtenez la clé ). Nous l'indiquons avant de renvoyer les informations. Il doit d'abord être transmis via l'utilitaire Moment pour être formaté de manière plus lisible ( MMMM Do, YYYY ).

Remarque: bien que nous obtenions un format simple et facile à utiliser. lire la chaîne de date, il est stocké en tant que produit de chaîne de date précise de l'objet Date de JavaScript. Ce n'est donc pas une opération destructrice.

Enfin, nous renvoyons notre modèle.

Synchronisation de la base de données

Nous devons maintenant synchroniser notre base de données avant de pouvoir l'utiliser dans notre application. Dans server.js importez les modèles en haut du fichier:

 // Importez le fichier index.js dans le répertoire models
const Models = require ("./ lib / models /");

Ensuite, supprimez le bloc de code suivant:

 attendent server.start ();
console.log (`Serveur fonctionnant à: $ {server.info.uri}`);

Remplacez-le par celui-ci:

 attendent Models.sequelize.sync ();

attendre server.start ();
console.log (`Serveur fonctionnant à: $ {server.info.uri}`);

Ce code va synchroniser les modèles avec notre base de données. Une fois cela fait, le serveur sera démarré.

Construction des contrôleurs

Les contrôleurs sont des fonctions qui acceptent la demande et réponse toolkit objets de Hapi.js. L'objet request contient des informations sur la ressource demandée, et nous utilisons reply pour renvoyer des informations au client.

Dans notre application, nous allons renvoyer uniquement un objet JSON pour l'instant, mais nous ajouterons les vues une fois que nous les aurons construites.

Nous pouvons considérer les contrôleurs comme des fonctions qui joignent nos modèles à nos vues; ils communiqueront avec nos modèles pour obtenir les données, puis les renverront dans une vue.

Le contrôleur domestique

Le premier contrôleur que nous allons construire gérera la page d'accueil de notre site. Créez un fichier home.js dans un répertoire lib / controllers avec le contenu suivant:

 "use strict";

const Models = require ("../ models /");

module.exports = async (request, h) => {
  résultat const = attendre Models.Note.findAll ({
    commande: [["date", "DESC"]]
  });

  revenir {
    Les données: {
      notes: résultat
    },
    page: "Accueil - Tableau d'affichage",
    description: "Bienvenue sur mon tableau de notes"
  };
};

Premièrement, nous obtenons toutes les notes dans notre base de données en utilisant la méthode findAll de notre modèle. Cette fonction renverra une promesse et, si elle se résout, nous obtiendrons un tableau contenant toutes les notes de notre base de données.

Nous pouvons organiser les résultats par ordre décroissant, en utilisant le paramètre order dans le paramètre objet d'options passé à la méthode findAll donc le dernier élément apparaîtra en premier. Vous pouvez vérifier toutes les options disponibles dans la documentation Sequelize.js .

Une fois que nous avons le contrôleur principal, nous pouvons éditer notre fichier routes.js . Tout d'abord, nous importons le module en haut du fichier, à côté de l'importation du module Path :

 const Home = require ("./ controllers / home");

Ensuite, nous ajoutons le contrôleur que nous venons de faire au tableau:

 {
  méthode: "GET",
  chemin: "/",
  gestionnaire: Accueil,
  config: {
    description: "Obtient toutes les notes disponibles"
  }
},

Vous pouvez vérifier que les choses fonctionnent à ce stade en redémarrant le serveur ( nœud server.js ) et en visitant http: // localhost: 3000 / . Vous devriez voir la réponse suivante:

 {
  "data": {"notes": []},
  "page": "Accueil - Tableau de notes",
  "description": "Bienvenue sur mon tableau de notes"
}

Boilerplate of the Note Controller

Puisque nous allons identifier nos notes avec un slug, nous pouvons en générer un en utilisant le titre de la note et la bibliothèque slug alors installons-le et l'inclure comme dépendance avec la commande suivante:

 npm install slug

Le dernier contrôleur que nous devons définir dans notre application nous permettra de créer, lire, mettre à jour et supprimer des notes.

Nous pouvons créer un fichier note.js à l'intérieur du répertoire lib / controllers et ajoutez le contenu suivant:

 "use strict";

const {Note} = require ("../ models /");
const Slugify = require ("slug");
const Path = require ("chemin");

module.exports = {
  // Ici, nous allons inclure nos fonctions qui gèreront les demandes restantes dans le fichier routes.js.
};

La fonction créer

Pour ajouter une note à notre base de données, nous allons écrire une fonction créer qui va envelopper la créer sur notre modèle en utilisant les données contenues dans l'objet de charge utile.

Ajoutez ce qui suit à l'intérieur de l'objet que nous exportons:

 create: async (request, h) => {
  résultat const = attendre Note.create ({
    date: nouvelle Date (),
    title: request.payload.noteTitle,
    slug: Slugify (request.payload.noteTitle, {lower: true}),
    description: request.payload.noteDescription,
    contenu: request.payload.noteContent
  });

  // Génère une nouvelle note avec les données «résultat»
  résultat de retour;
},

Une fois la note créée, nous récupérerons les données de la note et l'enverrons au client au format JSON à l'aide de la fonction de réponse .

Pour l'instant, nous renvoyons simplement le résultat, mais une fois nous construisons les vues dans la section suivante, nous pourrons générer le HTML avec la nouvelle note et l'ajouter dynamiquement sur le client. Bien que cela ne soit pas complètement nécessaire et dépendra de la façon dont vous allez gérer votre logique frontale, nous allons renvoyer un bloc HTML pour simplifier la logique sur le client.

Notez également que la date est généré à la volée lorsque nous exécutons la fonction, en utilisant new Date () .

La lire Fonction

Pour rechercher un seul élément, nous utilisons la findOne sur notre modèle. Puisque nous identifions les notes par leur slug, le filtre doit contenir le slug fourni par le client dans l'URL ( http: // localhost: 3000 / note /: slug: ):

 lire: async (demande, h) => {
  const note = attendre Note.findOne ({
    où: {
      slug: request.params.slug
    }
  });

  note de retour;
},

Comme dans la fonction précédente, nous allons simplement renvoyer le résultat, qui va être un objet contenant les informations de note. Les vues seront utilisées une fois que nous les aurons construites dans la section Création des vues .

La mise à jour Fonction

Pour mettre à jour une note, nous utilisons la mise à jour de la méthode sur notre modèle. Il faut deux objets – les nouvelles valeurs que nous allons remplacer et les options contenant un filtre avec le slug de note, qui est la note que nous allons mettre à jour:

 update : async (demande, h) => {
  const values ​​= {
    title: request.payload.noteTitle,
    description: request.payload.noteDescription,
    contenu: request.payload.noteContent
  };

  const options = {
    où: {
      slug: request.params.slug
    }
  };

  attendre Note.update (valeurs, options);
  résultat const = attendre Note.findOne (options);

  résultat de retour;
},

Après la mise à jour de nos données, étant donné que notre base de données ne renvoie pas la note mise à jour, nous pouvons retrouver la note modifiée pour la renvoyer au client, afin que nous puissions afficher la version mise à jour dès que les modifications sont apportées. [19659136] La fonction delete

Le contrôleur de suppression supprimera la note en fournissant le slug à la fonction destroy de notre modèle. Ensuite, une fois la note supprimée, nous redirigeons vers la page d'accueil. Pour ce faire, nous utilisons la fonction de redirection de la boîte à outils de réponse de Hapi:

 delete: async (request, h) => {
  attendre Note.destroy ({
    où: {
      slug: request.params.slug
    }
  });

  return h.redirect ("/");
}

Utilisation du contrôleur de notes dans nos itinéraires

À ce stade, nous devrions avoir notre fichier de contrôleur de notes prêt avec toutes les actions CRUD. Mais pour les utiliser, nous devons l'inclure dans notre fichier routes.

D'abord, importons notre contrôleur en haut du fichier routes.js :

 const Note = require (". / contrôleurs / note ");

Nous devons remplacer chaque gestionnaire par nos nouvelles fonctions, nous devrions donc avoir notre fichier de routes comme suit:

 {
  méthode: "POST",
  chemin: "/ note",
  gestionnaire: Note.create,
  config: {
    description: "Ajoute une nouvelle note",
    charge utile: {
      multipart: true,
    }
  }
},
{
  méthode: "GET",
  chemin: "/ note / {slug}",
  gestionnaire: Note.read,
  config: {
    description: "Obtient le contenu d'une note"
  }
},
{
  méthode: "PUT",
  chemin: "/ note / {slug}",
  gestionnaire: Note.update,
  config: {
    description: "Met à jour la note sélectionnée",
    charge utile: {
      multipart: true,
    }
  }
},
{
  méthode: "GET",
  chemin: "/ note / {slug} / delete",
  gestionnaire: Note.delete,
  config: {
    description: "Supprime la note sélectionnée"
  }
}

Remarque: nous incluons nos fonctions sans () à la fin, car nous référençons nos fonctions sans les appeler.

Dans Hapi v19, [19659147] request.payload.multipart a été remplacé par false par défaut . Nous devons la remettre à true pour les itinéraires POST et PUT car nous utiliserons un objet FormData pour transmettre données vers le serveur, et les données transmises seront au format multipart / form-data .

Création des vues

À ce stade, notre site reçoit des appels HTTP et répond avec des objets JSON . Pour le rendre utile à tout le monde, nous devons créer les pages qui rendent nos informations d'une manière agréable.

Dans cet exemple, nous allons utiliser le Pug (anciennement Jade) langage de modélisation, bien que ce ne soit pas obligatoire, et nous pouvons utiliser d'autres langues avec Hapi.js. Nous allons utiliser le plugin Vision pour activer la fonctionnalité d'affichage sur notre serveur.

Remarque: si vous n'êtes pas familier avec Jade / Pug, consultez notre Guide du débutant vers Pug .

Vous pouvez installer les packages avec la commande suivante:

 npm install @ hapi / vision @ 5.5.4 pug

Ici, nous installons la v5.5.4 du plugin vision, qui est compatible avec Hapi v18. Si vous avez choisi d'installer Hapi v19, vous pouvez simplement taper npm i @ hapi / vision pour récupérer la dernière version.

Le composant Note

Premièrement, nous allons construire le composant de note qui va être réutilisé dans nos vues. De plus, nous allons utiliser ce composant dans certaines de nos fonctions de contrôleur pour créer une note à la volée à l'arrière pour simplifier la logique sur le client.

Créez un fichier dans lib / views / components appelé note.pug avec le contenu suivant:

 article.content
  h2.title: a (href = `/ note / $ {note.slug}`) = note.title
  p.subtitle.is-6 Publié le # {note.date}
  p = note.content

Il est composé du titre de la note, de la date de publication et du contenu de la note.

La mise en page de base

La mise en page de base contient les éléments communs de nos pages – ou en d'autres termes, pour notre exemple , tout ce qui n'est pas contenu. Créez un fichier dans lib / views / appelé layout.pug avec le contenu suivant:

 doctype html
tête
  méta (charset = 'utf-8')
  meta (name = 'viewport' content = 'width = device-width, initial-scale = 1')
  title = page
  méta (nom = 'description' contenu = description)
  lien (rel = 'stylesheet' href = 'https: //cdn.jsdelivr.net/npm/bulma@0.8.0/css/bulma.min.css')
  script (defer = '' src = 'https: //use.fontawesome.com/releases/v5.3.1/js/all.js')
corps
  bloquer le contenu
  script (src = '/ scripts / main.js')

Le contenu des autres pages sera chargé à la place de contenu de bloc . Notez également que nous afficherons une variable de page dans l'élément title et une variable description dans l'élément meta (name = 'description') . Nous créerons ces variables dans nos itinéraires plus tard.

À des fins de style, nous incluons le framework CSS Bulma et Font Awesome à partir d'un CDN. Nous incluons également un fichier main.js au bas de la page, qui contiendra tout notre code JavaScript personnalisé pour le frontal. Veuillez créer ce fichier maintenant dans un répertoire statique / public / scripts / .

La vue d'accueil

Sur notre page d'accueil, nous afficherons une liste de toutes les notes de notre base de données et un bouton qui affichera une fenêtre modale avec un formulaire qui nous permet de créer une nouvelle note via Ajax.

Créez un fichier dans lib / views appelé home.pug avec ce qui suit contenu:

 étend la mise en page

bloquer le contenu
  section.section
    .récipient

      h1.title.has-text-centred
        | Tableau de notes

      .tabs.is centré
        ul
          li
            a.show-modal (href = '#') Publier

      main (container) .notes-list
        chaque note dans data.notes
          inclure des composants / note
          heure

      .modal
        .modal-background
        .modal-card
          header.modal-card-head
            p.modal-card-title Ajouter une note
            button.delete (aria-label = 'close')
          section.modal-card-body
            form (action = '/ note' method = 'POST'). note-form # note-form
              .champ
                .contrôle
                  input.input (name = 'noteTitle' type = 'text' placeholder = 'Title')
              .champ
                .contrôle
                  input.input (name = 'noteDescription' type = 'text' placeholder = 'Short description')
              .champ
                .contrôle
                  textarea.textarea (name = 'noteContent' placeholder = 'Contents')
              .champ
                .contrôle
                  button.button.is-link Enregistrer

La vue Note

La page de note est assez similaire à la page d'accueil, mais dans ce cas, nous montrons un menu avec des options spécifiques à la note actuelle, le contenu de la note et la même forme que dans la maison page, mais avec les informations de note actuelles déjà remplies, elles sont donc là lorsque nous les mettons à jour.

Créez un fichier dans lib / views appelé note.pug avec le contenu suivant:

 étend la disposition

bloquer le contenu
  section.section
    .récipient
      h1.title.has-text-centred
          | Tableau de notes

      .tabs.is centré
        ul
          li: a (href = '/') Accueil
          li: a.show-modal(href='#') Update
          li: a(href=`/note/${note.slug}/delete`) Delete

      include components/note

      .modal
        .modal-background
        .modal-card
          header.modal-card-head
            p.modal-card-title Edit note
            button.delete(aria-label='close')
          section.modal-card-body
            form(action=`/note/${note.slug}` method='PUT').note-form#note-form
              .field
                .control
                  input.input(name='noteTitle' type='text' placeholder='Title' value=note.title)
              .field
                .control
                  input.input(name='noteDescription' type='text' placeholder='Short description' value=note.description)
              .field
                .control
                  textarea.textarea(name='noteContent' placeholder='Contents') #{note.content}
              .field
                .control
                  button.button.is-link Save

The JavaScript on the Client

To create and update notes, we’ll use some JavaScript, both to show/hide a modal with a form, and to submit the requests via Ajax. Although this is not strictly necessary, we feel it provides a better experience for the user.

This is the content of our main.js file in the static/public/scripts/ directory:

// Modal

const modal = document.querySelector(".modal");
const html = document.querySelector("html");

const showModal = () => {
  modal.classList.add("is-active");
  html.classList.add("is-clipped");
};

const hideModal = () => {
  modal.classList.remove("is-active");
  html.classList.remove("is-clipped");
};

document.querySelector("a.show-modal").addEventListener("click", function(e) {
  e.preventDefault();
  showModal();
});

modal.querySelector(".modal .delete").addEventListener("click", function(e) {
  e.preventDefault();
  hideModal();
});

// Form submition

const form = document.querySelector("#note-form");
const url = form.getAttribute("action");
const method = form.getAttribute("method");

const prependNote = html => {
  const notesList = document.querySelector(".notes-list");
  const div = document.createElement("div");
  div.innerHTML = html;
  notesList.insertBefore(div.firstChild, notesList.firstChild);
};

const updateNote = html => {
  const article = document.querySelector("article");
  const div = document.createElement("div");
  div.innerHTML = html;
  article.parentNode.replaceChild(div.firstChild, article);
};

const onSuccess = html => {
  hideModal();
  form.reset();

  if (method === "POST") {
    prependNote(html);
  } else if (method === "PUT") {
    updateNote(html);
  }
};

form.addEventListener("submit", e => {
  e.preventDefault();

  fetch(url, {
    method,
    body: new FormData(form)
  })
    .then(response => response.text())
    .then(text => onSuccess(text))
    .catch(error => console.error(error));
});

Every time the user submits the form in the modal window, we get the information from the form elements and send it to our back end, depending on the action URL and the method (POST or PUT). Then, we’ll get the result as a block of HTML containing our new note data. When we add a note, we’ll just add it on top of the list on the home page, and when we update a note we replace the content for the new one in the note view.

Adding Support for Views on the Server

To make use of our views, we have to include them in our controllers and add the required settings.

In our server.js file, let’s import the Node Path utility at the top of the file, since we’re using it in our code to indicate the path of our views:

const Path = require("path");

Now, replace the server.route(Routes); line with the following code block:

await server.register([require("@hapi/vision")]);

server.views({
  engines: { pug: require("pug") },
  path: Path.join(__dirname, "lib/views"),
  compileOptions: {
    pretty: false
  },
  isCached: Settings.env === "production"
});

// Add routes
server.route(Routes);

In the code we’ve added, we first register the Vision plugin with our Hapi.js server, which is going to provide the view functionality. Then we add the settings for our views — like the engine we’re going to use and the path where the views are located. At the end of the code block, we add our routes back in.

This will make our views work on the server, but we still have to declare the view that we’re going to use for each route.

Setting the Home View

Open the lib/controllers/home.js file and replace the return statement with the following:

return h.view('home', {
  data: {
    notes: result
  },
  page: 'Home — Notes Board',
  description: 'Welcome to my Notes Board'
});

After registering the Vision plugin, we now have a view method available on the reply object. We’re going to use it to select the home view in our views directory and to send the data that’s going to be used when rendering the views.

In the data that we provide to the view, we also include the page title and a meta description for search engines.

If you’d like to try things out at this point, head to http://localhost:3000/. You should see a nicely styled notes board, with a Publish button that doesn’t do anything.

Setting the Note View: create Function

Right now, every time we create a note we send a JSON object from the server to the client. But since we’re doing this process with Ajax, we can send the new note as HTML ready to be added to the page. To do this, we render the note component with the data we have.

Start off by requiring Pug at the top of the controllers/note.js file:

const Pug = require("pug");

Then, in the create method, replace the line return result; with the following code block:

// Generate a new note with the 'result' data
return Pug.renderFile(
  Path.join(__dirname, "../views/components/note.pug"),
  {
    note: result
  }
);

We use the renderFile method from Pug to render the note template with the data we just received from our model.

Setting the Note View: read Function

When we enter a note page, we should get the note template with the content of our note. To do this, we have to replace the read function’s return note; line with this:

return h.view("note", {
  note,
  page: `${note.title} — Notes Board`,
  description: note.description
});

As with the home page, we select a view as the first parameter and the data that we’re going to use as the second one.

Setting the Note View: update Function

Every time we update a note, we’ll reply similarly to when we create new notes. Replace the return result; line in the update function with the following code:

// Generate a new note with the updated data
return Pug.renderFile(
  Path.join(__dirname, "../views/components/note.pug"),
  {
    note: result
  }
);

Note: the delete function doesn’t need a view, since it will just redirect to the home page once the note is deleted.

Serving Static Files

The JavaScript and CSS files that we’re using on the client side are provided by Hapi.js from the static/public/ directory. But it won’t happen automatically; we have to indicate to the server that we want to define this folder as public. This is done using the Inert package, which you can install with the following command:

npm install @hapi/inert

In the server.register function inside the server.js file, import the Inert plugin and register it with Hapi like this:

await server.register([require("@hapi/vision"), require("@hapi/inert")]);

Now we have to define the route where we’re going to provide the static files, and their location on our server’s filesystem. Add the following entry at the end of the exported object in routes.js:

{
  // Static files
  method: "GET",
  path: "/{param*}",
  handler: {
    directory: {
      path: Path.join(__dirname, "../static/public")
    }
  },
  config: {
    description: "Provides static resources"
  }
}

This route will use the GET method, and we’ve replaced the handler function with an object containing the directory that we want to make public.

You can find more information about serving static content in the Hapi.js documentation.

Conclusion

At this point, we have a very basic Hapi.js application using the MVC architecture. Although there are still things we should take care of before putting our application in production (such as input validation, error handling, error pages, and so on) this should work as a foundation to learn and build your own applications.

If you’d like to take this example a bit further, after finishing all the small details (not related the architecture) to make this a robust application, you could implement an authentication system so only registered users are able to publish and edit notes. But your imagination is the limit, so feel free to fork the application repository and go to town!

Dive deeper into Node.js with further reading:



Source link