Здравствуйте, дорогие читатели. Если вы открыли этот пост, значит, паутинная разработка переднего края (то есть, веб фронтэнд девелопмент, я хотел сказать) трогает вас за живое. И прежде, чем вы начнете кидать помидоры благодарить рассказчика, прошу дочитать… хотя бы до середины.


На написание статьи меня подтолкнули простые причины: идет война за сердца и умы разработчиков, и многие уважаемые софтверные гиганты считают своей обязанностью облегчить участь девелопера (что хорошо, кстати). При этом не стесняясь сломать ему мозг и нервную систему (а вот это не очень). Так сказать, во имя счастья будущих поколений. Может быть, я ошибаюсь, но хочу поделиться с вами информацией об инструменте, который достаточно давно открыл для себя и с тех пор не ем кактусы, как те мыши: Mithril (MithrilJS).


В принципе, Mithril не является серебряной пулей или крестом животворящим, но он стоит того, чтобы его попробовать и использовать.


В двух словах, это React-like фреймворк без заморочек, связанных с управлением состоянием, с роутингом и удобными методами серверных запросов из коробки. API очень компактное и умещается на одной странице. Фреймворк пропагандирует питоновский принцип "должен быть один единственный и очевидный способ действия". Он компактный (6кБ gzip) и быстрый.


"Пфф", — подумает React-разработчик, — "все как Реакт, зачем тогда это нужно?"


Даю ответ: когда вы программируете на React, вы думаете не столько о логике приложения, сколько о том, как обуздать зубодробительное управление состоянием приложения. Киньте в меня помидор, если это не так. Flux, Redux, MobX, состояние в компонентах, компоненты высшего порядка, стейтлесс-компоненты, компоненты-функции и т.д. и т.п. Не забываем, что еще надо выбрать роутер (или сделать самому простой) и библиотеку для серверного взаимодействия.


"Эй, используй redux-saga, redux-thunk, react-easy-redux-saga-inegrator-with-router-and-holy-shit! И не забудь create-react-app от самого FB, чтобы с настройками проекта полегче".
"Эй, берешь Angular, это же фреймворк, а не какой-то шаблонизатор. Но не забудь скомпилировать шаблоны сразу, чтобы размер бандла получился меньше 2МБ, запомни синтакис геттеров-сеттеров {([()])} и полюби RxJS — это реальное вау, у нас даже есть один перец в команде, который понял, как на нем пайпы делать".


Хмм...


А мне просто SPA хочется сделать. Может даже на ES5. Может даже без Webpack/Grunt/Gulp/Browserify/TypeScript/Flow. Чтобы подключить скрипт на странице и погнали.
"Да ты еретик! Сжечь его!" — воскрикнут многие.


Стоп.


Если честно, я не пишу SPA на ES5, но небольшие скрипты пишу. И если это не часть большого проекта, то хотел бы, чтобы когда-нибудь мог просто открыть скрипт и модифицировать его, а не смотреть со слезами на бандл и думать, где искать исходники трехстрочного скрипта (который даже не я писал).


О Mithril подробнее


Это действительно React-like. Тут нас, можно сказать, переучили. JS-first, все есть жаваскрипт… Да, вариант. Будем отрабатывать эту версию.


Вот как выглядит код на Mithril. Я буду писать на ES5. Вы с легкостью добавите импорты, замените var на let и const, опустите ненужные function и примените стрелочные функции при необходимости.


Просто я хочу, чтобы этот пост могли читать все, а не только вы, многоуважаемые ниндзи.


// store.js
var store = {
    todosOrdered: [1],
    todoDetails: {
        // default for example
        1: {
            title: 'Ваша первая задача',
            isActive: true
        }
    }

};

// components/Todo.js

// Хэндлеры для правильной связки с объектом состояния
// из компонента вызываем хэндлер
// хэндлер меняет состояние
// это удобно для отладки
function handleTodoCheck(id) {
    store.todoDetails[id].isActive = !store.todoDetails[id].isActive
}

function handleTodoTitleChange(id, value) {
    // здесь могла быть запись на сервер
    console.log('id=' + id + ";value=" + value);
    store.todoDetails[id].title = value
}

// это просто объект
var Todo = {

    // функция рендеринга
    view: function (vnode) {
        var id = vnode.attrs.id;           // получаем переданные аргументы
        var todo = store.todoDetails[id];  // обращаемся к внешнему хранилищу без магии и дополнительных инструментов

        return(
            // строим объект-представление
            // класс задан в CSS-нотации, в данном случае аналог 'div.todo'
            m('.todo', {
                // обрабатываем условия
                // добавляем условный класс
                class: todo.isActive ? "active" : "no-active"
            }, [
                m('input', {
                    'type': 'checkbox',
                    onchange: handleTodoCheck.bind(null, id)  // настраиваем хэндлер с привязанным аргументом
                }),
                m('input', {
                    value: todo.title,
                    // слушаем изменения описания и меняем состояние
                    onchange: m.withAttr('value', handleTodoTitleChange.bind(null, id))
                })
            ])
        )
    }
}

// components/Todos.js
var Todos = {
    view: function (vnode) {
        return(
            m('.todos', 
                // проходимся по списку
                store.todosOrdered.map(function(todoId) {
                    // возвращаем компонент с переданными аргументами
                    return m(Todo, {id: todoId})
                }),
            )
        )
    }
}

Здесь как бы все про отрисовку и стейт-менеджмент. Соль и перец добавьте по вкусу.
У нас есть иерархия компонентов (один компонент — одна задача), они взаимодействуют с пользователем и обновляют наше представление.


В данном примере есть отношение родитель-потомок, но можно было и без него. Здесь нет чистых функций и машины времени.


Но тут все очень просто, есть логирование и понятно что происходит. При желании все можно снабдить дополнительной отладочной информацией.


Если же я хочу делать проверку типов, то я использую TypeScript и осуществляю проверку типов аргументов с помощью интерфейсов. Вроде этого:


interface TodoAttrs {
    id: number
}

// Здесь нет ";", мы же все это скормим транспайлеру
// const не только для функций, да
const Todo = {
    view(vnode) {
        let attrs: TodoAttrs = vnode.attrs // вот оно 1
        let id = attrs.id
        ....

const Todos = {
    view(vnode) {
        return(
            // опять же, стрелочная функция
            store.todosOrdered.map(todoId => m(Todo, <TodoAttrs>{id: todoId})) // вот оно 2
        )
    }
}

Я прошу снобов не кидаться на меня и не ругать за надуманный пример. Понятно, что он надуман. Но в Mithril действительно все легко — он очень близок к аутентичному JS:


  1. Компонент — это объект с функцией view, возвращающей дерево VirtualDOM. Есть хуки жизненного цикла, если надо (oninit и т.п)
  2. При изменении аргументов компонент перерисовывается.
  3. При изменении внешних объектов, на которые ссылается метод view() компонента, из других компонентов, компонент перерисовывается.
  4. Если вы сделали какую-то магию за пределами компонентов и хотите их обновить, вызывайте m.redraw(). Нужные компоненты перерисуются.
  5. Profit.

В итоге, что мы здесь видим? (я имею в виду в том числе и листинг выше) Несколько правильных, на мой взгляд вещей:


  1. Мы не сходим с ума от того, как обмениваться информацией между компонентами. React + Redux? React + MobX? Или хватит Higher Order Componens? Может лучше Vue + Vuex?.. Зачем?! (это я уже кричу и плачу). Возьмите внешний объект (или несколько) и используйте как хранилище состояния.
  2. Адептам html-first (привет Vue и Riot, да и Angular) в качестве ответа на возможный вопрос по синтаксису:
    а во что в итоге превращается DSL для реализации циклов, условий, привязки переменных, слушателей событий и т.п? Во что он превратится через пару лет и пару мажорных обновлений фреймворка? На мой взгляд, в не очень очевидный синтаксис с "переменными внутри текста". И это критично. И еще приложение (и шаблоны) на Mithril легко дебажить.

Отдельно про JSX


Можно легко использовать JSX (а также Babel, Webpack и все остальное), мануал есть в официальной документации.


На JSX это будет выглядеть так:


// components/Todos.jsx

const Todos = {
    view(vnode) {
        return(
            <div className="todos">
                {store.todosOrdered.map(todoId => <Todo id={todoId} />)}
            </div>
        )
    }
}

В общем, по сравнению с hyperscript типа m('span.cool', "I'm cool"), на вкус и цвет все фломастеры разные. Я использую гиперскрипт (такое название, да), да и автор фреймворка рекомендует.


И еще


Вообще, у Mithril на момент публиции 8k звезд на гитхабе, — может это и не так много, но больше, чем у некоторых весьма обсуждаемых языков программирования. Фреймворк зрелый: текущий репозиторий на гитхабе с 2014 года, двести с лишним контриьбюторов и последнее обновление несколько дней назад.


Я призываю вас прочитать документацию и воспользоваться удобным функциональным инструментом в работе, тем более, что там и читать-то особо нечего по сравнению с фолиантом "Angular для профессионалов" (тут не могу не остановиться и не порадоваться, что создатели этого замечательного инструмента сделали его таким, чтобы потом можно было зарабытывать на обучении. Берите пример, как говорится).


Хочу напомнить, что у Mihtril есть роутинг


m.route('/man', ManPage)

И удобный метод серверных запросов


m.request({
    method: "POST",
    url: "/todo",
    data: {id: id, title: title}, // добавим нашу задачку
    withCredentials: true,  // отправим куки
})
.then(function(data) {  // data - это распарсенный JSON
    console.log(data)
})

Как подключить? Просто 6кБ gzip:


<script src="https://unpkg.com/mithril"></script>

Все описанное выше будет работать.


Ну или npm со всеми свистелками, само собой.
Есть очень бойкое сообщество в Gitter


Если есть интерес, могу (постараться) перевести документацию. Действительно, я считаю, что Mithril того стоит.


Прошу не ругать меня за использование англицизмов, ведь, в конце концнов, мы давно работаем за компьютрами, а не за ЭВМ.


Это все, что я хотел сказать. Спасибо.


Если где ошибся/не разглядел/не понял/не оценил, готов к конструктивной критике.


Да, забыл, вот ссылки на имеющиеся бенчмарки:
https://lhorie.github.io/todomvc-perf-comparison/todomvc-benchmark/
https://developit.github.io/preact-perf/


Репозиторий на GitHub

Комментарии (18)


  1. x4fab
    05.11.2017 16:50

    Удваиваю, отличная штука, простая, удобная и при желании расширяемая (скажем, можно добавить свою версию .prop(), сохраняющую значение в localStorage или вызывающую колбек). Да и Flarum написан на Mithril, о чём-то это да говорит.


  1. Mellorn
    05.11.2017 16:58

    Заинтересовало, спасибо.
    И да, переведённая документация — было бы очень даже неплохо.


  1. gnaeus
    05.11.2017 17:06
    +1

    Судя по документации Mithril, его autoredraw равнозначен вызову this.forceUpdate() в корневом компоненте React. И перегенерации всего vdom tree. А происходит это на каждый чих.


    Если беспокоит именно размер загружаемых скриптов – можно использовать preact. Он вообще 3 кБ весит.


    1. Northborn Автор
      05.11.2017 17:53

      Вы правы, но не во всем.
      С той же страницы:
      «Mithril также может избежать автоматической перерисовки, если частота запрашиваемого рендеринга превышает один кадр анимации (обычно около 16 мс). Это означает, например, что при использовании быстрых событий, таких как onresize или onscroll, Mithril автоматически регулирует количество перерисовок, чтобы избежать лагов».
      То есть, пусть считает когда надо, на скорость UI не повлият.

      При желании вы можете отключить autoredraw c помощью
      function clickHandler(e) {
      // теперь redraw не запустится при вызове обработчика
      e.redraw = false
      }
      Также он не пересчитывает VDOM для компонентов, которые в данный момент не отображаются на уровне роутера. В итоге, не так уж и много считать выходит.

      Опять же, бенчмарки подтверждают скорострельность инструмента, а в Chrome Development Tools видно, что страницы не грузят процессор.

      По сравнению с React, думаю, тут вопрос в том числе и в том, на что уходят циклы процессора при использовании различных дополнительных библиотек для управления состоянием.

      Согласен про preact, но Mithril умеет все, что нужно «у целом», в отличие от того же preact, который копирует зону ответственности React, и будет требовать дополнительной обвязки.
      Такой расклад.


      1. TheShock
        05.11.2017 20:00

        То есть, пусть считает когда надо, на скорость UI не повлият.

        Почему не повлияет? Да, множество запросов стекаются в один и рендерятся только во время рендеринга кадра, но из-за большого дерева вполне может тормозить. То есть, скажем, у нас дерево рендерится 500 мс. Да, если вызвать forceUpdate трижды, то рендер будет длиться 500 мс, а не 1500. Но это не значит, что ЮИ не тормознет.

        В идеале — если поменялись пропсы/стейт компонента — этот компонент нужно перерендерить, при этом у его дочерних компонентов перерендеривать только те, у которых поменялись пропсы/стейт. Тогда будет минимум пересчетов.

        Также он не пересчитывает VDOM для компонентов, которые в данный момент не отображаются на уровне роутера

        Ну это само собою разумеется.


      1. TheShock
        05.11.2017 20:29

        Опять же, бенчмарки подтверждают скорострельность инструмента, а в Chrome Development Tools видно, что страницы не грузят процессор

        Бенчмарк странный — он в цикле кликает на 50 элементов, соответственно нету смысла перерисовывать по одному элементу и перерисовка всей страницы выгоднее.
        В реальной жизни будет происходить клик по одному элементу 50 раз и в реальной жизни в Мифриле будет в 50 раз больше перерисовок, чем в Реакте


  1. saggid
    05.11.2017 17:32

    Я всё также за Riot.js в большинстве случаев. Очень простой, очень маленький, всё что надо делает. У нас довольно большой проект на нём написан, уже пару лет как всё работает, обновились уже с первой версии на третью — по мелочам поправили кое-какие места, но по большей части ничего даже править не нужно было.


  1. burfee
    05.11.2017 20:19

    Используем около 2 лет в на проде, успешно переехали с нулевой версии на первую. У нас довольно много кода (около 1МБ из которого наверно половина UI) и довольно жирный интерфейс.
    Насколько я понял в митриле подход с представлением похож на реактовый, а модель обновления состояния похожа на ангулярную, когда обновление происходит не когда данные поменялись, а когда возникло событие (напр. UI-обработчик, сетевой запрос, либо вручную).
    Основные плюсы — удобный, минималистичный, интуитивно понятный API, большая свобода выбора в организации кода, прозрачная архитектура. Размер как бы без разницы нам.
    На счет зрелости — не уверен, некоторые на мой взгляд принципиальные баги цветут и пахнут месяцами, хотя вроде активность есть. В целом алгоритм сравнения V-DOM туповат, с ним было много проблем. В результате не используем фрагменты, атрибут key, тем более компоненты, prop и все что было удалено в релизе 1.0. Короче научились жить с ним. Нормально в принципе, нравится больше ангуляра, но не могу сказать, что сильно рады в плане надежности, прикрутить бы к нему рендер понадежнее и было бы совсем хорошо.


  1. justboris
    05.11.2017 20:41

    Как подключить? Просто 6кБ gzip:
    <script src="https://unpkg.com/mithril"></script>

    Лучше все-таки указывать версию, чтобы ненароком ничего не сломалось: https://unpkg.com/mithril@1.1.5


  1. justboris
    05.11.2017 20:47

    Если искать маленькие SPA-фреймворки, то стоит посмотреть еще и на choo.


    В нем все так же есть компоненты, встроенные роутер и store. Тоже интересная альтернатива


    1. gnaeus
      05.11.2017 22:35

      Мне вот всегда было интересно, чем люди объясняют использование микро-фреймворков именно при разработке SPA? Я понимаю, если на нем реализована только небольшая часть страницы.


      Помню, тут была статья от DevExpress, как они используют Preact в своих виджетах.
      Ну или мобильный сайт 2gis. Но прелесть Preact именно в совместимости с "большим братом" вплоть до библиотек.


      Такие либы, как choo и mithril выглядят круто! Их разработчики заслуживают уважения.
      Но если писать целый сайт, то размер собственного кода достаточно скоро превысит размер фреймворка. И будет уже без разницы, 40 кб кода + 20 кб Vue или 40 кб кода + 5 кб микро-фреймворка. А заплатим мы за это странными багами в незрелой либе и повышенным порогом вхождения в проект.


      1. justboris
        05.11.2017 23:08

        Все верно вы рассказываете. Если веб-приложение большое, пишется целой командой разработчика, то ±40 Кб кода фреймворка погоды не сделают.


        А если приложение маленькое, и код пишется одним человеком, возможно будет оптимальнее взять фреймворк поменьше.


        Еще mithril и choo и компания очень хорошо смотрятся для встраиваемых виджетов. Тащить большой фреймворк туда не с руки, а что-то маленькое заходит идеально.


        1. hippoage
          06.11.2017 10:26

           Если код пишется одним человеком, то тем более нужно брать что-то популярное (vue/angular/react), т.к. иначе потом никто не сможет развивать проект.


          1. justboris
            06.11.2017 12:06

            Тогда получается, что абсолютно всегда нужно брать что-то популярное, а то никто не сможет развивать проект?


      1. AlexanderY
        06.11.2017 11:56

        Еще не забывайте про +200 кб шрифтов и пара мегабайт изображений. Размер кода уже давно не показатель.


  1. Northborn Автор
    06.11.2017 14:48

    Господа, господа. Акцент основного посыла не на маленьком размере, а на компактом API и наличии в фреймоворке всего, требуемого для разработки хоть виджета, хоть SPA. То есть я говорю о целостности и простоте фреймворка. Ведь для Vue вы тоже должны подключить axios или что-нибудь другое для серверных запросов, для роутинга — официальный роутер… версии 1 или 2? А какие зависимости это ломает? То есть еще вопрос, что есть «микрофреймворк», если React без обвязок отвечает только слой представления, Vue тоже. И вот когда вы начинаете делать из этого ядра фреймворк, то неожиданно у вас (и ваших коллег) получается много фреймворков на базе, в частности, ядра React.
    При этом я не призываю пересаживаться умеющих готовить React, это не за чем.

    Но, к примеру, если у вас есть другие интересы кроме фронтенда и вы хотите делать современное приложение, потратив минимум времени на изучение и внедрение, да еще и с хорошим темпом разработки, то Mithril — это реально хороший инструмент.

    При этом у вас получится расширяемое и поддерживаемое приложение при наличии минимальной способности декомпозировать код.
    Конечно, есть риск, что при смене парадигмы придется переписывать. Но ведь переписывали с ванили на JQuery, с JQuery на Backbone, потом на Angular 1, потом, скорее всего, на React. Но на самом Реакте сколько раз вы переписывали c Реакта на Реакт? Можете не отвечать: несколько раз. Потому что FB делал рендерер ленты. И сделал его. А все остальное по ходу пьесы. Сначала Flux, потом Redux, потом новое решение — MobX. Не используйте ref. Используйте функциональные компоненты. И так далее.

    Знаете, сколько раз в день я открываю документацию Mithril во время разработки? Ни разу. Столько же и StackOverflow. Потому что не за чем. Если вы понимаете JS, вы знаете Mithril. Потому что он построен на простых концепциях. У Angular 2/3/4/5 постоянно что-то не так с документацией, недавно тоже была статья о том, что люди лезут в исходники чтобы понять, что происходит и как надо делать. А это потеря времени и денег.
    Кстати, Mithril тоже работает с Redux, но он работает и без него, при этом вы можете использовать те же подходы.

    Я написал про Mithril, потому что верю, что такая вещь, как обработка пользовательского ввода, не должна занимать «бОльшую половину» мозга. Пусть останется место и время для чего-то большего. Peace, folks.


  1. vintage
    07.11.2017 00:32
    +1

    У вас кривые бенчмарки. Первый вообще для mithril ничего не оказывает, а второй работает, но замеряет лишь синхронные операции. Всё, что исполняется отложенно он не учитывает.


    Вот правильный бенчмарк, замеряющий полное время рендеринга: http://mol.js.org/app/bench/#bench=https%3A%2F%2Feigenmethod.github.io%2Ftodomvc%2Fbenchmark%2F/sample=angular2_es2015~mithril~mol~typescript-react/sort=fill


    1. Northborn Автор
      08.11.2017 05:09

      Я обновил (локально) TodoMVC для Mithril (ведь он на 0.2 для существующих бенчмарков) до 1.1.5 с применением описанного в статье подхода. Будет время, дооформлю и сделаю пулл реквест в основной репозиторий. Прогнал ваш тест у себя на ноуте. Вот цифры, получившиеся при создании и завершении 200 задач. Упорядочено по возрастанию (меньше — лучше):


      Вариант    Создание + Заврешение, мс
      $mol    1151
      Angular2+    4451
      TypeScript & React15    5257
      Riot.js    6112
      Mithril (1.1.5)    6847
      Elm     7118
      Vue.js    8833
      AngularJS    9394
      jQuery    23809

      Пару нижних вариантов для сравнения оставил. Время холодного старта здесь не учитывается.