Адаптивный дизайн и медиа-запросы

Адаптивный дизайн в приложениях на Mithril строится на сочетании возможностей CSS и реактивной модели представлений, которую предоставляет сам фреймворк. Mithril не навязывает собственную систему работы с адаптивностью, что позволяет использовать проверенные подходы веб-разработки и гибко интегрировать их в компонентную архитектуру.

Mithril ориентирован на создание одностраничных приложений с минимальным весом и высокой производительностью. Адаптивный дизайн в этом контексте решает несколько задач:

  • корректное отображение интерфейса на экранах разного размера;
  • оптимизация пользовательского опыта для мобильных и десктопных устройств;
  • управление сложностью интерфейса без дублирования логики.

Адаптивность в Mithril обычно разделяется на два уровня:

  • визуальный уровень (CSS, media queries);
  • логический уровень (условный рендеринг компонентов).

Использование CSS media queries

Основной механизм адаптивного дизайна — медиа-запросы CSS. Mithril никак не ограничивает их использование, так как он отвечает только за генерацию DOM-дерева.

Пример базового медиа-запроса:

.container {
    display: flex;
    flex-direction: row;
}

@media (max-width: 768px) {
    .container {
        flex-direction: column;
    }
}

Компонент Mithril при этом остаётся неизменным:

const Layout = {
    view() {
        return m(".container", [
            m(".sidebar", "Sidebar"),
            m(".content", "Content")
        ])
    }
}

Такой подход предпочтителен, когда адаптация касается только внешнего вида, а не логики приложения.

Адаптивность через условный рендеринг

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

Определение ширины экрана

Ширина экрана может храниться в состоянии:

const Screen = {
    width: window.innerWidth
}

window.addEventListener("resize", () => {
    Screen.width = window.innerWidth
    m.redraw()
})

Использование в компоненте:

const Header = {
    view() {
        return Screen.width < 768
            ? m(".header-mobile", "Mobile Header")
            : m(".header-desktop", "Desktop Header")
    }
}

Ключевой момент — явный вызов m.redraw() при изменении глобального состояния вне жизненного цикла Mithril.

Инкапсуляция логики адаптивности

Для уменьшения связности рекомендуется выносить логику определения типа экрана в отдельный модуль.

const Media = {
    isMobile() {
        return window.matchMedia("(max-width: 768px)").matches
    }
}

Использование:

const Menu = {
    view() {
        if (Media.isMobile()) {
            return m(".menu-mobile")
        }
        return m(".menu-desktop")
    }
}

Такой подход упрощает тестирование и централизует работу с медиаправилами.

Использование matchMedia и подписок

window.matchMedia позволяет реагировать на изменение условий без постоянного прослушивания resize-событий.

const mediaQuery = window.matchMedia("(max-width: 768px)")

const MediaState = {
    mobile: mediaQuery.matches
}

mediaQuery.addEventListener("change", e => {
    MediaState.mobile = e.matches
    m.redraw()
})

Компонент:

const Layout = {
    view() {
        return MediaState.mobile
            ? m(MobileLayout)
            : m(DesktopLayout)
    }
}

Этот способ более точен и экономичен, чем отслеживание изменения размеров окна.

Адаптивные компоненты и композиция

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

const MobileCard = {
    view({ attrs }) {
        return m(".card.mobile", attrs.title)
    }
}

const DesktopCard = {
    view({ attrs }) {
        return m(".card.desktop", [
            m("h3", attrs.title),
            m("p", attrs.description)
        ])
    }
}

Контейнер:

const Card = {
    view(vnode) {
        return MediaState.mobile
            ? m(MobileCard, vnode.attrs)
            : m(DesktopCard, vnode.attrs)
    }
}

Такой подход:

  • сохраняет читаемость кода;
  • исключает сложные условия внутри одного шаблона;
  • облегчает поддержку.

Адаптивная маршрутизация

Иногда адаптивность затрагивает не только компоненты, но и маршруты.

m.route(document.body, "/", {
    "/": {
        render() {
            return MediaState.mobile
                ? m(MobileHome)
                : m(DesktopHome)
        }
    }
})

Этот приём используется для радикально разных интерфейсов при сохранении общей бизнес-логики.

Работа с CSS-in-JS и адаптивностью

При использовании CSS-in-JS (например, через inline-стили) медиа-запросы теряют часть выразительности. В таких случаях адаптивность чаще переносится на уровень логики.

const Box = {
    view() {
        const style = MediaState.mobile
            ? { padding: "8px" }
            : { padding: "24px" }

        return m(".box", { style }, "Content")
    }
}

Этот способ допустим для простых сценариев, но не заменяет полноценные CSS-медиа-запросы.

Производительность и redraw-стратегия

Частые изменения состояния, связанные с адаптивностью, могут приводить к лишним перерисовкам. Mithril предоставляет возможность ограничивать redraw:

mediaQuery.addEventListener("change", e => {
    MediaState.mobile = e.matches
    m.redraw.strategy("diff")
    m.redraw()
})

Также важно избегать вычислений размеров внутри view, перенося их в состояние или lifecycle-методы.

Интеграция с современными CSS-подходами

Mithril отлично сочетается с:

  • Flexbox и Grid;
  • container queries;
  • utility-CSS (например, Tailwind).

Компонентная модель фреймворка позволяет использовать адаптивные классы без дополнительной логики:

m(".grid grid-cols-3 md:grid-cols-1", ...)

В этом случае вся адаптивность полностью выносится в CSS, а Mithril отвечает только за структуру.

Стратегия выбора подхода

Выбор между CSS-медиа-запросами и условным рендерингом определяется уровнем изменений:

  • только внешний вид — CSS;
  • изменение структуры — условный рендеринг;
  • разные сценарии взаимодействия — отдельные компоненты;
  • разные маршруты — адаптивная маршрутизация.

Такое разделение позволяет сохранять простоту компонентов и не перегружать их логикой, не относящейся напрямую к отображению данных.