Ejemplo de Simple Notes SPA en Mithril.js

Mithril.js es una herramienta impopular para construir aplicaciones web de clientes. Prácticamente no hay publicaciones sobre Habré sobre este tema. En esta publicación, quiero mostrar cómo puedes hacer una pequeña aplicación en Mithril. La aplicación se basará en esta publicación ( traducción )

¿Qué es mihtril?


Mithril es un marco reactivo diseñado para crear SPA (aplicaciones web de una sola página). En general, es solo javascript y 13 firmas de funciones API. Además, hay una biblioteca de flujo de mithril que no está incluida en mithril y se usa por separado. El núcleo de mithril incluye el enrutamiento de la aplicación y el trabajo con solicitudes XHR. El concepto central es la abstracción: un nodo virtual (vnode). Un nodo virtual es solo un objeto js con algún conjunto de atributos. Los nodos virtuales son creados por la función especial m (). El estado actual de la interfaz se almacena en una lista de nodos virtuales (DOM virtual). En la representación inicial de la página de la aplicación, el DOM virtual se traduce al DOM. Cuando se inicia el controlador de eventos de la API DOM, cuando se completa la promesa m.request () y cuando cambia la URL (navegación a lo largo de las rutas de la aplicación), se genera una nueva matriz DOM virtual,se compara con el antiguo, y los nodos modificados cambian el DOM del navegador. Además de los eventos DOM y la finalización de la solicitud m.request (), se puede llamar a redibujar manualmente con la función m.redraw ().

No hay plantillas similares a HTML en mithril listas para usar, no hay soporte para JSX, aunque puede usar todo esto con varios complementos para construir si lo desea. No usaré estas oportunidades aquí.

Si el primer argumento para m () es una cadena (por ejemplo, 'div'), la función devuelve un nodo virtual simple y, como resultado, se mostrará una etiqueta HTML en el DOM

<div></div>

Si el primer argumento para m () es el objeto o la función que devuelve el objeto, entonces dicho objeto debe tener un método view (), y dicho objeto se llama componente. El método view () del componente, a su vez, siempre debe devolver la función m () (o una matriz del tipo: [m (),]). Por lo tanto, podemos construir una jerarquía de objetos componentes. Y está claro que, en última instancia, todos los componentes devuelven nodos vnode simples.

Tanto los nodos virtuales como los componentes tienen métodos de ciclo de vida, y se denominan oninit (), oncreate (), onbeforeupdate (), etc. Cada uno de estos métodos se llama en un momento muy específico en la representación de la página.

Puede pasar parámetros al nodo virtual o componente como un objeto, que debería ser el segundo argumento para la función m (). Puede obtener un enlace a este objeto dentro del nodo utilizando la notación vnode.attrs. El tercer argumento para la función m () son los descendientes de este nodo y se puede acceder a través del enlace vnode.children. Además de la función m (), la función m.trust () devuelve nodos simples.

El autor de mithril no ofrece ningún patrón especial de diseño de aplicaciones, aunque aconseja evitar algunas soluciones fallidas, por ejemplo, componentes "demasiado gruesos" o manipular a los descendientes del árbol de componentes. El autor tampoco ofrece formas o métodos especiales para controlar el estado de la aplicación como un todo o componentes. Aunque la documentación establece que no debe usar el estado del nodo en sí, manipúlelo.

Todas estas características del mithril parecen muy inconvenientes, y el marco parece estar inacabado, no hay recomendaciones especiales, no hay estado / almacenamiento, no hay reductor / despachador de eventos, ni plantillas. En general, haz lo que puedas.

Lo que necesitas para un ejemplo


Usaremos:


El servidor frontend no es importante aquí, solo tiene que proporcionar al cliente index.html y archivos de script y de estilo.

No instalaremos mithril en node_modules, y enlazaremos el código de la aplicación y el marco en un solo archivo. El código de la aplicación y el mithril se cargarán en la página por separado.

No describiré el procedimiento de instalación de las herramientas, aunque puedo decir sobre postgREST, solo descargue el archivo binario, colóquelo en una carpeta separada, cree un archivo de configuración test.conf como este:

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" 

Al mismo tiempo, su clúster postgesql debe tener una base de base de prueba y usuario1. En esta base de prueba, cree una tabla:

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

El inicio del servidor postgREST se realiza con el comando:

postgrest test.conf

Después de iniciar el servidor, muestra mensajes informativos sobre la conexión a la base de datos y en qué puerto escuchará a los clientes.

Planificando un proyecto


Entonces, si se entiende más o menos cómo funciona Mitril, debe descubrir cómo hacer la aplicación. Aquí está el plan:

  1. Almacenaremos los datos de la aplicación en un objeto local, llamemos a su modelo
  2. Las aplicaciones API se almacenarán en un archivo separado
  3. Almacenaremos rutas de aplicaciones en un archivo separado
  4. El archivo de ruta será el punto de entrada para crear la aplicación.
  5. Cada componente separado (y usaré el esquema de representación de componentes) y las funciones asociadas con él se almacenarán en un archivo separado
  6. Cada componente que representa los datos del modelo tendrá acceso al modelo.
  7. Los controladores de eventos DOM de componentes se localizan en el componente

Por lo tanto, no necesitamos eventos personalizados, más bien eventos DOM nativos, las devoluciones de llamada para estos eventos se localizan en los componentes. Usaré el enlace bidireccional. Quizás no a todos les gusta este enfoque, pero no a todos les gusta redux o vuex. Además, la técnica de enlace unidireccional también se puede implementar de manera elegante en mitril usando mithril-sream. Pero en este caso, esta es una solución redundante.

Estructura de carpetas de aplicaciones




La carpeta pública será servida por el servidor frontal, hay un archivo index.html y directorios con estilos y scripts.

La carpeta src contiene la raíz del enrutador y la definición de API, y dos directorios, para el modelo y las vistas.

En la raíz del proyecto hay un archivo de configuración rollup.config, y el proyecto se construye usando el comando:

rollup –c

Para no aburrir al lector con los largos trozos de código que están disponibles en github.com, solo comentaré los principales elementos de implementación para demostrar un enfoque idiomático para mitral.

API y enrutador


Código 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'],
]

Definí una API para el servidor REST y para el enrutador.

Enrutador:

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

Aquí el enrutador está montado en el cuerpo del documento. El enrutador en sí es un objeto que describe las rutas y los componentes que se utilizarán en esta ruta, la función render () debería devolver vnode.

El objeto appApi define todas las rutas de aplicación válidas y el objeto appMenu define todos los elementos de navegación posibles para la aplicación.

La función de representación, cuando se llama, genera un modelo de aplicación y lo pasa al nodo raíz.

Modelo de aplicación


La estructura que almacena datos relevantes, la llamé modelo. El código fuente de la función que devuelve el modelo:

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

Aquí se inicializa y devuelve el objeto modelo. Los enlaces dentro del objeto pueden cambiar, pero el enlace al objeto en sí permanece constante.

Además de la función getModel, el objeto global moModel tiene funciones envolventes para la función m.request () mitry, estas son getList (modelo) y formSubmit (evento, modelo, método). El parámetro modelo es en realidad una referencia al objeto modelo, el evento es el objeto de evento que se genera cuando se envía el formulario, el método es el método HTTP con el que queremos guardar la nota (POST es una nota nueva, PATCH, DELETE es antigua).

Representación


La carpeta de vista contiene funciones que son responsables de representar elementos de página individuales. Los dividí en 4 partes:

  • vuApp: el componente raíz de la aplicación,
  • vuNavBar - barra de navegación,
  • vuNotes: una lista de notas,
  • vuNoteForm - formulario de edición de notas,
  • vuDialog: elemento de diálogo HTML

El enrutador determina que vuView (menú, vista) se devuelve en una sola ruta.

Definición de esta función:

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

Este es solo un contenedor que devuelve el componente vuMain, si el objeto appMenu es lo suficientemente complejo como para tener objetos anidados en una estructura similar a un objeto externo, entonces dicho contenedor es una forma adecuada de devolver un componente con diferentes elementos de navegación y componentes secundarios (solo necesita escribir menos código) .

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

Esto simplemente devuelve los vnodos de la navegación y el contenido real de la página.

De aquí en adelante, donde sea posible definir un componente, usaré cierres. Los cierres se llaman una vez durante la representación inicial de la página y almacenan localmente todos los enlaces pasados ​​a objetos y definiciones de sus propias funciones.

El cierre como definición siempre debe devolver un componente.

Y en realidad el componente de contenido de la aplicación:

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

Como ya tenemos un modelo, cuando llamo a un cierre, quiero obtener la lista completa de notas de la base de datos. Habrá tres componentes en la página:

  • vuNotes: una lista de notas con el botón Agregar,
  • vuNoteForm - formulario de edición de notas,
  • vuModalDialog: un elemento de diálogo que mostraremos modalmente y se cerrará cuando sea necesario.

Como cada uno de estos componentes necesita saber cómo dibujarse, pasamos un enlace al objeto modelo en cada uno.

Lista de notas de componentes:

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

El indicador bool editMode se almacena en el objeto modelo; si el valor del indicador es verdadero, entonces mostramos el formulario de edición; de lo contrario, una lista de notas. Puede elevar la prueba un nivel más alto, pero luego el número de nodos virtuales y los nodos DOM reales cambiarían cada vez que se cambie el indicador, y esto es un trabajo innecesario.

Aquí somos idiomáticos para mitril, generamos una página, verificando la presencia o ausencia de atributos en el modelo utilizando operadores ternarios.

Aquí está el cierre que devuelve la función de visualización de notas:

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

Todos los manejadores de clics se definen localmente. El modelo no sabe cómo está estructurado el objeto de nota, solo definí la propiedad clave, con la que podemos seleccionar el elemento deseado de la matriz model.list. Sin embargo, el componente debe saber exactamente cómo está estructurado el objeto que dibuja.

No daré el texto completo del código del formulario para editar la nota; solo miraremos el controlador de envío de formularios por separado:

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

Dado que la definición se produce en el cierre, tenemos un enlace al modelo, y devolvemos una promesa con el procesamiento posterior del resultado: la prohibición de mostrar el formulario cuando la solicitud se completa normalmente, o en caso de error: abrir un diálogo con el texto de error.

Cada vez que accede al servidor de fondo, se vuelve a leer la lista de notas. En este ejemplo, no es necesario ajustar la lista en la memoria, aunque esto se puede hacer.

El componente de diálogo se puede ver en el repositorio , lo único que debe enfatizarse es que en este caso usé un objeto literal para definir el componente, porque quiero que las funciones de apertura y cierre de la ventana estén disponibles para otros componentes.

Conclusión


Escribimos una pequeña aplicación de SPA en javascript y mithril.js, tratando de mantener los modismos de este marco. Quiero prestar atención una vez más que esto es solo código javascript. Quizás no del todo limpio. El enfoque le permite encapsular pequeños fragmentos de código, aislar la mecánica de un componente y utilizar un estado común para todos los componentes.

All Articles