Contoh SPA Simple Notes di Mithril.js

Mithril.js adalah alat yang tidak populer untuk membangun aplikasi web klien. Praktis tidak ada publikasi tentang Habré tentang topik ini. Dalam posting ini, saya ingin menunjukkan bagaimana Anda dapat membuat aplikasi kecil di Mithril. Aplikasi akan didasarkan pada publikasi ini ( terjemahan )

Apa itu Mihtril?


Mithril adalah kerangka kerja reaktif yang dirancang untuk membuat SPA (aplikasi web satu halaman). Semua dalam semua, itu hanya javascript dan 13 tanda tangan dari fungsi API. Selain itu, ada perpustakaan mithril-stream yang tidak termasuk dalam mithril dan digunakan secara terpisah. Inti dari mithril meliputi perutean aplikasi dan bekerja dengan permintaan XHR. Konsep sentral adalah abstraksi - simpul virtual (vnode). Node virtual hanyalah objek js dengan beberapa set atribut. Simpul virtual dibuat oleh fungsi khusus m (). Keadaan antarmuka saat ini disimpan dalam daftar node virtual (virtual DOM). Pada rendering awal halaman aplikasi, DOM virtual diterjemahkan ke dalam DOM. Ketika penangan peristiwa DOM API dimulai, ketika janji m.request () selesai, dan ketika URL berubah (navigasi di sepanjang rute aplikasi), array DOM virtual baru dihasilkan,membandingkan dengan yang lama, dan node yang diubah mengubah DOM browser. Selain peristiwa DOM dan penyelesaian permintaan m.request (), redrawing dapat dipanggil secara manual dengan fungsi m.redraw ().

Tidak ada templat mirip-HTML di luar kotak, tidak ada dukungan JSX, meskipun Anda dapat menggunakan semua ini dengan berbagai plugin untuk membangun jika diinginkan. Saya tidak akan menggunakan peluang ini di sini.

Jika argumen pertama ke m () adalah string, (misalnya, 'div'), maka fungsi mengembalikan simpul virtual sederhana dan sebagai hasilnya, tag HTML akan ditampilkan di DOM

<div></div>

Jika argumen pertama ke m () adalah objek atau fungsi yang mengembalikan objek, maka objek tersebut harus memiliki metode view (), dan objek seperti itu disebut komponen. Metode view () komponen, pada gilirannya, harus selalu mengembalikan fungsi m () (atau array tipe: [m (),])). Dengan demikian, kita dapat membangun hierarki objek komponen. Dan jelas bahwa pada akhirnya semua komponen mengembalikan node vnode sederhana.

Kedua node dan komponen virtual memiliki metode siklus hidup, dan mereka disebut sama oninit (), oncreate (), onbeforeupdate (), dll. Masing-masing metode ini dipanggil pada titik waktu yang sangat spesifik dalam rendering halaman.

Anda bisa meneruskan parameter ke simpul atau komponen virtual sebagai objek, yang seharusnya menjadi argumen kedua ke fungsi m (). Anda bisa mendapatkan tautan ke objek ini di dalam simpul menggunakan notasi vnode.attrs. Argumen ketiga ke fungsi m () adalah turunan dari simpul ini dan dapat diakses melalui tautan vnode.children. Selain fungsi m (), simpul sederhana dikembalikan oleh fungsi m.trust ().

Penulis mithril tidak menawarkan pola desain aplikasi khusus, meskipun ia menyarankan untuk menghindari beberapa solusi yang gagal, misalnya, komponen "terlalu tebal" atau memanipulasi keturunan pohon komponen. Penulis juga tidak menawarkan cara atau metode khusus untuk mengontrol keadaan aplikasi secara keseluruhan atau komponen. Meskipun dokumentasi menyatakan bahwa Anda tidak boleh menggunakan status node itu sendiri, memanipulasi itu.

Semua fitur mithril ini tampaknya sangat merepotkan, dan kerangka kerja tampaknya belum selesai, tidak ada rekomendasi khusus, tidak ada keadaan / penyimpanan, tidak ada peredam / event dispatcher, tidak ada template. Secara umum, lakukan yang Anda bisa.

Apa yang Anda butuhkan sebagai contoh


Kami akan menggunakan:


Server frontend tidak penting di sini, hanya harus memberikan file index.html dan skrip dan gaya klien.

Kami tidak akan menginstal mithril di node_modules, dan mengikat kode aplikasi dan kerangka kerja menjadi satu file. Kode aplikasi dan mithril akan diunggah ke halaman secara terpisah.

Saya tidak akan menjelaskan prosedur instalasi untuk alat-alat tersebut, walaupun saya dapat mengatakan tentang postgREST, cukup unduh file biner, letakkan di folder terpisah, buat file konfigurasi test.conf seperti ini:

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" 

Pada saat yang sama, cluster postgesql Anda harus memiliki basis testbase dan user1. Di basis tes ini, buat tabel:

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

Memulai server postgREST dilakukan dengan perintah:

postgrest test.conf

Setelah memulai server, ini menampilkan pesan informasi tentang menghubungkan ke database, dan pada port mana ia akan mendengarkan klien.

Merencanakan proyek


Jadi, jika secara kasar memahami cara kerja mitril, Anda perlu memikirkan cara membuat aplikasi. Ini rencananya:

  1. Kami akan menyimpan data aplikasi dalam objek lokal, sebut saja modelnya
  2. Aplikasi API akan disimpan dalam file terpisah
  3. Kami akan menyimpan rute aplikasi dalam file terpisah
  4. File path akan menjadi titik masuk untuk membangun aplikasi.
  5. Setiap komponen terpisah (dan saya akan menggunakan skema render komponen) dan fungsi yang terkait dengannya akan disimpan dalam file terpisah
  6. Setiap komponen yang membuat data model akan memiliki akses ke model.
  7. Penangan peristiwa DOM komponen dilokalkan dalam komponen

Jadi, kita tidak perlu acara khusus, lebih tepatnya acara DOM asli, panggilan balik untuk acara ini dilokalkan dalam komponen. Saya akan menggunakan dua cara mengikat. Mungkin tidak semua orang menyukai pendekatan ini, tetapi tidak semua orang suka redux atau vuex. Selain itu, teknik mengikat satu arah juga dapat diterapkan secara elegan dalam mitril menggunakan mithril-sream. Tetapi dalam kasus ini, ini adalah solusi yang berlebihan.

Struktur Folder Aplikasi




Folder publik akan dilayani oleh server depan, ada file index.html, dan direktori dengan gaya dan skrip.

Folder src berisi akar dari router dan definisi API, dan dua direktori, untuk model dan tampilan.

Di root proyek ada file konfigurasi rollup.config, dan proyek dibangun menggunakan perintah:

rollup –c

Agar tidak membuat pembaca bosan dengan potongan kode panjang yang tersedia di github.com, saya hanya akan mengomentari elemen implementasi utama untuk menunjukkan pendekatan idiomatis untuk mitral.

API dan router


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

Saya mendefinisikan API untuk server REST dan untuk router.

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

Di sini router dipasang pada badan dokumen. Router itu sendiri adalah objek yang menggambarkan rute dan komponen yang akan digunakan pada rute ini, fungsi render () harus mengembalikan vnode.

Objek appApi mendefinisikan semua rute aplikasi yang valid, dan objek appMenu mendefinisikan semua elemen navigasi yang mungkin untuk aplikasi.

Fungsi render, ketika dipanggil, menghasilkan model aplikasi dan meneruskannya ke simpul root.

Model aplikasi


Struktur yang menyimpan data yang relevan, saya sebut model. Kode sumber fungsi yang mengembalikan model:

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

Ini diinisialisasi, dan mengembalikan objek model. Tautan di dalam objek mungkin berubah, tetapi tautan ke objek itu sendiri tetap konstan.

Selain fungsi getModel, objek moModel global memiliki fungsi wrapper untuk fungsi mitri m.request (), ini adalah getList (model), dan formSubmit (event, model, method). Parameter model sebenarnya adalah referensi ke objek model, event adalah objek event yang dihasilkan ketika formulir dikirimkan, metode adalah metode HTTP yang dengannya kita ingin menyimpan catatan (POST adalah catatan baru, PATCH, DELETE sudah tua).

Perwakilan


Folder tampilan berisi fungsi yang bertanggung jawab untuk merender elemen halaman individual. Saya membaginya menjadi 4 bagian:

  • vuApp - komponen root aplikasi,
  • vuNavBar - bilah navigasi,
  • vuNotes - daftar catatan,
  • vuNoteForm - formulir pengeditan catatan,
  • vuDialog - elemen dialog HTML

Router menentukan bahwa vuView (menu, view) dikembalikan pada satu rute.

Definisi fungsi ini:

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

Ini hanya pembungkus yang mengembalikan komponen vuMain, jika objek appMenu cukup kompleks untuk memiliki objek bersarang dalam struktur yang mirip dengan objek eksternal, maka pembungkus tersebut adalah cara yang tepat untuk mengembalikan komponen dengan elemen navigasi dan komponen anak yang berbeda (Anda hanya perlu menulis lebih sedikit kode) .

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

Ini hanya mengembalikan vnodes navigasi dan konten halaman yang sebenarnya.

Selanjutnya, di mana dimungkinkan untuk mendefinisikan komponen, saya akan menggunakan penutupan. Penutupan dipanggil sekali selama rendering awal halaman, dan secara lokal menyimpan semua tautan yang diteruskan ke objek dan definisi fungsi mereka sendiri.

Penutupan sebagai definisi harus selalu mengembalikan komponen.

Dan sebenarnya komponen konten aplikasi:

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

Karena kami sudah memiliki model, ketika saya menelepon penutupan, saya ingin mendapatkan seluruh daftar catatan dari database. Akan ada tiga komponen pada halaman:

  • vuNotes - daftar catatan dengan tombol add,
  • vuNoteForm - formulir pengeditan catatan,
  • vuModalDialog - elemen dialog yang akan kami tampilkan secara modal, dan banting bila perlu.

Karena masing-masing komponen ini perlu tahu cara menggambar itu sendiri, kami meneruskan tautan ke objek model di masing-masing.

Daftar Catatan Komponen:

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

Bendera bool editMode disimpan dalam objek model, jika nilai flag benar, maka kami menunjukkan formulir pengeditan, jika tidak - daftar catatan. Anda dapat menaikkan tanda centang satu tingkat lebih tinggi, tetapi kemudian jumlah node virtual dan node DOM itu sendiri akan berubah setiap kali bendera diaktifkan, dan ini adalah pekerjaan yang tidak perlu.

Di sini kami idiomatis untuk mitril, kami menghasilkan halaman, memeriksa ada atau tidaknya atribut dalam model menggunakan operator ternary.

Berikut adalah penutupan yang mengembalikan fungsi tampilan catatan:

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

Semua penangan klik didefinisikan secara lokal. Model tidak tahu bagaimana objek catatan terstruktur, saya hanya mendefinisikan properti kunci, yang dengannya kita dapat memilih elemen yang diinginkan dari array model.list. Namun, komponen harus tahu persis bagaimana objek yang digambar terstruktur.

Saya tidak akan memberikan teks lengkap dari kode untuk formulir untuk mengedit catatan, kita hanya melihat penangan pengiriman formulir secara terpisah:

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

Karena definisi terjadi pada penutupan, kami memiliki tautan ke model, dan kami mengembalikan janji dengan pemrosesan selanjutnya dari hasil: larangan menampilkan formulir ketika permintaan selesai secara normal, atau dalam kasus kesalahan - membuka dialog dengan teks kesalahan.

Setiap kali Anda mengakses server backend, daftar catatan dibaca ulang. Dalam contoh ini, tidak perlu menyesuaikan daftar dalam memori, meskipun ini bisa dilakukan.

Komponen dialog dapat dilihat dalam repositori , satu-satunya hal yang perlu ditekankan adalah bahwa dalam hal ini saya menggunakan objek literal untuk mendefinisikan komponen, karena saya ingin fungsi membuka dan menutup jendela tersedia untuk komponen lain.

Kesimpulan


Kami menulis aplikasi SPA kecil dalam javascript dan mithril.js, mencoba untuk tetap menggunakan idiom kerangka ini. Saya ingin memperhatikan sekali lagi bahwa ini hanya kode javascript. Mungkin tidak cukup bersih. Pendekatan ini memungkinkan Anda untuk merangkum potongan-potongan kecil kode, mengisolasi mekanisme komponen, dan menggunakan keadaan umum untuk semua komponen.

All Articles