Здравствуйте, дорогие читатели. Если вы открыли этот пост, значит, паутинная разработка переднего края (то есть, веб фронтэнд девелопмент, я хотел сказать) трогает вас за живое. И прежде, чем вы начете кидать помидоры благодарить рассказчика, прошу дочитать… хотя бы до середины.
На написание статьи меня подтолкнули простые причины: идет война за сердца и умы разработчиков, и многие уважаемые софтверные гиганты считают своей обязанностью облегчить участь девелопера (что хорошо, кстати). При этом не стесняясь сломать ему
В принципе, 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 attr: TodoAttrs = vnode.attrs // вот оно 1
var id = attr.id
....
const Todos = {
view(vnode) {
return(
// опять же, стрелочная функция
store.todosOrdered.forEach(todoId => m(Todo, <TodoAttrs>{id: todoId})) // вот оно 2
)
}
}
Я прошу снобов не кидаться на меня и не ругать за надуманный пример. Понятно, что он надуман. Но в Mithril действительно все легко — он очень близок к аутентичному JS:
- Компонент — это объект с функцией view, возвращающей дерево VirtualDOM. Есть хуки жизненного цикла, если надо (oninit и т.п)
- При изменении аргументов компонент перерисовывается.
- При изменении внешних объектов, на которые ссылается метод view() компонента, из других компонентов, компонент перерисовывается.
- Если вы сделали какую-то магию за пределами компонентов и хотите их обновить, вызывайте m.redraw(). Нужные компоненты перерисуются.
- Profit.
В итоге, что мы здесь видим? (я имею в виду в том числе и листинг выше) Несколько правильных, на мой взгляд вещей:
- Мы не сходим с ума от того, как обмениваться информацией между компонентами. React + Redux? React + MobX? Или хватит Higher Order Componens? Может лучше Vue + Vuex?.. Зачем?! (это я уже кричу и плачу). Возьмите внешний объект (или несколько) и используйте как хранилище состояния.
- Адептам 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/
Автор: Northborn