Что такое Mihtril
Mithril – реактивный фреймворк, предназначенный для создания SPA (одностраничных веб приложений). В общем, это просто javascript и 13 сигнатур функций API. Кроме этого, есть библиотека mithril-stream, не входящая в mithril и используемая отдельно. В ядро mithril входит маршрутизация приложения и работа с XHR запросами. Центральным понятием является абстракция — виртуальный узел (vnode). Виртуальный узел – это просто js объект с некоторым набором атрибутов. Виртуальные узлы создаются специальной функцией m(). Текущее состояние интерфейса хранится в списке виртуальных узлов (virtual DOM). При начальном рендеринге страницы приложения, virtual DOM транслируется в DOM. При запуске обработчика событий DOM API, при завершении промиса m.request() и при смене URL (навигация по маршрутам приложения) генерируется новый массив virtual DOM, сравнивается со старым, и изменившиеся узлы изменяют DOM браузера. Кроме событий DOM и завершения запроса m.request(), перерисовку можно вызвать вручную функцией m.redraw().
В mithril из коробки нет HTML подобных шаблонов, нет поддержки JSX, хотя при желании все это можно использовать с помощью различных плагинов при сборке. Я здесь не буду использовать эти возможности.
Если первым аргументом m() будет строка, (например ‘div’), тогда функция возвращает простой виртуальный узел и в результате в DOM будет выведен HTML тэг
<div></div>
Если первым аргументом m() будет объект или функция возвращающая объект, то такой объект должен иметь метод view(), и такой объект называется компонентом. Метод view() компонента в свою очередь должен всегда возвращать функцию m() (или массив типа: [ m(), ]). Таким образом, мы можем построить иерархию объектов-компонентов. И понятно, что в конечном итоге все компоненты возвращают простые vnode узлы.
И виртуальные узлы, и компоненты имеют методы жизненного цикла, и называются они одинаково oninit(), oncreate(), onbeforeupdate(), и т.д. Каждый из этих методов вызывается во вполне определенный момент времени рендеринга страницы.
Виртуальному узлу или компоненту можно передать параметры в виде объекта, которым должен быть второй аргумент функции m(). Получить ссылку на этот объект внутри узла можно с помощью нотации vnode.attrs. Третий аргумент функции m() – это потомки этого узла и доступ к ним можно получить по ссылке vnode.children. Кроме функции m(), простые узлы возвращает функция m.trust().
Автор mithril не предлагает никаких особых паттернов проектирования приложения, хотя и советует избегать некоторых неудачных решений, например, «слишком толстых» компонентов или манипуляцией потомками дерева компонентов. Автор также не предлагает особых путей или способов управления состоянием приложения в целом или компонентов. Хотя в документации уточняется, что не следует использовать состояние самого узла, манипулировать им.
Все эти особенности mithril кажутся очень неудобными, и фреймворк представляется недоделанным, нет особых рекомендаций, нет состояния/хранилища, нет редуктора/диспетчера событий, нет шаблонов. В общем, делайте, как умеете.
Что нужно для примера
Будем использовать:
- Mithril.js версии 2.0.4
- css библиотеку pure.css версии 1.0.1
- fontawesome v5.12 для иконок
- rollup.js версии 1.32 для сборки приложения
- postgREST версии 5.2.0, в качестве backend REST сервера
- postgresql версии 9.6 или выше как база данных.
Фронтенд сервер здесь не важен, он просто должен отдать клиенту index.html и файлы скриптов и стилей.
Не будем устанавливать mithril в node_modules, и связывать код приложения и фреймворк в один файл. Код приложения и mithril будет загружаться на страницу по отдельности.
Процедуру установки инструментов я описывать не буду, хотя по поводу postgREST могу сказать, просто скачайте бинарный файл, поместите его в отдельную папку, создайте там конфигурационный файл test.conf типа такого:
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"
При этом, в вашем кластере postgesql должна быть база testbase и пользователь user1. В этой тестовой базе создайте таблицу:
-- 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 )
Запуск сервера postgREST производится командой:
postgrest test.conf
После запуска сервера, он выводит информационные сообщения о подключении к базе, и на каком порту будет прослушивать клиентов.
Планируем проект
Итак, если примерно понятно как работает митрил, нужно придумать, как сделать приложение. Вот план:
- Данные приложения будем хранить в локальном объекте, назовем его модель
- API приложения будем хранить в отдельном файле
- Маршруты приложения будем хранить в отдельном файле
- Маршрутный файл будет точкой входа для сборки приложения
- Каждый отдельный компонент (а я буду использовать компонентную схему рендеринга) и связанные с ним функции будем хранить в отдельном файле
- Каждый компонент, который рендерит данные модели, будет иметь доступ к модели
- Обработчики событий DOM компонента локализуем в компоненте
Таким образом, нам не нужны пользовательские события, достаточно нативных событий DOM, колбэки по этим событиям локализуем в компонентах. Я буду использовать two way binding. Возможно, такой поход не всем нравится, но и не всем нравится redux или vuex. Тем более что технику one way binding можно изящно реализовать и в митрил, с использованием mithril-sream. Но в данном случае это избыточное решение.
Структура папок приложения

Папка public будет обслуживаться фронт сервером, там есть файл index.html, и каталоги со стилями и скриптами.
Папка src содержит в корне роутер и определение API, и два каталога, для модели и представлений.
В корне проекта есть конфигурационный файл rollup.config, и сборка проекта выполняется командой:
rollup –c
Чтобы не утомлять читателя длинными кусками кода, который доступен на github.com, я прокомментирую только основные элементы реализации, чтобы продемонстрировать идиоматичный для митрил подход.
API и router
Код 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'], ]
Я определил API для REST сервера и для маршрутизатора.
Маршрутизатор:
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);
Здесь роутер примонтирован к телу документа. Сам роутер это объект описывающий маршруты и компоненты, которые будут использованы на этом маршруте, функция render() должна вернуть vnode.
Объект appApi определяет все допустимые маршруты приложения, а объект appMenu — все возможные элементы навигации приложения.
Функция render при вызове генерирует модель приложения и передает ее корневому узлу.
Модель приложения
Структуру, хранящую актуальные данные я назвал моделью. Исходный код функции возвращающей модель:
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; },
Здесь инициализируется, и возвращаем объект модели. Ссылки внутри объекта могут меняться, однако ссылка на сам объект остается постоянной.
Кроме функции getModel в глобальном объекте moModel есть функции обертки для митрил-функции m.request(), это getList(model), и formSubmit(event, model, method). Параметр model, это собственно ссылка на объект модели, event – объект события который генерируется при отправке формы, method- HTTP method, с помощью которого мы хотим сохранить заметку (POST- новая заметка, PATCH, DELETE — старая).
Представления
В папке view лежат функции, которые отвечают за рендеринг отдельных элементов страницы. Я разделил их на 4 части:
- vuApp – корневой компонент приложения,
- vuNavBar – панель навигации,
- vuNotes – список заметок,
- vuNoteForm – форма редактирования заметки,
- vuDialog – HTML элемент dialog
В роутере определено, что по единственному маршруту возвращается vuView( menu, view )
Определение этой функции:
export const vuView= (appMenu, view)=> m(vuMain, appMenu, view);
Это просто обертка, которая возвращает компонент vuMain, если объект appMenu будет достаточно сложным, иметь вложенные объекты по структуре сходные с внешним объектом, то такая обертка является подходящим способом возвращать компонент, с разными элементами навигации и дочерними компонентами (просто писать кода нужно будет меньше).
Компонент 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) ]; }}; };
Здесь просто возвращается vnodes навигации и собственно контента страницы.
Здесь и далее, где это возможно для определения компонента я буду использовать замыкания. Замыкания вызываются единожды при начальном рендеринге страницы, и хранят локально все переданные ссылки на объекты и определения собственных функций.
Замыкание в качестве определения всегда должно возвращать компонент.
И собственно компонент контента приложения:
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) ]; }}; }
Поскольку у нас уже есть модель, при вызове замыкания я хочу получить весь список заметок из БД. На станице будут три компонента:
- vuNotes — список заметок с кнопкой добавить,
- vuNoteForm – форма редактирования заметки,
- vuModalDialog – элемент dialog, который мы будем показывать модально, и захлопывать когда нужно.
Поскольку каждому из этих компонентов нужно знать, как себя отрисовывать, мы в каждый передаем ссылку на объект модели.
Компонент список заметок:
//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 ) ) )) ]); }}; }
В объекте модели хранится bool флаг editMode, если значение флага true, то показываем форму редактирования, в противном случае — список заметок. Можно проверку поднять на уровень выше, но тогда количество виртуальных узлов и собственно узлов DOM менялось бы каждый раз при переключении флага, а это лишняя работа.
Здесь мы идиоматично для митрил, генерируем страницу, проверяя наличие или отсутствие атрибутов в модели с помощью тернарных операторов.
Вот замыкание возвращающее функцию отображения заметки:
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; }
Все обработчики кликов определены локально. Модель не знает, как устроен объект note, я определил только свойство key, с помощью которого мы из массива model.list можем выбрать нужный элемент. Однако компонент должен точно знать, как устроен объект, который он отрисовывает.
Полный текст кода формы редактирования заметки я приводить не буду, просто отдельно посмотрим на обработчик отправки формы:
// 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; } ); };
Поскольку определение происходит в замыкании, у нас есть ссылка на модель, и возвращаем мы промис с последующей обработкой результата: запрет на показ формы при нормальном завершении запроса, или при ошибке — открытие диалога с текстом ошибки.
При каждом обращении к бэкенд серверу, перечитывается список заметок. В данном примере нет нужды корректировать список в памяти, хотя это и можно сделать.
Компонент dialog, можно посмотреть в репозитории, единственно, что нужно подчеркнуть — в данном случае я использовал объектный литерал для определения компонента, поскольку хочу, чтобы функции открытия и закрытия окна были доступны другим компонентам.
Заключение
Мы написали небольшое приложение SPA на javascript и mithril.js, стараясь придерживаться идиоматики этого фреймворка. Хочется еще раз обратить внимание, что это просто код на javascript. Возможно не совсем чистый. Подход позволяет инкапсулировать небольшие куски кода, изолировать механику компонента, и использовать общее для всех компонентов состояние.
ссылка на оригинал статьи https://habr.com/ru/post/492496/
Добавить комментарий