Как показывать данные с сервера

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

{{#if data}}
//Отображаем данные
{{else}}
//Отображаем информацию о том, что идет загрузка, например, preloader.
{{/if}}

Когда загружать данные? Конечно, чем раньше, тем лучше. Для этого идеально подходит событие init у страницы. Почему не mounted? Потому, что в общем случае, оно не подходит: при использовании keepAlive оно будет вызываться и при перемещении страницы из памяти в DOM, а в этом случае данные уже все есть и загружать ничего не нужно.

С событием понятно, далее следует простой механизм:

  1. Получили данные.
  2. Вызвали $setState(), чтобы показать данные пользователю.

События страницы асинхронные, это значит что $setState может быть вызван в одном из двух случаев:

  1. Идет “показ” страницы, т. е. анимация, т.е. afterIn еще не вызвано.
  2. Страница уже показана, анимация завершена, событие afterIn вызвано.

В какой из двух моментов будет вызван $setState, зависит от того, как быстро придет от сервера ответ. На моей практике чаще встречаются сервера, которые отвечают быстро и получается случай 1. Для проектов, где я пишу backend, всегда возникает случай 1.

Тут и возникает проблема: в итоге вы обновляем DOM когда идет анимация страницы и видим тормоза (даже на простом интерфейсе и даже на iPhone), что говорить о “сложных” страницах.

Что делать? Конечно, делать изменения в DOM только после того, как анимация завершена, а для этого есть специальное событие afterIn. Какие варианты приходят в голову?

Первый, самый простой, - это просто загружать данные в событии afterIn. Но тут есть сразу 2 проблемы:

  1. Мы упускаем драгоценное время, ведь в большинстве случаев мы уже готовы показать пользователю данные в afterIn, а тут мы только начнем их загружать.
  2. afterIn будет вызываться и тогда, когда страница просто помещается во View, а создана при этом давно.

Если 2ю проблему можно обойти, заведя отдельную переменную с информацией о том, загружены ли данные, то 1ю проблему - не решить.

Вариант второй - использовать событие beforeIn, тут мы имеем такие же проблемы, хотя потерянное время будет меньше (это про проблему 1).

Вариант третий, правильный - нужно загружать данные в init, а показывать их только в afterIn. Для этого идеально подойдет Promise. Для начала напишем глобальную Mixin:

Framework7.registerComponentMixin('update-lock', {
   data: function() {
       return {
           updateLockPromise : null, //Сам Promise, он нам нужен в компоненте роутера
           updateLockPromiseResolve : null, //Ссылка на resolve в Promise, по другому вызывать нельзя
       }
   },
   methods : {
       createUpdateLock() { //Создаем блокировку, т.е. Promise
           this.updateLockPromise = new Promise((resolve => {
               this.updateLockPromiseResolve = resolve; //Сохраняем ссылку на resolve, чтобы потом его вызвать
           }));
       },
       destroyUpdateLock() { //Удаляем блокировку, просто вызываем resolve для Provise
           this.updateLockPromiseResolve();
       }
   },
   on : {
       pageInit() {
           this.createUpdateLock(); // Ставим блокировку
       },
       pageAfterIn() {
          this.destroyUpdateLock(); //Снимаем блокировку
       }
   }
});

В самом компоненте роутера нам нужно подключить Mixin:

mixins: [ 'update-lock']

Затем нам нужно не показывать данные, пока не снята блокировка, для этого есть 2 варианта,

async () => {
	await this.updateLockPromise; //Ждем, дальше не идем
	/* Далее код работает с $setState */
}

или так (что одно и тоже)

this.updateLockPromise.then(() => {
	/* Далее код работает с $setState */
})

Что же будет, если Promise уже будет помеченный как resolve, когда данные придут с сервера? Он сразу выполнится и все отработает правильно.

Теперь у нас больше нет “лаг” при изменении в DOM, если они совпадают с анимацией страницы.

Дополнение 1
Сразу возникает идея, вообще не вызывать $setState, если сейчас идет анимация страницы. Например, на странице мы подписываемся на событие и по его приходу что-то обновляем в DOM. Но на практике, конечно, этот случай крайне редкий.

У нас есть еще одно событие reinit: возможно стоит добавить еще одну установку блокировки? К сожалению, на мой взгляд, это событие вызывается не тогда когда нужно, поэтому при его наступлении устанавливать блокировку нельзя.

Посмотрите примеры:

page1 > page2 > (back) page1 (reinit - логично).

page1 > page2 > page3 >(back) page2 (init page1 - логично, нужно для Swipe Back) > (back) page1 (reinit - не логично, для нее уже вызван init)

Тогда можно пойти по другому пути: завести другой Promise и ставить блокировку на beforeIn, а снимать при afterIn.

Дополнение 2
Если данные придут быстро с сервера, то preloader быстро покажется и скроется. Это некрасиво, желательно установить какое-то минимальное время показа, скажем 800 мс.

В Mixin добавим отдельную переменную:

updateLockDestroyDelay: 0

А в компоненте роутера мы ее можем переопределить. Теперь необходимо изменить pageAfterIn в Mixin:

pageAfterIn() {
   setTimeout(() => {
       if (typeof this.destroyUpdateLock !== 'undefined') { //Компонента может уже не и быть, например мы ушли раньше, чем закончился таймаут
           this.destroyUpdateLock();
       }
   }, this.updateLockDestroyDelay);
}

Вот и все, надеюсь, будет полезно.

4 Likes

Хорошая статья, спасибо!

1 Like