Exemplo de SPA do Simple Notes no Mithril.js

Mithril.js é uma ferramenta impopular para a criação de aplicativos Web clientes. Praticamente não há publicações sobre Habré sobre esse assunto. Neste post, quero mostrar como você pode fazer um pequeno aplicativo no Mithril. O aplicativo será baseado nesta publicação ( tradução )

O que é Mihtril


O Mithril é uma estrutura reativa projetada para criar SPA (aplicativos da Web de página única). Em suma, são apenas javascript e 13 assinaturas de funções da API. Além disso, existe uma biblioteca de mithril-stream que não está incluída no mithril e é usada separadamente. O núcleo do mithril inclui rotear o aplicativo e trabalhar com solicitações XHR. O conceito central é abstração - um nó virtual (vnode). Um nó virtual é apenas um objeto js com algum conjunto de atributos. Nós virtuais são criados pela função especial m (). O estado atual da interface é armazenado em uma lista de nós virtuais (DOM virtual). Na renderização inicial da página do aplicativo, o DOM virtual é convertido no DOM. Quando o manipulador de eventos da API DOM é iniciado, quando a promessa m.request () é concluída e quando o URL é alterado (navegação pelas rotas do aplicativo), um novo array DOM virtual é gerado,compara com o antigo e os nós alterados alteram o DOM do navegador. Além dos eventos DOM e da conclusão da solicitação m.request (), o redesenho pode ser chamado manualmente com a função m.redraw ().

Não existem modelos similares a HTML no mithril, não há suporte para JSX, embora você possa usar tudo isso com vários plugins para criar, se desejar. Não vou usar essas oportunidades aqui.

Se o primeiro argumento para m () for uma sequência (por exemplo, 'div'), a função retornará um nó virtual simples e, como resultado, uma tag HTML será exibida no DOM

<div></div>

Se o primeiro argumento para m () for o objeto ou função que retorna o objeto, esse objeto deverá ter um método view (), e esse objeto será chamado de componente. O método view () do componente, por sua vez, sempre deve retornar a função m () (ou uma matriz do tipo: [m (),]). Assim, podemos construir uma hierarquia de objetos componentes. E está claro que, em última análise, todos os componentes retornam nós vnode simples.

Os nós e componentes virtuais têm métodos de ciclo de vida e são chamados da mesma oninit (), oncreate (), onbeforeupdate () etc. Cada um desses métodos é chamado em um momento muito específico na renderização da página.

Você pode passar parâmetros para o nó ou componente virtual como um objeto, que deve ser o segundo argumento para a função m (). Você pode obter um link para este objeto dentro do nó usando a notação vnode.attrs. O terceiro argumento para a função m () são os descendentes desse nó e podem ser acessados ​​através do link vnode.children. Além da função m (), nós simples são retornados pela função m.trust ().

O autor do mithril não oferece nenhum padrão especial de design de aplicativo, embora ele recomende evitar algumas soluções malsucedidas, por exemplo, componentes "muito grossos" ou manipular os descendentes da árvore de componentes. O autor também não oferece maneiras ou métodos especiais para controlar o estado do aplicativo como um todo ou componentes. Embora a documentação declare que você não deve usar o estado do próprio nó, manipule-o.

Todos esses recursos do mithril parecem muito inconvenientes, e a estrutura parece estar inacabada, não há recomendações especiais, não há estado / armazenamento, não há redutor / expedidor de eventos, não há modelos. Em geral, faça o que puder.

O que você precisa para um exemplo


Nós vamos usar:


O servidor front-end não é importante aqui, apenas fornece ao cliente index.html e arquivos de script e estilo.

Não instalaremos o mithril no node_modules e vincularemos o código do aplicativo e a estrutura em um arquivo. O código do aplicativo e o mithril serão enviados para a página separadamente.

Não descreverei o procedimento de instalação das ferramentas, embora eu possa dizer sobre o postgREST, basta baixar o arquivo binário, colocá-lo em uma pasta separada, criar um arquivo de configuração 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" 

Ao mesmo tempo, seu cluster postgesql deve ter uma base de teste e usuário1. Nesta base de teste, crie uma tabela:

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

O início do servidor postgREST é feito com o comando:

postgrest test.conf

Após iniciar o servidor, ele exibe mensagens informativas sobre a conexão com o banco de dados e em qual porta ele escuta os clientes.

Planejando um projeto


Portanto, se é compreensível como o mitril funciona, você precisa descobrir como fazer a aplicação. Aqui está o plano:

  1. Vamos armazenar dados do aplicativo em um objeto local, vamos chamar seu modelo
  2. Os aplicativos de API serão armazenados em um arquivo separado
  3. Armazenaremos rotas de aplicativos em um arquivo separado
  4. O arquivo de caminho será o ponto de entrada para a criação do aplicativo.
  5. Cada componente separado (e eu usarei o esquema de renderização de componentes) e as funções associadas a ele serão armazenadas em um arquivo separado
  6. Cada componente que renderiza dados do modelo terá acesso ao modelo.
  7. Os manipuladores de eventos DOM do componente estão localizados no componente

Portanto, não precisamos de eventos personalizados, mas eventos DOM nativos, os retornos de chamada para esses eventos estão localizados nos componentes. Vou usar a ligação em dois sentidos. Talvez nem todos gostem dessa abordagem, mas nem todo mundo gosta de redux ou vuex. Além disso, a técnica de ligação unidirecional também pode ser elegantemente implementada no mitril usando mithril-sream. Mas, neste caso, esta é uma solução redundante.

Estrutura de pastas do aplicativo




A pasta pública será servida pelo servidor frontal, há um arquivo index.html e diretórios com estilos e scripts.

A pasta src contém a raiz do roteador e a definição da API e dois diretórios para o modelo e as visualizações.

Na raiz do projeto, há um arquivo de configuração rollup.config, e o projeto é criado usando o comando:

rollup –c

Para não aborrecer o leitor com os longos trechos de código disponíveis no github.com, comentarei apenas os principais elementos de implementação para demonstrar uma abordagem idiomática para mitral.

API e roteador


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

Eu defini uma API para o servidor REST e para o roteador.

Roteador:

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

Aqui o roteador está montado no corpo do documento. O próprio roteador é um objeto que descreve as rotas e os componentes que serão usados ​​nessa rota; a função render () deve retornar vnode.

O objeto appApi define todas as rotas válidas do aplicativo e o objeto appMenu define todos os elementos de navegação possíveis para o aplicativo.

A função de renderização, quando chamada, gera um modelo de aplicativo e o passa para o nó raiz.

Modelo de aplicação


A estrutura que armazena dados relevantes, chamei de modelo. O código fonte da função que retorna o 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;
  },

Aqui, ele é inicializado e retorna o objeto de modelo. Os links dentro do objeto podem mudar, mas o link para o próprio objeto permanece constante.

Além da função getModel, o objeto moModel global possui funções de wrapper para a função mitry m.request (), são getList (model) e formSubmit (evento, modelo, método). O parâmetro model é na verdade uma referência ao objeto model, event é o objeto de evento gerado quando o formulário é enviado, method é o método HTTP com o qual queremos salvar a anotação (POST é uma nova anotação, PATCH, DELETE é antigo).

Representação


A pasta de exibição contém funções responsáveis ​​pela renderização de elementos de página individuais. Dividi-os em 4 partes:

  • vuApp - o componente raiz do aplicativo,
  • vuNavBar - barra de navegação,
  • vuNotes - uma lista de notas,
  • vuNoteForm - formulário de edição de notas,
  • vuDialog - elemento de diálogo HTML

O roteador determina que vuView (menu, vista) é retornado em uma única rota.

Definição desta função:

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

Esse é apenas um invólucro que retorna o componente vuMain, se o objeto appMenu for complexo o suficiente para ter objetos aninhados em estrutura semelhante a um objeto externo, esse invólucro é uma maneira adequada de retornar um componente com diferentes elementos de navegação e componentes filhos (você só precisa escrever 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)
    ];
  }};
};

Isso simplesmente retorna os vnodes da navegação e o conteúdo real da página.

A seguir, onde for possível definir um componente, usarei fechamentos. Os fechamentos são chamados uma vez durante a renderização inicial da página e armazenam localmente todos os links passados ​​para objetos e definições de suas próprias funções.

Fechar como definição deve sempre retornar um componente.

E, na verdade, o componente de conteúdo do aplicativo:

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 já temos um modelo, quando chamo de fechamento, quero obter a lista inteira de anotações do banco de dados. Haverá três componentes na página:

  • vuNotes - uma lista de notas com o botão Adicionar,
  • vuNoteForm - formulário de edição de notas,
  • vuModalDialog - um elemento de diálogo que mostraremos modalmente, e bateremos quando necessário.

Como cada um desses componentes precisa saber como se desenhar, passamos um link para o objeto de modelo em cada um.

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

O sinalizador bool editMode é armazenado no objeto de modelo; se o valor do sinalizador for verdadeiro, mostraremos o formulário de edição, caso contrário - uma lista de notas. Você pode elevar o teste um nível mais alto, mas o número de nós virtuais e os nós DOM reais mudam toda vez que o sinalizador é alternado, e isso é um trabalho desnecessário.

Aqui somos idiomáticos para o mitril, geramos uma página, verificando a presença ou ausência de atributos no modelo usando operadores ternários.

Aqui está o fechamento que retorna a função de exibição 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 os manipuladores de clique são definidos localmente. O modelo não sabe como o objeto de nota está estruturado; apenas defini a propriedade key, com a qual podemos selecionar o elemento desejado na matriz model.list. No entanto, o componente deve saber exatamente como o objeto que ele desenha é estruturado.

Não fornecerei o texto completo do código para o formulário de edição da nota; apenas analisamos o manipulador de envio de formulários separadamente:

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

Como a definição ocorre no fechamento, temos um link para o modelo e retornamos uma promessa com o processamento subsequente do resultado: proibição de mostrar o formulário quando a solicitação for concluída normalmente ou em caso de erro - abrir uma caixa de diálogo com o texto do erro.

Cada vez que você acessa o servidor back-end, a lista de notas é relida. Neste exemplo, não há necessidade de ajustar a lista na memória, embora isso possa ser feito.

O componente de diálogo pode ser visualizado no repositório , a única coisa que precisa ser enfatizada é que, neste caso, usei um literal de objeto para definir o componente, porque quero que as funções de abertura e fechamento de janela estejam disponíveis para outros componentes.

Conclusão


Escrevemos um pequeno aplicativo SPA em javascript e mithril.js, tentando manter os idiomas dessa estrutura. Quero prestar atenção mais uma vez que este é apenas código javascript. Talvez não seja muito limpo. A abordagem permite encapsular pequenos pedaços de código, isolar a mecânica de um componente e usar um estado comum para todos os componentes.

All Articles