javascript

За что я люблю именно Mithril (он же MithrilJS)

  • понедельник, 6 ноября 2017 г. в 03:13:19
https://habrahabr.ru/post/341708/
  • JavaScript


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


На написание статьи меня подтолкнули простые причины: идет война за сердца и умы разработчиков, и многие уважаемые софтверные гиганты считают своей обязанностью облегчить участь девелопера (что хорошо, кстати). При этом не стесняясь сломать ему мозг и нервную систему (а вот это не очень). Так сказать, во имя счастья будущих поколений. Может быть, я ошибаюсь, но хочу поделиться с вами информацией об инструменте, который достаточно давно открыл для себя и с тех пор не ем кактусы, как те мыши: 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.forEach(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.forEach(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.forEach(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