Загрузка данных и состояния загрузки

Mithril.js предоставляет лёгкий и эффективный механизм работы с асинхронными операциями и состояниями загрузки, что особенно важно при построении одностраничных приложений с динамическим контентом. Основные инструменты для этих задач — m.request и управление состоянием компонентов через свойства объекта state или локальные переменные.


Асинхронные запросы с m.request

m.request является встроенным способом выполнения HTTP-запросов. Основные параметры:

m.request({
    method: "GET",
    url: "/api/data",
    params: { limit: 10 },
})
.then(function(result) {
    console.log(result);
})
.catch(function(error) {
    console.error(error);
});

Особенности:

  • Возвращает Promise, что позволяет использовать .then() и .catch().
  • Автоматически парсит JSON-ответы.
  • Поддерживает метод params для передачи query-параметров и body для POST-запросов.

Для улучшения читаемости кода часто используют синтаксис async/await:

async function loadData() {
    try {
        const data = await m.request({ method: "GET", url: "/api/data" });
        console.log(data);
    } catch (error) {
        console.error(error);
    }
}

Управление состоянием загрузки

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

  1. Локальные свойства компонента
const DataComponent = {
    loading: true,
    data: null,
    oninit: async function() {
        try {
            this.data = await m.request({ method: "GET", url: "/api/data" });
        } catch (e) {
            console.error(e);
        } finally {
            this.loading = false;
        }
    },
    view: function() {
        if (this.loading) return m("p", "Загрузка данных...");
        return m("ul", this.data.map(item => m("li", item.name)));
    }
};

Ключевые моменты:

  • Свойство loading используется для отображения индикатора.
  • Асинхронная функция в oninit гарантирует, что запрос начнётся при инициализации компонента.
  • Блок finally обеспечивает корректное завершение состояния загрузки независимо от результата запроса.
  1. Использование state объекта

Для более сложных компонентов удобно хранить данные в объекте state:

const DataComponent = {
    state: {
        loading: true,
        data: [],
        error: null
    },
    oninit: async function(vnode) {
        try {
            vnode.state.data = await m.request({ method: "GET", url: "/api/data" });
        } catch (e) {
            vnode.state.error = e;
        } finally {
            vnode.state.loading = false;
        }
    },
    view: function(vnode) {
        const { loading, data, error } = vnode.state;
        if (loading) return m("p", "Загрузка данных...");
        if (error) return m("p.error", "Ошибка загрузки данных");
        return m("ul", data.map(item => m("li", item.name)));
    }
};

Преимущества vnode.state:

  • Отделение логики состояния от объекта компонента.
  • Поддержка нескольких экземпляров одного компонента с разными состояниями.
  • Упрощение управления ошибками и обработкой асинхронных операций.

Обработка ошибок и повторные запросы

Асинхронные операции требуют продуманной обработки ошибок и возможности повторной загрузки:

const DataComponent = {
    state: { loading: false, data: [], error: null },
    oninit: function(vnode) { vnode.state.loadData(); },
    loadData: async function() {
        this.state.loading = true;
        this.state.error = null;
        try {
            this.state.data = await m.request({ url: "/api/data" });
        } catch (e) {
            this.state.error = "Не удалось загрузить данные";
        } finally {
            this.state.loading = false;
        }
    },
    view: function(vnode) {
        const { loading, data, error } = vnode.state;
        if (loading) return m("p", "Загрузка...");
        if (error) return m("div",
            m("p.error", error),
            m("button", { onclick: () => vnode.state.loadData() }, "Повторить попытку")
        );
        return m("ul", data.map(item => m("li", item.name)));
    }
};

Особенности:

  • Повторный запрос реализован через метод loadData.
  • Ошибка отображается пользователю с возможностью повторной попытки.
  • Состояние загрузки и ошибки управляются централизованно в state.

Асинхронная загрузка и маршрутизация

При работе с m.route полезно загружать данные до отображения страницы. Mithril поддерживает асинхронные маршруты:

m.route(document.body, "/users", {
    "/users": {
        onmatch: async function() {
            const users = await m.request({ url: "/api/users" });
            return {
                view: () => m("ul", users.map(u => m("li", u.name)))
            };
        }
    }
});

Преимущества:

  • Данные подгружаются до рендера компонента.
  • Не требуется промежуточное состояние загрузки внутри компонента.
  • Позволяет реализовать “loading screen” для всей страницы.

Использование m.stream для реактивного состояния

Mithril предоставляет m.stream, который позволяет автоматически обновлять представление при изменении данных:

const dataStream = m.stream([]);

const DataComponent = {
    oninit: async function() {
        const result = await m.request({ url: "/api/data" });
        dataStream(result);
    },
    view: function() {
        if (dataStream().length === 0) return m("p", "Загрузка...");
        return m("ul", dataStream().map(item => m("li", item.name)));
    }
};

Особенности:

  • m.stream создаёт реактивную переменную, автоматически обновляющую DOM при изменении.
  • Позволяет использовать функциональный подход без прямого управления состоянием компонента.

Рекомендации по организации кода

  • Разделять логические слои: запрос данных, управление состоянием и отображение.
  • Сохранять флаги загрузки и ошибки внутри компонента или vnode.state.
  • Использовать async/await для улучшения читаемости асинхронного кода.
  • Для больших приложений применять потоковые данные (m.stream) или отдельные сервисы для загрузки данных.

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