Skip to content

Instantly share code, notes, and snippets.

@codedokode
Last active October 24, 2017 19:14
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save codedokode/6dfde1f0ec3d895bc940b67e7919cc29 to your computer and use it in GitHub Desktop.
Save codedokode/6dfde1f0ec3d895bc940b67e7919cc29 to your computer and use it in GitHub Desktop.

1040147

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

Начнем с API. Самое простое, что мне пришло в голову - сделать 2 метода: один вызвается в начале, чтобы получить начальный список сообщений, второй дергается по таймеру, чтобы получать новые сообщения, которые мы добавляем в список. При этом в ответе на запрос содержится дата/время, которые надо передать в следующем запросе.

class Api {
    // Получить последние N сообщений
    function loadLastMessages(limit) { ... } // -> Promise<MessageBatch>

    // Получить новые сообщения после since через API
    function loadNewMessages(since) { ... } // -> Promise<MessageBatch>
}

// Контроллер - пишем код прямо в него, кто нам запретит
class SomeCtrl {
    init() {
        api.loadLastMessages(SOME_LIMIT).then(
            function (batch) {
                that.renderBatch(batch); // вывести полученные сообщения
                that.queueRefresh(batch.lastUpdate)
            },
            this.handleLoadError(...).bind(this)
        );
    }

    queueRefresh(lastUpdate) {
        setTimeout(function () { that.loadNewMessages(lastUpdate); }, SOME_TIMEOUT);
    }

    loadNewMessages(lastUpdate) {
        api.loadNewMessages(since).then(
            function (batch) {
                that.renderBatch(batch);
                that.queueRefresh(batch.lastUpdate)
            },
            this.handleUpdateError(...).bind(this)
        );
    }
}

При ошибке для начала - можно показывать иконку ошибки, и делать повторный запрос с увеличивающимися интервалами времени. Вообще, тут стоит еще добавить индикацию, чтобы пользователь видел в каком состоянии система, а не смотрел на белый экран и гадал, это сообщения не загрузились или их там и нет.

Заметь, что в моем коде все действия последовательны, выполняются по очереди - мы не запускаем обновление сообщений, пока не получим начальный список. Отсутствие параллельных запросов позволяет избежать проблем с тем, что ответ на второй параллельный запрос может придти раньше, чем на первый.

Теперь попробуем запилить то же, но с моделью. Мы перенесем получение и обновление данных в модель, и используем паттерн observer (взял первое, что пришло в голову):

class MessageListModel {

    construct () {
        // Событие получения новых сообщений
        this.updateEvent = new EventEmitter;

        // Событие изменения статуса связи (успешная/идут ошибки) 
        // Используется для вывода иконок и уведомлений о проблемах
        // с соединением с сервером.
        this.statusChangeEvent = new EventEmitter;

        // Пдробная информация об ошибках со связью
        this.errorEvent = new EventEmitter;  

        // Время последнего обновления
        this.lastUpdate = null;          
    },
    // Подписка на события обновления списка сообщений
    // обновления будут приходить только после загрузки
    // начальных сообщений
    subscribeToUpdates(callback) { // -> eventId
        this._startUpdateLoop();
        return this.updateEvent.subscribe(callback);
    },
    // отписка
    unsubscribeFromUpdates(eventId) {
        this.updateEvent.unsubscribe(eventId);
        this._stopUpdateLoopIfNoSubscribersExist(); // надеюсь суть понятна из названия
    },
    // Получить последние N сообщений
    getLastMessages(limit) { // ->  Promise<MessageBatch>, никогда не реджектится
        var result = new Promise(function (resolve, reject) {
            this._loadLastMessages(limit, resolve);
        });

        return result;
    },

    // Отправляет запрос на получение сообщений
    _loadLastMessages(limit, resolver) {
        api.getLastMessages(limit).then(
            function (batch) {
                this.setConnectionStatus(true);
                this.lastUpdate = batch.updateTime;
                resolve(batch);
            },
            function (error) {
                this.errorEvent.emit(error);
                this.setConnectionStatus(false);
                setTimeout(function () {

                    that._loadLastMessages(limit, resolver);

                }, RETRY_TIMEOUT);
            }
        );
    },

    // Запускает процесс обновления данных
    _startUpdateLoop() {
        if (loop is started) { 
            return;
        }

        this._queueUpdate();
    },

    _stopUpdateLoopIfNoSubscribersExist() {
        ...
    },

    _cancelUpdateTimeout() {
        ....
    },

    _queueUpdate() {
        this.timeoutId = setTimeout(
            function () { that._loadMoreMessages(); },
            SOME_TIMEOUT
        );
    },

    _loadMoreMessages() {
        if (!this.lastUpdate) {
            // Мы еще не получили начальный набор сообщений
            this._queueUpdate();
            return;
        }

        api.loadNewMessages(this.lastUpdate).then(
            function (batch) {
                this._setConnectionStatus(true);
                this.updateEvent.emit(batch, false);
                this.lastUpdate = batch.updateTime;
                this._queueUpdate();
            },
            function (error) {
                this.errorEvent.emit(error);
                this._setConnectionStatus(false);
                this._queueUpdate();
            }
        )
    }    
}

Тут я вижу недостаточек: мы храним время последнего обновления в поле this.lastUpdate, и это затрудняет понимание кода и проверку правильности. Так как трудно понять, в какой момент там хранится какое значение. Можно ли уже его использовать или нет? Из-за этого мы ставим костыль с проверкой его на не-пустоту в _loadMoreMessages(). То есть везде, где оно используется, мы в теории должны лепить такую проверку, иначе есть риск допустить ошибку.

Возможно, ту можно было бы вместо обычной переменной сделать промис, this.initialUpdateTimePromise, который бы резолвился после получения начальных сообщений. И опираться на этот промис. И может быть, код станет логичней. А может и нет. Оставим изучение этого вопроса как упражнение читателю.

Вообще, конечно, лучше передавать переменные через аргументы функций, так будет меньше проблем (но в данном примере у меня это не получилось, и я ввел поле).

Теперь, вынеся работу с данными в модель, мы можем реализовывать разные архитектуры. Мы можем в контроллере подписываться на события этой модели и обрабатыать их, а можем - передать модель во вью и там подписываться на события. В общем, делать как нам удобно. Главное что у нас теперь есть модель, которая представляет собой самообновляющийся список сообщений одного пользователя. Как и положено в MVC (с активной моделью, которая сама сообщает об изменениях).

Кода правда стало чуть больше, но хорошая архитектура не дается бесплатно.

Вот пример использования модели в контроллере. При инициализации мы просто пописываемся на интересующие нас события:

class SomeCtrl {
    init() {

        this.renderConnectionStatus(STATUS_LOADING);

        // Обрабатывать ошибки не требуется, это делает модель
        this.msgListModel.getLastMessages(SOME_LIMIT).then(function (batch) {
            this.renderInitialMessages(batch);
        });

        this.msgListModel.onConnectionStatusChange(function (newStatus) {
            this.renderConnectionStatus(newStatus);
        });

        this.msgListModel.subscribeToUpdates(function (batch) {
            this.renderNewMessages(batch);
            this.deleteOldMessages();
        });
    }
}

Желательно также во view предусмотреть ограничение числа сообщений и удалять старые по мере добавления новых, чтобы DOM и потребление памяти не росли неограниченно при долгой работе (чем больше узлов DOM, тем медленнее все будет работать).

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

Вообще, код довольно топорный, так что тут можно подумать, как его можно улучшить. Ну например, есть такая штука как "реактивное программирование" (FRP). Не пригодится ли оно тут?

Там в основе лежит такая штука, как Observable. Это абстрактный поток событий, которые происходят асинхронно (то есть мы не можем получить этот поток целиком, но мы можем подписаться на него и нас будут вызывать при приходе события). Пока это напоминает обычный паттерн Observer. Но в реактивном программировании с Observable можно делать различные операции и преобразования, порождая новые Observables.

Более того, вроде есть библиотеки и шаблонизаторы, заточенные на работу с Observables.

Соответственно, наш список сообщений может быть смоделирован с помощью observable - первое событие в нем это начальный список сообщений, а последующие события - это обновления. Мы можем представить его как список блоков сообщений (Observable), или список отдельных сообщений (Observable). Что лучше, я не знаю.

Такая абстракция позволяет нам написать функцию, которая возвращает поток существующих и будущих сообщений и сделать интерфейс модели предельно минималистичным:

// Святые угодники, вы только посмотрите на этот интерфейс 
// и сравните с первой версией кода!
class MessageListModel {
    getMessagesStream(initalCount) { .. } // -> Observable(Message)
    getConnectionStatusStream() { ...} // -> Observable(NetworkStatus)
}

class MessageListCtrl {
    init(msgListModel) {
        msgListModel.getMessagesStream(INITIAL_COUNT).subscribe(function (message) {
            this.appendMessage(message);
            // можно this.view.appendMessage, это не принципиально
        });

        msgListModel.getConnectionStatusStream().subscribe(function (status) {
            this.displayNetworkStatus(status);
        });
    }
}

Это по сути то же самое, что и код выше, просто добавлена лишняя абстракция. Главный момент тут - это MVC. Напомню еще раз, что ошибки надо обрабатывать, индикаторы ожидания пользователю показывать.

Вот тут кстати есть интересный момент. Мы передаем initialCount, значит если мы сделаем 2 вызова:

var stream1 = model.getMessagesStream(10);
var stream2 = model.getMessagesStream(20);

то мы получим 2 независимых потока сообщений. Вопрос: можно ли при этом избежать двойных запросов к API на получение новых сообщений и вместо этого делать один запрос для обоих потоков? Я думаю, что можно, оставим реализацию этого как упражнение читателю.

В общем, разбирайся. Ну и может кому-то еще будет интересно почитать, повысим уровень образованности нашего треда в сравнении с другими.

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

Насчет твоего кода - он подозрительно маленький, мне кажется, там что-то упущено, например, я там не вижу таймера для обновления сообщений. Попробуй его сравнить с моим примером.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment