Exemple de SPA Simple Notes sur Mithril.js

Mithril.js est un outil impopulaire pour créer des applications Web clientes. Il n'y a pratiquement aucune publication sur Habré à ce sujet. Dans cet article, je veux montrer comment vous pouvez faire une petite application sur Mithril. La candidature sera basée sur cette publication ( traduction )

Qu'est-ce que Mihtril


Mithril est un framework réactif conçu pour créer des SPA (applications web à page unique). Dans l'ensemble, c'est juste du javascript et 13 signatures de fonctions API. De plus, il existe une bibliothèque de flux de mithril qui n'est pas incluse dans le mithril et est utilisée séparément. Le cœur du mithril comprend le routage de l'application et l'utilisation des demandes XHR. Le concept central est l'abstraction - un nœud virtuel (vnode). Un nœud virtuel est juste un objet js avec un ensemble d'attributs. Les nœuds virtuels sont créés par la fonction spéciale m (). L'état actuel de l'interface est stocké dans une liste de nœuds virtuels (DOM virtuel). Lors du rendu initial de la page d'application, le DOM virtuel est traduit dans le DOM. Lorsque le gestionnaire d'événements DOM API démarre, lorsque la promesse m.request () est terminée et lorsque l'URL change (navigation le long des itinéraires de l'application), un nouveau tableau DOM virtuel est généré,se compare à l'ancien, et les nœuds modifiés changent le DOM du navigateur. En plus des événements DOM et de l'achèvement de la demande m.request (), le redessin peut être appelé manuellement avec la fonction m.redraw ().

Il n'y a pas de modèles HTML en mithril prêts à l'emploi, il n'y a pas de support JSX, bien que vous puissiez utiliser tout cela avec différents plugins pour construire si vous le souhaitez. Je n'utiliserai pas ces opportunités ici.

Si le premier argument de m () est une chaîne (par exemple, 'div'), la fonction renvoie un simple nœud virtuel et, par conséquent, une balise HTML sera affichée dans le DOM

<div></div>

Si le premier argument de m () est l'objet ou la fonction qui renvoie l'objet, alors un tel objet doit avoir une méthode view (), et un tel objet est appelé un composant. La méthode view () du composant, à son tour, doit toujours renvoyer la fonction m () (ou un tableau du type: [m (),]). Ainsi, nous pouvons construire une hiérarchie d'objets composants. Et il est clair qu'en fin de compte, tous les composants renvoient de simples nœuds vnode.

Les nœuds virtuels et les composants ont des méthodes de cycle de vie, et ils sont appelés les mêmes oninit (), oncreate (), onbeforeupdate (), etc. Chacune de ces méthodes est appelée à un moment très précis du rendu de la page.

Vous pouvez transmettre des paramètres au nœud virtuel ou au composant en tant qu'objet, qui doit être le deuxième argument de la fonction m (). Vous pouvez obtenir un lien vers cet objet à l'intérieur du nœud en utilisant la notation vnode.attrs. Le troisième argument de la fonction m () est la descendance de ce nœud et est accessible via le lien vnode.children. En plus de la fonction m (), des nœuds simples sont renvoyés par la fonction m.trust ().

L'auteur de mithril n'offre aucun modèle de conception d'application spécial, bien qu'il conseille d'éviter certaines décisions infructueuses, par exemple, des composants «trop épais» ou la manipulation des descendants de l'arborescence des composants. L'auteur ne propose pas non plus de méthodes ou de méthodes spéciales pour contrôler l'état de l'application dans son ensemble ou des composants. Bien que la documentation indique que vous ne devez pas utiliser l'état du nœud lui-même, manipulez-le.

Toutes ces fonctionnalités du mithril semblent très gênantes et le cadre semble inachevé, il n'y a pas de recommandations spéciales, il n'y a pas d'état / de stockage, il n'y a pas de réducteur / répartiteur d'événements, pas de modèles. En général, faites comme vous le pouvez.

Ce dont vous avez besoin pour un exemple


Nous utiliserons:


Le serveur frontal n'est pas important ici, il suffit de donner au client index.html et les fichiers de script et de style.

Nous n'installerons pas de mithril dans node_modules, et lierons le code d'application et le framework dans un seul fichier. Le code d'application et le mithril seront téléchargés sur la page séparément.

Je ne décrirai pas la procédure d'installation des outils, bien que je puisse dire à propos de postgREST, téléchargez simplement le fichier binaire, placez-le dans un dossier séparé, créez un fichier de configuration test.conf comme ceci:

db-uri = "postgres://postgres:user1@localhost:5432/testbase"
server-port= 5000
# The name of which database schema to expose to REST clients
db-schema= "public"
# The database role to use when no client authentication is provided.
# Can (and probably should) differ from user in db-uri
db-anon-role = "postgres" 

En même temps, votre cluster postgesql devrait avoir une base de test et user1. Dans cette base de test, créez une table:

-- Postgrest sql notes table 
create table if not exists notes (
id serial primary key,
title varchar(127) NOT NULL,
content text NOT NULL,
created timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
completed timestamp with time zone,
ddel smallint default 0
)

Le démarrage du serveur postgREST se fait avec la commande:

postgrest test.conf

Après avoir démarré le serveur, il affiche des messages d'information sur la connexion à la base de données et sur le port sur lequel il écoutera les clients.

Planifier un projet


Donc, si vous comprenez à peu près comment fonctionne Mitril, vous devez comprendre comment faire la demande. Voici le plan:

  1. Nous allons stocker les données d'application dans un objet local, appelons son modèle
  2. Les applications API seront stockées dans un fichier séparé
  3. Nous stockons les itinéraires d'application dans un fichier séparé
  4. Le fichier de chemin sera le point d'entrée pour la construction de l'application.
  5. Chaque composant séparé (et j'utiliserai le schéma de rendu des composants) et les fonctions qui lui sont associées seront stockés dans un fichier séparé
  6. Chaque composant qui restitue les données du modèle aura accès au modèle.
  7. Les gestionnaires d'événements DOM du composant sont localisés dans le composant

Ainsi, nous n'avons pas besoin d'événements personnalisés, plutôt d'événements DOM natifs, les rappels de ces événements sont localisés dans les composants. J'utiliserai une reliure bidirectionnelle. Peut-être que tout le monde n'aime pas cette approche, mais tout le monde n'aime pas redux ou vuex. De plus, la technique de liaison unidirectionnelle peut également être mise en œuvre avec élégance dans le mitril en utilisant du fil de mithril. Mais dans ce cas, il s'agit d'une solution redondante.

Structure du dossier d'application




Le dossier public sera servi par le serveur frontal, il y a un fichier index.html et des répertoires avec des styles et des scripts.

Le dossier src contient la racine du routeur et la définition de l'API, ainsi que deux répertoires, pour le modèle et les vues.

À la racine du projet, il y a un fichier de configuration rollup.config, et le projet est construit à l'aide de la commande:

rollup –c

Afin de ne pas ennuyer le lecteur avec les longs morceaux de code disponibles sur github.com, je ne commenterai que les principaux éléments de mise en œuvre pour démontrer une approche idiomatique pour mitral.

API et routeur


Code API:

// used by backend server
export const restApi= {
  notes: { url: 'notes', editable: ['add', 'edit', 'del'] }
}

// used by routers
export const appApi = {
  root: "/",
}
// used by navBar
// here array of arrays though it may be hash eg
export const appMenu = [
  [`${appApi.root}`, 'Home'],
  [`${appApi.root}`, 'About'],
  [`${appApi.root}`, 'Contacts'],
]

J'ai défini une API pour le serveur REST et pour le routeur.

Routeur:

import { restApi, appApi } from './appApi';
import { moModel } from './model/moModel';
import { vuView, vuApp } from './view/vuApp';
import { vuNavBar } from './view/vuNavBar';

// application router
const appRouter = { [appApi.root]: {
  render() {
    const view = m(vuApp, {
      model: moModel.getModel( restApi.notes ),
    });
    return vuView( {menu: vuNavBar}, view);
  }
}};

// once per app
m.route(document.body, "/", appRouter);

Ici, le routeur est monté sur le corps du document. Le routeur lui-même est un objet qui décrit les routes et les composants qui seront utilisés sur cette route, la fonction render () devrait retourner vnode.

L'objet appApi définit tous les itinéraires d'application valides et l'objet appMenu définit tous les éléments de navigation possibles pour l'application.

La fonction de rendu, lorsqu'elle est appelée, génère un modèle d'application et le transmet au nœud racine.

Modèle d'application


La structure qui stocke les données pertinentes, j'ai appelé le modèle. Le code source de la fonction qui renvoie le modèle:

getModel(
    {url=null, method="GET", order_by='id', editable=null} = {}
  ) {
/**
  * url - string of model's REST API url
  * method - string of model's REST method
  * order_by - string "order by" with initially SELECT 
  * editable - array defines is model could changed
*/
    const model = {
      url: url,
      method: method,
      order_by: order_by,
      editable: editable,
      key: order_by, // here single primary key only
      list: null, // main data list 
      item: {}, // note item
      error: null, // Promise error
      save: null, // save status
      editMode: false, // view switch flag
      word: '' // dialog header word
    };  
    model.getItem= id => {
      model.item= {};
      if ( !id ) {
        model.editMode= true;
        return false;
      }
      const key= model.key;
      for ( let it of model.list ) {
        if (it[key] == id) {
          model.item= Object.assign({}, it);
          break;
        }
      }
      return false;
    };
    return model;
  },

Ici, il est initialisé et retourne l'objet modèle. Les liens à l'intérieur de l'objet peuvent changer, mais le lien vers l'objet lui-même reste constant.

En plus de la fonction getModel, l'objet global moModel a des fonctions wrapper pour la fonction mitry m.request (), ce sont getList (modèle) et formSubmit (événement, modèle, méthode). Le paramètre modèle est en fait une référence à l'objet modèle, événement est l'objet événement qui est généré lorsque le formulaire est soumis, méthode est la méthode HTTP avec laquelle nous voulons enregistrer la note (POST est une nouvelle note, PATCH, DELETE est ancien).

Représentation


Le dossier de vue contient des fonctions qui sont responsables du rendu des éléments de page individuels. Je les ai divisés en 4 parties:

  • vuApp - le composant racine de l'application,
  • vuNavBar - barre de navigation,
  • vuNotes - une liste de notes,
  • vuNoteForm - formulaire d'édition de notes,
  • vuDialog - élément de dialogue HTML

Le routeur détermine que vuView (menu, vue) est renvoyé sur un seul itinéraire

Définition de cette fonction:

export const vuView= (appMenu, view)=> m(vuMain, appMenu, view);

Il s'agit simplement d'un wrapper qui renvoie le composant vuMain, si l'objet appMenu est suffisamment complexe pour avoir des objets imbriqués dans une structure similaire à un objet externe, alors un tel wrapper est un moyen approprié de renvoyer un composant avec différents éléments de navigation et composants enfants (il vous suffit d'écrire moins de code) .

Composant VuMain:

const vuMain= function(ivnode) {
  // We use ivnode as argument as it is initial vnode
  const { menu }= ivnode.attrs;
  return { view(vnode) {
    // IMPORTANT !
    // If we use vnode inside the view function we MUST provide it for view
    return [
      m(menu),
      m('#layout', vnode.children)
    ];
  }};
};

Cela renvoie simplement les vnodes de la navigation et le contenu réel de la page.

Ci-après, là où il est possible de définir un composant, j'utiliserai des fermetures. Les fermetures sont appelées une fois lors du rendu initial de la page et stockent localement tous les liens transmis aux objets et les définitions de leurs propres fonctions.

La fermeture en tant que définition doit toujours renvoyer un composant.

Et en fait le composant de contenu d'application:

export const vuApp= function(ivnode) {
  const { model }= ivnode.attrs;
  //initially get notes
  moModel.getList( model );
  
  return { view() { 
    return [
      m(vuNotes, { model }),
      m(vuNoteForm, { model }),
      vuModalDialog(model)
    ];
  }};
}

Puisque nous avons déjà un modèle, lorsque j'appelle une fermeture, je veux obtenir la liste complète des notes de la base de données. Il y aura trois composants sur la page:

  • vuNotes - une liste de notes avec le bouton ajouter,
  • vuNoteForm - formulaire d'édition de notes,
  • vuModalDialog - un élément de dialogue que nous afficherons de façon modale, et claquerons si nécessaire.

Étant donné que chacun de ces composants doit savoir comment se dessiner, nous passons un lien vers l'objet modèle dans chacun.

Liste des notes sur les composants:

//Notes List
export const vuNotes= function(ivnode) {
  const { model }= ivnode.attrs;
  const _display= ()=> model.editMode ? 'display:none': 'display:block';
  const vuNote= noteItem(model); // returns note function
  
  return { view() {
    return model.error ? m('h2', {style: 'color:red'}, model.error) :
    !model.list ? m('h1', '...LOADING' ) :
    m('div', { style: _display() }, [
      m(addButton , { model } ),
      m('.pure-g', m('.pure-u-1-2.pure-u-md-1-1',
        m('.notes', model.list.map( vuNote ) )
      ))
    ]);
  }};
}

L'indicateur bool editMode est stocké dans l'objet modèle; si la valeur de l'indicateur est vraie, alors nous montrons le formulaire d'édition, sinon - une liste de notes. Vous pouvez augmenter le test d'un niveau plus haut, mais le nombre de nœuds virtuels et les nœuds DOM réels changeraient chaque fois que l'indicateur est commuté, ce qui est un travail inutile.

Ici, nous sommes idiomatiques pour mitril, nous générons une page, vérifiant la présence ou l'absence d'attributs dans le modèle à l'aide d'opérateurs ternaires.

Voici la fermeture qui renvoie la fonction d'affichage des notes:

const noteItem= model=> {
  // click event handler
  const event= ( msg, word='', time=null)=> e=> {
    model.getItem(e.target.getAttribute('data'));
    if ( !!msg ) {
      model.save= { err: false, msg: msg };
      model.word= word;
      if ( !!time )
        model.item.completed= time;
      vuDialog.open();
    } else {
      model.editMode= true;
    }
  };
  // trash icon's click handler
  const _trash= event('trash', 'Dlelete');
  
  // check icon's click handler
  const _check= event('check', 'Complete',
    // postgre timestamp string
    new Date().toISOString().split('.')[0].replace('T', ' '));
  
  // edit this note
  const _edit= event('');
  
  const _time= ts=> ts.split('.')[0];
  
  // Single Note 
  const _note= note=> m('section.note', {key: note.id}, [
    m('header.note-header', [ m('p.note-meta', [
      // note metadata
      m('span', { style: 'padding: right: 3em' }, `Created: ${_time( note.created )}`),
      note.completed ? m('span', `  Completed: ${_time( note.completed )}`) : '', 
      // note right panel 
      m('a.note-pan', m('i.fas.fa-trash', { data: note.id, onclick: _trash } )),
      note.completed ? '' : [
        m('a.note-pan', m('i.fas.fa-pen', {data: note.id, onclick: _edit } )),
        m('a.note-pan', m('i.fas.fa-check', { data: note.id, onclick: _check} ))
      ]
    ]),
      m('h2.note-title', { style: note.completed ? 'text-decoration: line-through': ''}, note.title)
    ]),
    m('.note-content', m('p', note.content))
  ]);
  return _note;
}

Tous les gestionnaires de clics sont définis localement. Le modèle ne sait pas comment l'objet note est structuré, je n'ai défini que la propriété key, avec laquelle nous pouvons sélectionner l'élément souhaité dans le tableau model.list. Cependant, le composant doit savoir exactement comment l'objet qu'il dessine est structuré.

Je ne donnerai pas le texte complet du code du formulaire pour modifier la note; nous examinons simplement le gestionnaire de soumission de formulaire séparément:

// form submit handler
  const _submit= e=> {
    e.preventDefault();
    model.item.title= clean(model.item.title);
    model.item.content= clean(model.item.content);
    const check= check_note(model.item);
    if ( !!check ) {
      model.save= { err: true, msg: check }; 
      model.word= 'Edit';
      vuDialog.open();
      return false;
    } 
    return moModel.formSubmit(e, model, _method() ).then(
      ()=> { model.editMode=false; return true;}).catch(
      ()=> { vuDialog.open(); return false; } );
  };

Étant donné que la définition se produit dans la fermeture, nous avons un lien vers le modèle et nous retournons une promesse avec le traitement ultérieur du résultat: l'interdiction de montrer le formulaire lorsque la demande est terminée normalement, ou en cas d'erreur - ouvrir une boîte de dialogue avec le texte de l'erreur.

Chaque fois que vous accédez au serveur principal, la liste des notes est relue. Dans cet exemple, il n'est pas nécessaire d'ajuster la liste en mémoire, bien que cela puisse être fait.

Le composant de dialogue peut être affiché dans le référentiel , la seule chose qui doit être soulignée est que dans ce cas j'ai utilisé un littéral objet pour définir le composant, parce que je veux que les fonctions d'ouverture et de fermeture de fenêtre soient disponibles pour d'autres composants.

Conclusion


Nous avons écrit une petite application SPA en javascript et mithril.js, essayant de coller avec les idiomes de ce framework. Je veux faire attention une fois de plus que ce n'est que du code javascript. Peut-être pas tout à fait propre. L'approche vous permet d'encapsuler de petits morceaux de code, d'isoler la mécanique d'un composant et d'utiliser un état commun pour tous les composants.

All Articles