Beispiel für ein Simple Notes SPA unter Mithril.js

Mithril.js ist ein unbeliebtes Tool zum Erstellen von Client-Webanwendungen. Zu diesem Thema gibt es praktisch keine Veröffentlichungen zu Habré. In diesem Beitrag möchte ich zeigen, wie Sie eine kleine Anwendung auf Mithril erstellen können. Der Antrag basiert auf dieser Veröffentlichung ( Übersetzung )

Was ist Mihtril?


Mithril ist ein reaktives Framework zur Erstellung von SPA (Single-Page-Webanwendungen). Alles in allem ist es nur Javascript und 13 Signaturen von API-Funktionen. Darüber hinaus gibt es eine Mithril-Stream-Bibliothek, die nicht in Mithril enthalten ist und separat verwendet wird. Der Kern von Mithril umfasst das Weiterleiten der Anwendung und das Arbeiten mit XHR-Anforderungen. Das zentrale Konzept ist die Abstraktion - ein virtueller Knoten (vnode). Ein virtueller Knoten ist nur ein js-Objekt mit einigen Attributen. Virtuelle Knoten werden mit der Sonderfunktion m () erstellt. Der aktuelle Status der Schnittstelle wird in einer Liste virtueller Knoten (virtuelles DOM) gespeichert. Beim ersten Rendern der Anwendungsseite wird das virtuelle DOM in das DOM übersetzt. Wenn der DOM-API-Ereignishandler gestartet wird, wenn das Versprechen m.request () erfüllt ist und wenn sich die URL ändert (Navigation entlang der Anwendungsrouten), wird ein neues virtuelles DOM-Array generiert.vergleicht mit dem alten, und die geänderten Knoten ändern das Browser-DOM. Zusätzlich zu DOM-Ereignissen und dem Abschluss der Anforderung m.request () kann das Neuzeichnen mit der Funktion m.redraw () manuell aufgerufen werden.

Es gibt keine HTML-ähnlichen Vorlagen in Mithril, es gibt keine JSX-Unterstützung, obwohl Sie all dies mit verschiedenen Plugins verwenden können, um sie zu erstellen, wenn Sie dies wünschen. Ich werde diese Möglichkeiten hier nicht nutzen.

Wenn das erste Argument für m () eine Zeichenfolge ist (z. B. 'div'), gibt die Funktion einen einfachen virtuellen Knoten zurück und als Ergebnis wird ein HTML-Tag im DOM angezeigt

<div></div>

Wenn das erste Argument für m () das Objekt oder die Funktion ist, die das Objekt zurückgibt, muss ein solches Objekt eine view () -Methode haben, und ein solches Objekt wird als Komponente bezeichnet. Die view () -Methode der Komponente sollte wiederum immer die Funktion m () (oder ein Array vom Typ [m (),]) zurückgeben. Auf diese Weise können wir eine Hierarchie von Komponentenobjekten erstellen. Und es ist klar, dass letztendlich alle Komponenten einfache vnode-Knoten zurückgeben.

Sowohl virtuelle Knoten als auch Komponenten verfügen über Lebenszyklusmethoden und werden als oninit (), oncreate (), onbeforeupdate () usw. bezeichnet. Jede dieser Methoden wird zu einem bestimmten Zeitpunkt beim Rendern der Seite aufgerufen.

Sie können Parameter als Objekt an den virtuellen Knoten oder die virtuelle Komponente übergeben. Dies sollte das zweite Argument für die Funktion m () sein. Sie können einen Link zu diesem Objekt innerhalb des Knotens mit der Notation vnode.attrs erhalten. Das dritte Argument für die Funktion m () sind die Nachkommen dieses Knotens und können über den Link vnode.children aufgerufen werden. Zusätzlich zur Funktion m () werden einfache Knoten von der Funktion m.trust () zurückgegeben.

Der Autor von Mithril bietet keine speziellen Muster für das Anwendungsdesign an, empfiehlt jedoch, einige erfolglose Lösungen zu vermeiden, z. B. „zu dicke“ Komponenten oder die Manipulation der Nachkommen des Komponentenbaums. Der Autor bietet auch keine speziellen Möglichkeiten oder Methoden an, um den Status der Anwendung als Ganzes oder der Komponenten zu steuern. Obwohl in der Dokumentation angegeben ist, dass Sie den Status des Knotens selbst nicht verwenden sollten, bearbeiten Sie ihn.

Alle diese Funktionen von Mithril scheinen sehr unpraktisch zu sein, und das Framework scheint noch nicht fertig zu sein. Es gibt keine speziellen Empfehlungen, es gibt keinen Status / Speicher, es gibt keinen Reduzierer / Ereignis-Dispatcher, keine Vorlagen. Tun Sie im Allgemeinen, was Sie können.

Was Sie für ein Beispiel brauchen


Wir werden verwenden:


Der Frontend-Server ist hier nicht wichtig, er muss lediglich den Client index.html sowie Skript- und Stildateien angeben.

Wir werden mithril nicht in node_modules installieren und den Anwendungscode und das Framework in einer Datei binden. Der Anwendungscode und Mithril werden separat auf die Seite hochgeladen.

Ich werde den Installationsvorgang für die Tools nicht beschreiben, obwohl ich über postgREST sagen kann. Laden Sie einfach die Binärdatei herunter, legen Sie sie in einem separaten Ordner ab und erstellen Sie eine Konfigurationsdatei test.conf wie folgt:

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" 

Gleichzeitig sollte Ihr postgesql-Cluster eine Testbasis und user1 haben. Erstellen Sie in dieser Testbasis eine Tabelle:

-- 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
)

Das Starten des postgREST-Servers erfolgt mit dem folgenden Befehl:

postgrest test.conf

Nach dem Starten des Servers werden Informationsmeldungen zum Herstellen einer Verbindung zur Datenbank und zum Port, an dem die Clients abgehört werden, angezeigt.

Ein Projekt planen


Wenn Sie also genau wissen, wie Mitril funktioniert, müssen Sie herausfinden, wie die Anwendung erstellt wird. Hier ist der Plan:

  1. Wir werden Anwendungsdaten in einem lokalen Objekt speichern, nennen wir sein Modell
  2. API-Anwendungen werden in einer separaten Datei gespeichert
  3. Wir werden Anwendungsrouten in einer separaten Datei speichern
  4. Die Pfaddatei ist der Einstiegspunkt für die Erstellung der Anwendung.
  5. Jede einzelne Komponente (und ich werde das Komponenten-Rendering-Schema verwenden) und die damit verbundenen Funktionen werden in einer separaten Datei gespeichert
  6. Jede Komponente, die Modelldaten rendert, hat Zugriff auf das Modell.
  7. Komponenten-DOM-Ereignishandler sind in der Komponente lokalisiert

Daher benötigen wir keine benutzerdefinierten Ereignisse, sondern native DOM-Ereignisse. Rückrufe für diese Ereignisse sind in den Komponenten lokalisiert. Ich werde Zwei-Wege-Bindung verwenden. Vielleicht mag nicht jeder diesen Ansatz, aber nicht jeder mag Redux oder Vuex. Darüber hinaus kann die Einwegbindungstechnik auch in Mitril unter Verwendung von Mithril-Sream elegant implementiert werden. In diesem Fall ist dies jedoch eine redundante Lösung.

Struktur des Anwendungsordners




Der öffentliche Ordner wird vom Frontserver bereitgestellt, es gibt eine index.html-Datei und Verzeichnisse mit Stilen und Skripten.

Der Ordner src enthält das Stammverzeichnis des Routers und die API-Definition sowie zwei Verzeichnisse für das Modell und die Ansichten.

Im Stammverzeichnis des Projekts befindet sich eine Konfigurationsdatei rollup.config, und das Projekt wird mit dem folgenden Befehl erstellt:

rollup –c

Um den Leser nicht mit den langen Codestücken zu langweilen, die auf github.com verfügbar sind , werde ich nur die wichtigsten Implementierungselemente kommentieren, um einen idiomatischen Ansatz für Mitral zu demonstrieren.

API und Router


API-Code:

// 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'],
]

Ich habe eine API für den REST-Server und für den Router definiert.

Router:

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);

Hier ist der Router am Dokumentkörper montiert. Der Router selbst ist ein Objekt, das die Routen und Komponenten beschreibt, die auf dieser Route verwendet werden. Die Funktion render () sollte vnode zurückgeben.

Das appApi-Objekt definiert alle gültigen Anwendungsrouten und das appMenu-Objekt definiert alle möglichen Navigationselemente für die Anwendung.

Die Renderfunktion generiert beim Aufruf ein Anwendungsmodell und übergibt es an den Stammknoten.

Anwendungsmodell


Die Struktur, in der relevante Daten gespeichert sind, habe ich als Modell bezeichnet. Der Quellcode der Funktion, die das Modell zurückgibt:

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;
  },

Hier wird es initialisiert und das Modellobjekt zurückgegeben. Die Verknüpfungen innerhalb des Objekts können sich ändern, die Verknüpfung zum Objekt selbst bleibt jedoch konstant.

Zusätzlich zur Funktion getModel verfügt das globale moModel-Objekt über Wrapper-Funktionen für die Mitry-Funktion m.request (). Dies sind getList (Modell) und formSubmit (Ereignis, Modell, Methode). Der Modellparameter ist tatsächlich eine Referenz auf das Modellobjekt. Ereignis ist das Ereignisobjekt, das beim Senden des Formulars generiert wird. Methode ist die HTTP-Methode, mit der die Notiz gespeichert werden soll (POST ist eine neue Notiz, PATCH, DELETE ist alt).

Darstellung


Der Ansichtsordner enthält Funktionen, die für das Rendern einzelner Seitenelemente verantwortlich sind. Ich habe sie in 4 Teile geteilt:

  • vuApp - die Stammkomponente der Anwendung,
  • vuNavBar - Navigationsleiste,
  • vuNotes - eine Liste von Notizen,
  • vuNoteForm - Notizbearbeitungsformular,
  • vuDialog - HTML-Dialogelement

Der Router bestimmt, dass vuView (Menü, Ansicht) auf einer einzelnen Route zurückgegeben wird.

Definition dieser Funktion:

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

Dies ist nur ein Wrapper, der die vuMain-Komponente zurückgibt. Wenn das appMenu-Objekt komplex genug ist, um verschachtelte Objekte in einer Struktur ähnlich einem externen Objekt zu haben, ist ein solcher Wrapper eine geeignete Möglichkeit, eine Komponente mit verschiedenen Navigationselementen und untergeordneten Komponenten zurückzugeben (Sie müssen nur weniger Code schreiben). .

VuMain-Komponente:

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)
    ];
  }};
};

Dies gibt einfach die vnodes der Navigation und den tatsächlichen Inhalt der Seite zurück.

Im Folgenden, wo es möglich ist, eine Komponente zu definieren, werde ich Verschlüsse verwenden. Closures werden beim ersten Rendern der Seite einmal aufgerufen und speichern lokal alle übergebenen Links zu Objekten und Definitionen ihrer eigenen Funktionen.

Das Schließen als Definition sollte immer eine Komponente zurückgeben.

Und eigentlich die Anwendungsinhaltskomponente:

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)
    ];
  }};
}

Da wir bereits ein Modell haben, möchte ich beim Aufrufen eines Abschlusses die gesamte Liste der Notizen aus der Datenbank abrufen. Die Seite enthält drei Komponenten:

  • vuNotes - eine Liste von Notizen mit der Schaltfläche Hinzufügen,
  • vuNoteForm - Notizbearbeitungsformular,
  • vuModalDialog - ein Dialogelement, das wir modal anzeigen und bei Bedarf zuschlagen.

Da jede dieser Komponenten wissen muss, wie man sich selbst zeichnet, übergeben wir jeweils eine Verknüpfung zum Modellobjekt.

Komponentennotizliste:

//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 ) )
      ))
    ]);
  }};
}

Das bool editMode-Flag wird im Modellobjekt gespeichert. Wenn der Flag-Wert true ist, wird das Bearbeitungsformular angezeigt, andernfalls eine Liste mit Notizen. Sie können die Prüfung um eine Stufe höher erhöhen, aber dann würde sich die Anzahl der virtuellen Knoten und der DOM-Knoten selbst jedes Mal ändern, wenn das Flag gewechselt wird, und dies ist unnötige Arbeit.

Hier sind wir idiomatisch für Mitril. Wir generieren eine Seite und überprüfen das Vorhandensein oder Fehlen von Attributen im Modell mithilfe ternärer Operatoren.

Hier ist der Abschluss, der die Notenanzeigefunktion zurückgibt:

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;
}

Alle Klick-Handler werden lokal definiert. Das Modell weiß nicht, wie das Notizobjekt aufgebaut ist. Ich habe nur die Schlüsseleigenschaft definiert, mit der wir das gewünschte Element aus dem Array model.list auswählen können. Die Komponente muss jedoch genau wissen, wie das von ihr gezeichnete Objekt strukturiert ist.

Ich werde nicht den vollständigen Text des Codes für das Formular zum Bearbeiten der Notiz angeben. Wir betrachten den Formularübermittlungs-Handler nur separat:

// 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; } );
  };

Da die Definition im Abschluss erfolgt, haben wir einen Link zum Modell und geben ein Versprechen mit der anschließenden Verarbeitung des Ergebnisses zurück: das Verbot, das Formular anzuzeigen, wenn die Anforderung normal abgeschlossen ist, oder im Fehlerfall - Öffnen eines Dialogs mit dem Fehlertext.

Bei jedem Zugriff auf den Backend-Server wird die Liste der Notizen erneut gelesen. In diesem Beispiel muss die Liste im Speicher nicht angepasst werden, obwohl dies möglich ist.

Die Dialogkomponente kann im Repository angezeigt werden. Das einzige, was hervorgehoben werden muss, ist, dass ich in diesem Fall ein Objektliteral zum Definieren der Komponente verwendet habe, da die Funktionen zum Öffnen und Schließen von Fenstern für andere Komponenten verfügbar sein sollen.

Fazit


Wir haben eine kleine SPA-Anwendung in Javascript und mithril.js geschrieben und versucht, uns an die Redewendungen dieses Frameworks zu halten. Ich möchte noch einmal darauf achten, dass dies nur Javascript-Code ist. Vielleicht nicht ganz sauber. Mit diesem Ansatz können Sie kleine Codeteile kapseln, die Mechanik einer Komponente isolieren und für alle Komponenten einen gemeinsamen Status verwenden.

All Articles