Руководство Mithril JS Framework¶
Вступление¶
Пример кода¶
//model
var Page = {
list: function() {
return m.request({method: "GET", url: "pages.json"});
}
};
var Demo = {
//controller
controller: function() {
var pages = Page.list();
return {
pages: pages,
rotate: function() {
pages().push(pages().shift());
}
}
},
//view
view: function(ctrl) {
return m("div", [
ctrl.pages().map(function(page) {
return m("a", {href: page.url}, page.title);
}),
m("button", {onclick: ctrl.rotate}, "Rotate links")
]);
}
};
//initialize
m.mount(document.getElementById("example"), Demo);
Что такое Mithril¶
Mitrhil - это JavaScript MVC-фреймворк для клиентских приложений, представляющий из себя инструмент для разделения кода приложения на слой данных Модель (Model), слой пользовательсокго интерфейса (UI) Вид (View) и связующий слой Контроллер (Controller).
Объем сжатого кода Mithril составляет примерно 7кБ благодаря его компактному и выразительному API. Он предоставляет вам в распоряжение движок шаблонов с отслеживанием изменений в виртуальном DOM (virtual DOM) для производительного отображения елементов страницы, утилиты для высокоуровневого моделирования через функциональную композицию (functional composition), а также поддерживает маршрутизацию и создание компонентов (componentization).
Цель Mithril состоит в том, чтобы сделать ваши приложения расширяемыми, читаемыми и поддерживаемыми, а вас - даже лучшим разработчиком, чем вы являетесь сейчас :)
В отличие от некоторых других фреймворков, Mithril старается не завлечь вас в ловушку зависимостей: вы можете использовать лишь некоторые возможности фреймоворка в соответствии с вашими нуждами.
В то же время, использование полного набора предоставляемых инструментов может дать дополнительные выгоды: изучение подходов функционального программирования и освоение лучших практик программирования для ООП (объектно-орентированного программирования) и MVC - лишь часть из них.
Простое приложение¶
Минимальный шаблон¶
Подгрузив копию Mithril, для начала работы вам требуется минимальный код шаблона:
<!doctype html>
<title>Todo app</title>
<script src="mithril.min.js"></script>
<script>
//здесь будет приложение
</script>
И да, это правильный (валидный) HTML5! В соответствии со спецификацией, тэги <html>, <head> и <body> могут быть пропущены, так как соответствующие им DOM элементы будут неявно вставлены браузером во время отображения страницы.
Модель¶
В Mithril приложение обычно живет в пространстве имен (namespace) и содержит компоненты. Компоненты - это просто структуры, которые обеспечивают отображение страницы или ее части.
Дополнительно приложение может быть разделено на три основных слоя: Модель, Вид и Контроллер.
Для простоты наше приложение будет иметь только один компонент и мы собираемся использовать его как простраство имен для нашего приложения.
В Mithril компонент - это JS-объект, содержащий две функции: controller и view.
//пустой компонент Mithril
var myComponent = {
controller: function() {},
view: function() {}
}
В дополнение к функциям controller и view, компонент может быть использован для хранения данных, относящихся к нему.
Давайте создадим компонент
<script>
//это приложение имеет только один компонент: todo
var todo = {};
</script>
Обычно записи (entities) модели являются переиспользуемыми и существуют отдельно от компонентов (например, var User = [...]). В нашем примере все приложение будет в реализовано в одном компоненте и мы собираемся использовать этот компонент как простраство имен для записей модели.
var todo = {};
//для простоты мы используем этот компонент как простроанство имен для классов модели
//класс Todo class имеет два свойства
todo.Todo = function(data) {
this.description = m.prop(data.description);
this.done = m.prop(false);
};
//класс TodoList - это список элементов Todo
todo.TodoList = Array;
m.prop является просто фабрикой для геттеров и сеттеров. Геттеры-сеттеры работают так:
//объявить геттер-сеттер для начального значения `John`
var a_name = m.prop("John");
//прочитать значение
var a = a_name(); //a == "John"
//установить значение `Mary`
a_name("Mary"); //Mary
//прочитать значение
var b = a_name(); //b == "Mary"
Обратите внимание, что классы Todo и TodoList определены выше как констукторы на чистом JavaScript. Они могут быть инициализированы и использованы как в примере ниже:
var myTask = new todo.Todo({description: "Write code"});
//прочитать описание (description)
myTask.description(); //Write code
//выполнено?
var isDone = myTask.done(); //isDone == false
//отметить как выполненное
myTask.done(true); //true
//теперь точно выполнено
isDone = myTask.done(); //isDone == true
Класс TodoList - это просто псевдоним встроенноого класса Array.
var list = new todo.TodoList();
list.length; //0
В соответствии с классическим определением шаблона проектирования MVC, слой модели отвечает за хранение данных, управление состоянием и бизнес-логику (на уровне данных).
Вы можете видеть, что описанные выше классы подходят под эти критерии: они имеют свойства и методы, которые потребутся для хранения состояния. Можно создавать экземпляры Todo с изменяемымми свойствами. TodoList может иметь элементы, являющиеся экземплярами Todo, которые могут быть добавлены с помощью встроенного метода push. И так далее.
Вид-Модель (view-model)¶
Следующим шагом напишем вид-модель, который будет использовать наши классы моделей. Вид-модель - это сущность слоя модели, которая хранит состояние пользовательского интерфейса (UI). Во многих фреймворках состояние UI в основном хранится в контроллере, но такой подход делает код трудно масштабируемым, так как изначально контроллеры не разрабатывались для хранения данных. В Mithril данные состояния пользовательского интерфейса понимаются как данные модели даже при том, что они не обязательно соответствуют объектно-реляционной связи (ORM) хранимых данных.
Вид-модель также отвечает за обработку бизнес-логики, связанной с пользовательским интерфейсом. Для примера, форма может иметь поле ввода или кнопку отмены. В этом случае используемый вид-модель отвечает за отслеживание текущего состояния поля ввода, сравнивает с начальным состоянием и разрешает отмену при необходимости. При событии, возникающем при сохранении формы, вид-модель делегирует сохранение данных более соответствующей ORM-сущности.
В случае с нашим todo-приложением, вид-модель должен реализовывать следующее: отслеживать список выполняющихся задач (todo) и поле добавления новых задач, а также отбрабаывать логику добавления каждой задачи (todo) и последствия этого действия для пользовательского интерфейса (UI)
//определить view-model
todo.vm = {
init: function() {
//список выполняющихся задач
todo.vm.list = new todo.TodoList();
//поле для хранения описания (названия) новой задачи перед ее созданием
todo.vm.description = m.prop('');
//функция добавления задачи к списку, после добавления очищает поле description для удобства пользователя
todo.vm.add = function(description) {
if (description()) {
todo.vm.list.push(new todo.Todo({description: description()}));
todo.vm.description("");
}
};
}
};
Код выше опредедяет вид-модель под названием vm. Это просто объект javascript, который имеет функцию init.
Данная функция иницииализирует объект vm с тремя полями:
list- свойство, являющееся просто массивом;description- свойство, являющееся функциейm.prop(геттер-сеттер) с пустой строкой в качестве начального значения;add- метод добавления нового экземпляра Todo в свойство list в случае, если вводимое название (поле description) не является пустой строкой.
Ниже в этом руководстве мы передадим свойство description в функцию add в качестве параметра. Когда мы это сделаем, объясним, почему мы передали description как аргумент вместо просто присваивания в ООП-стиле.
Вы можете использовать вид-модель так:
//инициализировать вид-модель
todo.vm.init();
todo.vm.description(); //[empty string]
//добавим задачу
todo.vm.add(todo.vm.description);
todo.vm.list.length; //0, потому что вы не можете добавлять задачу без описания
//добавить описание и потом задачу
todo.vm.description("Write code");
todo.vm.add(todo.vm.description);
todo.vm.list.length; //1
Контроллер¶
В классическом MVC роль контроллера состоит в том, чтобы передавать события из слоя вида в слой модели. В традиционных серверных фреймворках слой контроллера имеет большое значение в связи с природой HTTP запросов и ответов, в связи с чем абстракции фреймворков, предоставляемые разработчикам, действуют подобно слою адаптера для преобразования и сериализации данных HTTP запросов во что-то, что может быть передано методам ORM.
В то же время в клиентском MVC такой проблемы запросов не существует и контроллер может быть предельно простым. Контроллеры Mithril могут быть совершенно минималистичными, выполняя простую необходимую роль: предоставлять набор функциональности уровня модели со своей областью видимости. Как вы помните, модели ответственны за реализацию бизнес-логики, и виды-модели релизуют логику, относится конкретно к состоянию UI, поэтому действительно нечего больше воплощать в контроллере, и все что от него требуется - предоставлять массив (список) слоя модели, соответствующий UI, отображаемому в текущий момент.
Другими словами, наш контроллер должен делать это:
todo.controller = function() {
todo.vm.init()
}
Представление (view)¶
Следующий шаг - написать вид (view) для того, чтобы пользователи могли взаимодейтсвовать с нашим приложением. В Mithril представление - просто чистый JavaScript. От этого получаются некоторые выгоды (правильные отчеты об ошибках, более удобняе лексические области видимости и т.д.), при этом синтаксис HTML доступен при использовании препроцессоров :https://github.com/insin/msx .
todo.view = function() {
return m("html", [
m("body", [
m("input"),
m("button", "Add"),
m("table", [
m("tr", [
m("td", [
m("input[type=checkbox]")
]),
m("td", "task description"),
])
])
])
]);
};
Метод m() создает элементы виртуального DOM. Как видите, можно использовать CSS-селекторы для определения атрибутов элементов. Используйте . для добаления CSS-класса и # для добавления id.
Опеределенно, если вы не планируете использовать HTML-препроцессор MSX https://github.com/insin/msx , мы рекомендуем использовать CSS-селекторы (напр., m(”.modal-body”)), получая семантически выразительный код.
Для тестирования за пределами кода, в настоящий момент может быть использован метод m.render:
m.render(document, todo.view());
Обратите внимание: мы передали родительский элемент DOM, к которому будет присоединен шаблон, а также сам шаблон.
Этот код сформирует слудующую разметку:
<html>
<body>
<input />
<button>Add</button>
<table>
<tr>
<td><input type="checkbox" /></td>
<td>task description</td>
</tr>
</table>
</body>
</html>
Обращаем внимание, что m.render - это достаточно низкоуровневый метод в Mithril, который выполняет отображение
элементов только один раз и не пытается реализовать систему автообновления (перерисовку) отображаемых элементов.
Для того, чтобы обновлять элементы, компонент todo должен иницииализироваться в каждом вызове m.mount или с помощью объявления маршрута m.route. Также вместо фреймворков, основанных на наблюдателях (таких как Knockout.js), изменение данных в геттере-сеттере m.prop НЕ приводит к перерисовке элементов в Mithril.
Связывание данных¶
Давайте реализуем связываение данных (data binding) для поля текстового ввода. Связываение данных соединяет элемент DOM с переменной JavaScript так, что обновление одного из них обновляет другое.
//связывание значения модели с полем ввода в шаблоне
m("input", {value: todo.vm.description()})
Этот код связывает геттер-сеттер description с тегом шаблона input. Обновление значения description в модели обновит элемент DOM input тогда, когда Mithril будет перерисовывать шаблон.
todo.vm.init();
todo.vm.description(); // пустая строка
m.render(document, todo.view()); // input пустой
todo.vm.description("Напишите код"); //установить description в контроллере
m.render(document, todo.view()); // теперь input говорит "Напишите код"
На первый взгляд может показаться, что мы делаем какие-то дорогие операции при перерисовке, но на самом деле вызов метода todo.view несколько раз в действительсноти не перерисовывает полный шаблон. Внутри Mithril хранит виртуальное представление DOM в кэше, сканирует изменения и затем проводит перерисовывает в DOM только измененные элементы. На практике это дает удивительно быстую перерисовку элементов.
В представленном выше случае Mithril изменит только атрибут value для требуемого элемента input.
Обратите внимание, что в данном примере мы только устанавливает значение для элемента input в DOM, но он никогда не читаются. Это значит, что если в этом поле ввода что-то будет введено с клавиатуры, то после перерисовки введенный текст будет потерян.
К счастью, связывание может быть двунаправленным: да, в дополнение к установку значения элемента DOM, можно читать введенные пользователем данные и для обсуждаемого примера обновить геттер-сеттер description в нашем виде-модели.
Вот основная реализация такого связывания для вида-модели:
m("input", {onchange: m.withAttr("value", todo.vm.description), value: todo.vm.description()})
Код, относящийся к onchange, может быть прочитан как “установить todo.vm.description равным значению атрибута value”
Заметьте, что в самом Mithril не опредено, по какому событию обновлять связанные данные: вы можете связать события onchange, onkeypress, oninput, onblur или любое другое событие.
Также вы можете указать атрибут DOM-элемента, которые будет связан. Это означает, что вы можете как связать атрибут value в теге select, так и, к примеру, свойство selectedIndex, если в этом есть необходимость.
Утилита m.withAttr - инструмент функционального программирования, предоставляемый вам Mithril для минимизации количества анонимных функций в представлении.
Вызов m.withAttr(“value”, todo.vm.description) в коде выше возвращает функцию, которая почти эквивалентна коду ниже:
onchange: function(e) {
todo.vm.description(e.target["value"]);
}
Разница в том, что кроме исключения необходимости создания анонимных функций, m.withAttr также заботится о получении корректного элемента, с которым произошло событие (target) и соответствующего источника данных в зависимости от того, является ли им свойство объекта JavaScript или эдемента DOM (DOMElement::getAttribute()).
Дополнительно к двустороннему связыванию данных, мы можем связывать параметризированные функции с событиями:
var vm = todo.vm
m("button", {onclick: vm.add.bind(vm, vm.description)}, "Add")
В этом коде мы просто используем встроенный метод JavaScript Function::bind. Это создает новую функцию с уже установленным параметром. (Примечание переводчика. Читается как “при клике вызвать функцию vm.add c контекстом this = vm и передать в нее параметр vm.description”.) В функциональном программировании это называется частичное приложение :https://en.wikipedia.org/wiki/Partial_application.
Выражение vm.add.bind(vm, vm.description) возвращает функцию, эквивалентную следующему коду:
onclick: function(e) {
todo.vm.add(todo.vm.description)
}
Заметьте, что когда мы используем параметризированное связываение, мы передаем ссылку на геттер-сеттер description, а не его значение. Вызвать геттер-сеттер для получения значения мы можем только в методах контроллера. Это форма “ленивого выполнения”, которая позволяет нам сказать “используй это значение позднее при вызове обработчика событий”.
Наконец, давайте посмотрим как Mithril поощряет использование m.prop: так как геттеры-сеттеры Mithril являются функциями, они хорошо сочетаются с инструментами функционального программирования и позволяют использовать некоторые мощные идиомы.
Hopefully by now, you’re starting to see why Mithril encourages the usage of m.prop: Because Mithril getter-setters are functions, they naturally compose well with functional programming tools, and allow for some very powerful idioms. В нашем примере мы будем использовать их наподобие указателей в языке C.
Также Mithril использует их другими интересными способами.
Умный читатель может заметить, что мы можем изменить метод add и сделать его существенно проще:
vm.add = function() {
if (vm.description()) {
vm.list.push(new todo.Todo({description: vm.description()}));
vm.description("");
}
};
Разница измененной версии в том, что метод add теперь не принимает аргументов.
Теперь мы можем сделать связывание с событием onclick в шаблоне значительно проще:
m("button", {onclick: todo.vm.add}, "Add")
Единственная причина, по которой мы рассказали о частичном применении, заключается в том, чтобы рассказать вам о технике, которая будет полезной при использовании параметризированных обработчиков событий. В реальной жизни лучше выбирайте более простые и понятные решения применительно к вашему случаю.
Для реализации работы со списочными данными в представлениях Mithril, мы просто используем методы JavaScript Array:
//представление (вид)
m("table", [
todo.vm.list.map(function(task, index) {
return m("tr", [
m("td", [
m("input[type=checkbox]")
]),
m("td", task.description()),
])
})
])
В коде выше todo.vm.list` - это массив (Array) и метод map - это один из его встроенных функциональных методов. Он позволяет нам итерировать список и объединять преобразованные вызываемой функцией элементы его списка в результирующий массив.
Как видно, мы возвращаем часть шаблона с двумы <td>. Второй из них связвает данные с геттером-сеттером description экземпляра класса Todo.
Возможно, вы уже начали замечать, что JavaScript имеет хорошую поддержку фукнционального программирования, что позволяет нам элегантно делать вещи, реализованными довольно неуклюже в других фрейворках (например, для циклов внутри <dl>/<dt>/<dd>).
Оставшаяся часть кода может быть реализована с помощью идиом, которые мы уже использовали. Полное представление может выглядеть так:
todo.view = function() {
return m("html", [
m("body", [
m("input", {onchange: m.withAttr("value", todo.vm.description), value: todo.vm.description()}),
m("button", {onclick: todo.vm.add}, "Add"),
m("table", [
todo.vm.list.map(function(task, index) {
return m("tr", [
m("td", [
m("input[type=checkbox]", {onclick: m.withAttr("checked", task.done), checked: task.done()})
]),
m("td", {style: {textDecoration: task.done() ? "line-through" : "none"}}, task.description()),
])
})
])
])
]);
};
Вот основные моменты данного шаблона:
- Шаблон отрисован как потомок (неявного в случае, если отсутствует явный) элемента <html> в документе.
- Поле текстового ввода input сохраняет свое значение в геттер-сеттер
todo.vm.description, определенный ранее. - Кнопка button вызывает при нажатии метод
todo.vm.add. - Таблица выводит список всех существующих задач (to-do) в случае, если они имеются.
- Чекбоксы сохраняют свое значение в геттер-сеттер
task.done.. - Описание (description) задачи становится зачеркнутым с использованием CSS в случае, если задача отмечена как выполненная.
- После обновления информации перерисовываются только измененные данные, а не весь шаблон.
До настоящего времени мы использовали m.render для ручного обновления отображения после того, как изменили данные. В то же время можно задействовать систему автообновления отображаемых элементов :http://mithril.js.org/auto-redrawing.html путем инициализации компонента todo с помощью m.mount:
//отобразить компонент todo внутри узла DOM document
m.mount(document, {controller: todo.controller, view: todo.view});
Система автообновления отображаемых элементов в Mithril отслеживает состояние контроллера и перерисовывает представление тогда, когда определяет, что контроллер завершил выполнение кода, в том числе асинхронные вызовы AJAX. Аналогично этому, она интеллектуально ждет завершения асинхронных вызовов внутри обработчиков событий перед обновлением выводимой информации.
Более детально изучить работу эвристики системы автообновления можно здесь :http://mithril.js.org/auto-redrawing.html
Итоги¶
Вот полный код нашего приложения
<!doctype html>
<script src="mithril.min.js"></script>
<script>
//this application only has one component: todo
var todo = {};
//for simplicity, we use this component to namespace the model classes
//the Todo class has two properties
todo.Todo = function(data) {
this.description = m.prop(data.description);
this.done = m.prop(false);
};
//the TodoList class is a list of Todo's
todo.TodoList = Array;
//the view-model tracks a running list of todos,
//stores a description for new todos before they are created
//and takes care of the logic surrounding when adding is permitted
//and clearing the input after adding a todo to the list
todo.vm = (function() {
var vm = {}
vm.init = function() {
//a running list of todos
vm.list = new todo.TodoList();
//a slot to store the name of a new todo before it is created
vm.description = m.prop("");
//adds a todo to the list, and clears the description field for user convenience
vm.add = function() {
if (vm.description()) {
vm.list.push(new todo.Todo({description: vm.description()}));
vm.description("");
}
};
}
return vm
}())
//the controller defines what part of the model is relevant for the current page
//in our case, there's only one view-model that handles everything
todo.controller = function() {
todo.vm.init()
}
//here's the view
todo.view = function() {
return m("html", [
m("body", [
m("input", {onchange: m.withAttr("value", todo.vm.description), value: todo.vm.description()}),
m("button", {onclick: todo.vm.add}, "Add"),
m("table", [
todo.vm.list.map(function(task, index) {
return m("tr", [
m("td", [
m("input[type=checkbox]", {onclick: m.withAttr("checked", task.done), checked: task.done()})
]),
m("td", {style: {textDecoration: task.done() ? "line-through" : "none"}}, task.description()),
])
})
])
])
]);
};
//initialize the application
m.mount(document, {controller: todo.controller, view: todo.view});
</script>
Этот пример доступен на jsFiddle :http://jsfiddle.net/fbgypzbr/16/ . Также на jsfiddle доступен расширенный пример :http://jsfiddle.net/glebcha/q7tvLxsa/
Замечания по архитектуре¶
Idiomatic Mithril code is meant to apply good programming conventions and be easy to refactor.