Mithril — JavaScript-фреймворк размером около десяти килобайт, которого достаточно для полноценного одностраничного приложения. В нём нет реактивных сигналов, компилятора и сотни зависимостей: есть функция m(), виртуальный DOM, роутер и HTTP-клиент. В 2026 году это разумный выбор для админок, внутренних инструментов и небольших сайтов, где важен контроль над кодом. Ниже — практика: установка, компоненты, роутинг, запросы, сборка и типичные ошибки.
Почему Mithril
Mithril занимает нишу между ванильным JavaScript и крупными фреймворками. Он не навязывает структуру каталогов и не требует JSX: представление описывается вызовами m("div", {class: "card"}, ...). Виртуальный DOM пересчитывается после каждого события, а diff применяет только реальные изменения.
- Один пакет без обязательных зависимостей.
- Роутер, HTTP-клиент и компоненты в комплекте.
- Понятный жизненный цикл и предсказуемый redraw.
- Быстрый старт без сборщика, но с поддержкой esbuild и Vite.
- Тесты через собственный раннер ospec.
Ограничения тоже честные: нет серверного рендеринга из коробки, нет готовой экосистемы компонентов и меньше готовых решений для сложных форм. Для большого продукта с десятком команд это может стать проблемой.
Первое приложение
Достаточно пакета mithril и любого статического сервера. Для сборки возьмём esbuild: он ставится одним пакетом и собирает бандл за доли секунды. Если проект на TypeScript, добавьте типы из @types/mithril.
npm init -y
npm install mithril
npm install --save-dev esbuild ospec
mkdir -p src dist
Минимальное приложение — компонент с методом view. Функция m.mount привязывает его к элементу и берёт на себя перерисовку.
import m from "mithril";
const App = {
view() {
return m("main", [
m("h1", "Список задач"),
m("p", "Каркас готов"),
]);
},
};
m.mount(document.getElementById("app"), App);
Сборка командой npx esbuild src/main.js --bundle --minify --outfile=dist/app.js даёт один файл. Для разработки добавьте --watch и --sourcemap, а HTML подключите к dist/app.js. Никакой магии: браузер получает обычный скрипт.
Компоненты и состояние
Состояние хранят в замыкании компонента или в vnode.state. Замыкание удобнее для переиспользуемых компонентов: у каждого экземпляра свои данные. Метод oninit вызывается до первого view и получает атрибуты.
function Counter() {
let count = 0;
return {
oninit(vnode) {
count = vnode.attrs.start ?? 0;
},
view() {
return m("div", [
m("span", `Значение: ${count}`),
m("button", {
onclick: () => { count++; },
}, "+1"),
]);
},
};
}
m.mount(document.getElementById("app"), {
view: () => m(Counter, { start: 3 }),
});
После обработчика Mithril сам вызывает redraw. Если состояние меняется в асинхронном колбэке вне m.request, вызовите m.redraw() вручную. Не мутируйте массив в цикле без необходимости: diff сравнивает vnode по ключам и типу.
Роутинг
Роутер связывает URL с компонентом. Карта маршрутов задаётся объектом, а параметры приходят через m.route.param. Переход выполняется через m.route.set, а ссылки лучше делать компонентом m.route.Link, чтобы не терять историю и среднюю кнопку мыши.
import m from "mithril";
const Home = { view: () => m("h2", "Главная") };
const Task = {
view: () => m("h2", `Задача ${m.route.param("id")}`),
};
m.route(document.getElementById("app"), "/", {
"/": Home,
"/task/:id": Task,
});
Для ленивой загрузки используйте резолвер с onmatch: он может вернуть промис и загрузить модуль по требованию. Маршрут с ... в конце, например /:path..., ловит остаток пути. Не забудьте про m.route.prefix, если приложение живёт в подкаталоге.
Запросы к API
m.request возвращает промис и автоматически делает redraw после завершения. Параметр background: true отключает перерисовку для фоновых опросов. Ошибки приходят с кодом и телом ответа, поэтому их нужно разбирать, а не только показывать «что-то пошло не так».
const api = {
list: () => m.request({ method: "GET", url: "/api/tasks" }),
create: (title) => m.request({
method: "POST",
url: "/api/tasks",
body: { title },
}),
};
async function load() {
try {
const tasks = await api.list();
console.log(tasks);
} catch (e) {
console.error(e.code, e.response);
}
}
Для отмены сохраните xhr через опцию config и вызовите abort(). Таймаут задаётся полем timeout, а куки — withCredentials. Не храните токен в localStorage без необходимости: HTTP-only cookie безопаснее при правильной настройке CORS.
Списки, ключи и жизненный цикл
Список vnode должен иметь уникальный key. Без него diff сопоставляет элементы по позиции и может переиспользовать DOM неверно, особенно после удаления из середины. Ключ ставят на корневой элемент строки, а не на дочерние.
function TaskList() {
let items = [{ id: 1, title: "Первая" }];
return {
view: () => m("ul", items.map((item) =>
m("li", { key: item.id }, item.title)
)),
};
}
Хуки oncreate и onremove полезны для интеграции с картами, редакторами и сторонними виджетами. onbeforeremove позволяет проиграть анимацию: верните промис, и элемент удалится после его завершения. onbeforeupdate с возвратом false пропускает перерисовку поддерева.
Сборка и тесты
В package.json достаточно двух скриптов: сборка и тесты. ospec запускается в Node и не требует браузера, если тестировать чистые функции и компоненты через вызов view.
{
"scripts": {
"build": "esbuild src/main.js --bundle --minify --outfile=dist/app.js",
"test": "ospec test/*.js"
}
}
Тест компонента проверяет структуру vnode, а не реальный DOM. Для интеграционных сценариев понадобится jsdom или Playwright. Не смешивайте эти уровни: быстрые тесты на логику и отдельный набор на критичные пользовательские пути.
Подводные камни
Первая ошибка — думать, что Mithril реактивен как Vue. Здесь нет вычисляемых свойств: view читает текущие данные и вызывается заново. Вторая — забыть ключи в списках. Третья — вставлять HTML через m.trust без санитизации: это прямой путь к XSS. Обычный текст Mithril экранирует сам.
Четвёртая — тяжёлые вычисления внутри view: они выполняются на каждую перерисовку. Выносите сортировку и фильтрацию в обработчики или кэшируйте результат. Пятая — глобальное состояние без плана: для небольшого приложения хватит модуля с объектом, но при росте добавьте явные функции доступа.
Итог
Mithril даёт компактный и предсказуемый фундамент для интерфейса: hyperscript, компоненты, роутер и HTTP-клиент в одном пакете. Начните с m.mount и простого view, затем добавьте маршруты и m.request. Следите за ключами списков, не доверяйте m.trust и выносите тяжёлую логику из view. Для админки или внутреннего инструмента этого достаточно, а для большого продукта заранее оцените экосистему и серверный рендеринг.