Работа в автономном режиме (или, попрощавшись с обязательной выборкой данных)
Недавно мы имели возможность начать с нуля наше (Angular) веб-приложение Zaaksysteem.nl. Это позволило нам полностью переосмыслить некоторые жизненно важные части нашего приложения, когда мы, наконец, перешли от серверного приложения к клиентскому. Очевидным преимуществом была офлайн-поддержка.
Это конечный результат после пары недель размышлений, кряхтения и измельчения.
Как видите, приложение доступно даже без подключения. Кроме того, любые данные или изменения данных немедленно отображаются на всех вкладках. Позвольте мне объяснить, как нам это удалось.
Кэширование ресурсов с помощью ServiceWorker
С появлением ServiceWorker кэширование ресурсов стало проще простого, избавившись от необходимости обращаться к серверу за активами и добавив поддержку автоматических обновлений. Меня не обязательно заботит автономное использование, но оптимизация для автономного режима дает очевидные преимущества для пользователей с плохим подключением, и даже если у вас сверхбыстрое соединение 4G, это никогда не так быстро, как просто получить ресурсы из кеша и проверить наличие обновлений в фон.
Sw-Precache делает 90% этого смехотворно простым. Практически все, что вам нужно сделать, это добавить пару строк в конец файла сборки, и эта изящная маленькая библиотека позаботится обо всем остальном: ваши ресурсы теперь загружаются при первом запуске, а последующие загрузки страниц используют кешированные ресурсы. Я написал небольшую службу вокруг ServiceWorker, которая позволяет нам уведомлять пользователя об обновлении ресурсов и давать им возможность обновить страницу.
Работа с запросами XHR
Этот подход частично не работает, когда вы имеете дело с запросами XHR. На самом деле вам не нужно выполнять предварительную выборку всех данных, или чтобы пользователь постоянно просматривал устаревшие данные, или чтобы пользователь обновлял данные каждый раз, когда в фоновом режиме загружаются новые данные. Такого рода поражения цели клиентского приложения, не так ли?
Но он только частично ломается, потому что подход отображения кешированных данных и получения обновлений в фоновом режиме довольно надежен. Нам просто нужно придумать способ его обновления без необходимости обновлять что-либо пользователю.
I. Кеширование данных API по запросу
Когда пользователь перемещается по приложению, мы кэшируем как можно больше данных в LocalStorage, выскакивая самые старые запросы, если достигнут предел (размер). Это гарантирует, что данные, которые больше всего нужны пользователям, почти всегда доступны для обслуживания из кеша.
II. Используйте постоянно обновляемый ресурс вместо обязательной выборки данных на основе обещаний
Мы отказались от $ http в пользу ресурса, вдохновленного службой Angular $ resource. Ресурс работает следующим образом:
- Потребитель передает URL-адрес, объект запроса или функцию, которая при вызове возвращает либо (или ноль).
- Ресурс проверяет кеш и, если он доступен, немедленно предоставляет эти данные с помощью метода data ().
- При необходимости данные извлекаются в фоновом режиме, а при извлечении кеш обновляется и возвращается функцией data ().
- При использовании функции ресурс проверяет результат для каждого дайджеста. Если результирующий URL-адрес изменится, процесс перезапустится.
- Когда кеш обновляется, ресурс тоже. Это также означает, что данные распределяются между вкладками, что позволяет минимизировать количество запросов и предотвращает просмотр пользователем устаревших данных.
Вот как будет выглядеть контроллер на основе $ http по сравнению с контроллером на основе ресурсов:
Для меня это довольно большая разница - а версия $ http даже не включает извлечение данных из кеша.
Ресурсы также позволяют добавлять к нему редукторы, которые вызываются при вызове data (). Затем возвращается результат этого, хм, сокращения. Ресурс также не имеет состояния по умолчанию, и редукторы вызываются только при изменении данных. Это также означает, что вы должны защитить себя от внешних источников, изменяющих данные, поэтому мы используем замечательную бесшовную-неизменяемость, чтобы предотвратить любые изменения. Вот пример:
Помимо очевидных преимуществ в производительности, кеширование особенно полезно для Angular, потому что ng-repeat в настоящее время не может работать с постоянно меняющимися массивами. Этот принцип очень хорошо сочетается с Angular, поскольку он просто постоянно вызывает data () для каждого дайджеста, освобождая вас от написания шаблона событий.
III. Оптимистичные обновления и мутации как сериализуемые объекты
Мы не только хотим дать пользователю мгновенную обратную связь о его действии, мы хотим, чтобы само действие ощущалось мгновенно. У нас возникли проблемы с производительностью API, поэтому нам нужен механизм, имитирующий ответ сервера, пока он еще недоступен. Мы также хотели убедиться, что они могут храниться в автономном режиме и синхронизироваться, когда соединение становится доступным. Кроме того, если вкладка с приложением закрывается или вылетает из строя, сохраненные мутации должны быть перехвачены другим экземпляром приложения, запущенным на другой вкладке, чтобы пользователь не потерял никаких данных.
Для этого мы используем мутации. Мутация просто состоит из параметра type и свойства data. Они добавляются к ресурсу через `mutate ()`, который затем заботится о передаче мутации в mutationService, прикрепляя к нему запрос. Затем мутации сохраняются в LocalStorage. Как только добавляется мутация, они сбрасываются, то есть отправляются на сервер. В этот момент (и с определенным интервалом) он также проверяет (через window.postMessage) на любые осиротевшие мутации.
Когда мутация синхронизируется, для этого типа запрашивается действие. Эти обработчики действий регистрируются сразу после инициализации приложения. Приложение состоит из следующих свойств и методов:
- тип: тип мутаций, которые должно обрабатывать действие.
- request ( mutationData ): это вызывается mutationService с данными мутации. Это позволяет действию возвращать объект requestOptions на основе типа действия и данных мутации. Например, это может быть `/ api / user / tweet / $ {mutationData.tweetId} / delete`.
- reduce (data, mutationData): эта функция вызывается ресурсом. Он должен изменить сохраненные данные на основе mutationData, чтобы имитировать ответ сервера. Он вызывается перед любым другим редуктором, который мог бы добавить потребитель.
- success () / error (): дополнительные обработчики успеха / ошибок.
Как только мутация успешно отправлена на сервер (или в случае сбоя), она удаляется из ресурса, а данные из ресурса обновляются. В настоящее время мы пытаемся заставить изменяющие запросы возвращать коллекцию или объект, на котором они были основаны, но довольно легко указать ресурсу снова получить свои данные после завершения мутации. Поскольку мутации хранятся в localStorage, эти данные также могут использоваться во всех вкладках, и любые изменения, вносимые пользователем, сразу же видны и на других вкладках, без извлечения каких-либо данных, как вы можете видеть на видео.
IV. Составные редукторы
Иногда выходные данные вашего метода контроллеров зависят от более чем одного вызова API или состояния компонента, или от того и другого. Поэтому мы используем составные редукторы.
Вы можете объединить несколько ресурсов, функций или констант в один ресурс и уменьшить его до одного значения, снова вызвав reduce () с функцией-редуктором. Затем эта функция вызывается при обновлении любого из ресурсов или при изменении результата функции после $ дайджеста. По умолчанию он вызывает данный метод редуктора только тогда, когда все зависимости разрешены, но вы также можете вызвать его при любом изменении. Вот как это будет выглядеть, если вы фильтруете список на основе ввода данных пользователем:
Мы только что закончили наше первое представление, в котором используется этот подход - панель инструментов. Вот реальный пример компонента. Это довольно сложно, с множеством зависимостей, но я все еще чувствую, что все под контролем. Мы обязательно исследуем эту концепцию дальше. И мы будем рады услышать ваши отзывы! Особенно, если вам известна какая-либо библиотека, которую мы можем использовать, которая выполняет те же действия, что и здесь. В любом случае, когда он станет немного более зрелым (API сейчас очень нестабилен) и если будет интерес, мы обязательно выделим его в отдельную библиотеку.