Руководство 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 с тремя полями:

  1. list - свойство, являющееся просто массивом;
  2. description - свойство, являющееся функцией m.prop (геттер-сеттер) с пустой строкой в качестве начального значения;
  3. 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.