Промисы в контексте Mithril

Mithril — лёгкий и быстрый фреймворк для создания одностраничных приложений (SPA), который активно использует асинхронные операции через промисы. Встроенные механизмы Mithril позволяют удобно управлять загрузкой данных, обновлением интерфейса и обработкой ошибок без необходимости подключения сторонних библиотек.

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

Основным способом работы с асинхронными данными в Mithril является функция m.request. Она возвращает объект Promise, что позволяет использовать привычный синтаксис .then() и .catch(), а также интегрировать его с async/await.

m.request({
    method: "GET",
    url: "/api/users"
})
.then(function(data) {
    console.log("Список пользователей:", data);
})
.catch(function(error) {
    console.error("Ошибка запроса:", error);
});

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

  • m.request автоматически сериализует запрос и парсит JSON-ответ.
  • Можно указать заголовки через поле headers.
  • Для POST-запросов используется поле body.
m.request({
    method: "POST",
    url: "/api/users",
    body: { name: "Иван", age: 30 },
    headers: { "Content-Type": "application/json" }
})
.then(response => console.log("Пользователь создан:", response))
.catch(error => console.error("Ошибка создания:", error));

Привязка промисов к состоянию компонентов

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

const UserList = {
    users: [],
    oninit: function() {
        m.request({ method: "GET", url: "/api/users" })
            .then(data => {
                UserList.users = data;
            })
            .catch(err => console.error(err));
    },
    view: function() {
        return m("ul", UserList.users.map(user => m("li", user.name)));
    }
};

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

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

Использование async/await

Для более чистого и читаемого кода можно использовать синтаксис async/await внутри методов жизненного цикла компонента.

const ProductList = {
    products: [],
    error: null,
    oninit: async function() {
        try {
            ProductList.products = await m.request({ method: "GET", url: "/api/products" });
        } catch(e) {
            ProductList.error = e.message;
        }
    },
    view: function() {
        if (ProductList.error) return m("div.error", ProductList.error);
        return m("ul", ProductList.products.map(p => m("li", p.name)));
    }
};

Преимущества использования async/await:

  • Код выглядит синхронным, что облегчает чтение.
  • Легко обрабатывать ошибки с помощью try/catch.
  • Удобно комбинировать несколько асинхронных операций последовательно.

Цепочки промисов и параллельные запросы

Mithril поддерживает использование Promise.all для выполнения нескольких запросов параллельно. Это полезно, когда нужно загрузить несколько ресурсов до отображения интерфейса.

oninit: async function() {
    try {
        const [users, posts] = await Promise.all([
            m.request({ method: "GET", url: "/api/users" }),
            m.request({ method: "GET", url: "/api/posts" })
        ]);
        this.users = users;
        this.posts = posts;
    } catch(e) {
        this.error = e.message;
    }
}

Особенности работы с несколькими промисами:

  • Все промисы запускаются одновременно, что ускоряет загрузку данных.
  • Любая ошибка одного запроса попадёт в общий блок catch.
  • Результаты доступны после завершения всех промисов в виде массива.

Автоматическая обработка прогресса и состояния загрузки

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

const DataLoader = {
    data: null,
    loading: true,
    error: null,
    oninit: async function() {
        try {
            DataLoader.data = await m.request({ method: "GET", url: "/api/data" });
        } catch(e) {
            DataLoader.error = e.message;
        } finally {
            DataLoader.loading = false;
        }
    },
    view: function() {
        if (DataLoader.loading) return m("div", "Загрузка...");
        if (DataLoader.error) return m("div.error", DataLoader.error);
        return m("pre", JSON.stringify(DataLoader.data, null, 2));
    }
};

Принципы:

  • Флаг loading позволяет отслеживать процесс запроса.
  • Блок finally гарантирует отключение индикатора после завершения операции.
  • Состояние ошибки и данных хранится отдельно для удобного отображения.

Взаимодействие с внешними библиотеками промисов

Mithril совместим с любыми стандартными Promise-библиотеками и API, такими как fetch, axios, Bluebird. Главное — сохранять реактивность через изменение состояния компонентов и вызов m.redraw() при необходимости.

import axios from "axios";

const Posts = {
    list: [],
    oninit: async function() {
        try {
            const response = await axios.get("/api/posts");
            Posts.list = response.data;
            m.redraw();
        } catch(e) {
            console.error(e);
        }
    },
    view: function() {
        return m("ul", Posts.list.map(p => m("li", p.title)));
    }
};
  • В случае использования сторонних библиотек важно вручную вызывать m.redraw() после получения данных, если библиотека не интегрирована с Mithril.

Обработка последовательных асинхронных операций

Иногда требуется выполнить серию промисов последовательно, где результат одного зависит от предыдущего. Mithril позволяет делать это через обычные цепочки промисов или async/await.

async function loadDataSequentially() {
    try {
        const user = await m.request({ method: "GET", url: "/api/user/1" });
        const posts = await m.request({ method: "GET", url: `/api/users/${user.id}/posts` });
        return { user, posts };
    } catch(e) {
        console.error("Ошибка последовательной загрузки:", e);
    }
}
  • Последовательная загрузка может быть медленнее, но необходима при зависимостях данных.
  • Легко комбинировать с состоянием компонентов для динамического обновления UI.

Интеграция с потоками данных и Mithril Streams

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

const usersStream = m.stream([]);

m.request({ method: "GET", url: "/api/users" })
    .then(usersStream);

const UserView = {
    view: () => m("ul", usersStream().map(u => m("li", u.name)))
};
  • Стрим автоматически реагирует на новые данные.
  • Промисы можно использовать как источник обновлений для потоков.