Обработка ошибок и состояний загрузки

Knockout.js предоставляет гибкий способ работы с динамическими данными в интерфейсе, однако для создания надежных приложений необходимо правильно обрабатывать ошибки и состояния загрузки. Основные механизмы для этого базируются на использовании observable, computed, а также на интеграции с асинхронными операциями.


Observable для состояния загрузки

Для отслеживания состояния загрузки данных создаются observable, которые сигнализируют о текущем статусе запроса. Обычно используют булевое значение:

function ViewModel() {
    var self = this;

    self.isLoading = ko.observable(false);
    self.data = ko.observableArray([]);
    
    self.loadData = function() {
        self.isLoading(true);
        fetch('/api/items')
            .then(response => response.json())
            .then(result => {
                self.data(result);
                self.isLoading(false);
            })
            .catch(error => {
                console.error('Ошибка загрузки данных:', error);
                self.isLoading(false);
            });
    };
}

ko.applyBindings(new ViewModel());

В этом примере isLoading переключается на true перед началом асинхронного запроса и возвращается в false после завершения или при ошибке. Это позволяет привязать индикатор загрузки к элементам интерфейса через visible binding:

<div data-bind="visible: isLoading">Загрузка...</div>
<ul data-bind="foreach: data">
    <li data-bind="text: $data"></li>
</ul>

Обработка ошибок через observable

Ошибки запросов удобно хранить в отдельном observable, чтобы отображать их пользователю:

self.errorMessage = ko.observable('');

self.loadData = function() {
    self.isLoading(true);
    self.errorMessage('');
    fetch('/api/items')
        .then(response => {
            if (!response.ok) throw new Error('Сервер вернул ошибку');
            return response.json();
        })
        .then(result => {
            self.data(result);
            self.isLoading(false);
        })
        .catch(error => {
            self.errorMessage(error.message);
            self.isLoading(false);
        });
};

Отображение ошибки в интерфейсе:

<div data-bind="visible: errorMessage, text: errorMessage" class="error"></div>

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


Использование computed для комбинированных состояний

Иногда требуется объединить несколько состояний в одно вычисляемое значение. Например, определение, доступен ли контент для отображения:

self.hasData = ko.computed(function() {
    return !self.isLoading() && self.data().length > 0 && !self.errorMessage();
});

Теперь hasData можно использовать для условного отображения основного контента:

<div data-bind="visible: hasData">
    <ul data-bind="foreach: data">
        <li data-bind="text: $data"></li>
    </ul>
</div>

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


Асинхронные операции с промисами и async/await

Knockout.js не ограничивает способ работы с асинхронностью. Применение async/await делает код более читаемым:

self.loadData = async function() {
    self.isLoading(true);
    self.errorMessage('');
    try {
        const response = await fetch('/api/items');
        if (!response.ok) throw new Error('Ошибка сервера');
        const result = await response.json();
        self.data(result);
    } catch (error) {
        self.errorMessage(error.message);
    } finally {
        self.isLoading(false);
    }
};

Такой подход уменьшает вложенность then/catch и делает обработку ошибок более предсказуемой.


Реактивная обработка ошибок на уровне компонентов

Для больших приложений удобно создавать универсальные observables для статусов:

self.status = ko.observable({ loading: false, error: '' });

self.loadData = async function() {
    self.status({ loading: true, error: '' });
    try {
        const response = await fetch('/api/items');
        if (!response.ok) throw new Error('Ошибка сервера');
        const result = await response.json();
        self.data(result);
    } catch (error) {
        self.status({ loading: false, error: error.message });
    } finally {
        self.status({ ...self.status(), loading: false });
    }
};

В интерфейсе:

<div data-bind="visible: status().loading">Загрузка...</div>
<div data-bind="visible: status().error, text: status().error" class="error"></div>

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


Практические рекомендации

  • Использовать отдельные observables для данных, ошибок и статуса загрузки.
  • Применять computed для вычисления комбинированных состояний интерфейса.
  • Логировать ошибки для диагностики, но предоставлять пользователю понятное сообщение.
  • Синхронизировать индикаторы загрузки с завершением асинхронных операций, используя finally или эквивалент.
  • Для сложных интерфейсов рассматривать использование универсального объекта состояния, содержащего loading, error, success.

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