Оригинальная ссылка:blog.thought RAM.IO/angular/201…
Эта статьяКитайское сообщество RxJSПереведенные статьи, если нужно перепечатать, просьба указывать источник, спасибо за сотрудничество!
Если вы также хотите присоединиться к нам в переводе более высококачественных статей RxJS для всех, пожалуйста, нажмите【здесь】
Напоминание: статья длинная, а исходный текст написан минут на 40 для прочтения, рекомендуется иметь много свободного времени во второй половине дня, чтобы прочитать ее не спеша.
При разработке веб-приложений производительность всегда является главным приоритетом. Чтобы ускорить приложения Angular, мы можем делать такие вещи, как встряхивание деревьев, AoT (с опережением времени), ленивая загрузка модулей и кэширование. Если вы хотите получить всестороннее представление о практических навыках повышения производительности приложений Angular, мы настоятельно рекомендуем вам обратиться кMinko GechevнаписаноКонтрольный список производительности Angular. В этой статье мы сосредоточимся на кэшировании.
На самом деле кэширование — это один из самых эффективных способов улучшить взаимодействие с пользователем на веб-сайте, особенно когда пользователь использует устройство с ограниченной пропускной способностью или плохим сетевым окружением.
Существует множество способов кэширования данных или ресурсов. Статические ресурсы обычно кэшируются стандартными кэшами браузера или Service Workers. Хотя сервис-воркеры также могут кэшировать запросы API, они часто более полезны для кэширования таких ресурсов, как изображения, файлы HTML, JS или CSS. Обычно мы используем специальные механизмы для кэширования данных приложения.
Независимо от используемого нами механизма кэширование обычноУлучшить отзывчивость приложения,Сократить расходы на интернет, и имеетКонтент, доступный во время сбоев в сетиПреимущества. Другими словами, когда контент кэшируется ближе к потребителю, например, на стороне клиента, запрос не вызовет дополнительной сетевой активности, а кэшированные данные могут быть извлечены быстрее, что экономит весь сетевой цикл.
В этой статье мы будем использовать инструменты, предоставляемые RxJS и Angular, для разработки расширенного механизма кэширования.
содержание
- мотивация
- нужно
- Реализовать базовое кэширование
- автоматическое обновление
- Отправлять уведомления об обновлениях
- Извлекайте новые данные по запросу
- Перспектива
- Особая благодарность
мотивация
Время от времени кто-то спрашивает, как кэшировать данные в приложении Angular, которое активно использует Observables? Большинство людей хорошо понимают, как использовать промисы для кэширования данных, но при переходе на реактивное программирование это происходит из-за его сложности (огромный API), сдвига ума (от императивного к декларативному) и многих концепций, и они чувствуют себя перегруженными. Поэтому сложно преобразовать существующий механизм кэширования, основанный на промисах, в механизм, основанный на Observables, особенно если вы хотите, чтобы механизм кэширования был немного более продвинутым.
Обычно используется в приложениях AngularHttpClientModule
серединаHttpClient
для выполнения HTTP-запроса.HttpClient
Все API основаны на Observable, то есть что-то вродеget
,post
,put
илиdelete
и другие методы возвращают Observable . Поскольку Observables по своей природе ленивы, только когда мы вызываемsubscribe
Только тогда запрос действительно будет выполнен. Однако вызов одного и того же Observable несколько разsubscribe
Вызывает повторное создание исходного Observable снова и снова, выполняя запрос для каждой подписки. Мы называем это холодными наблюдаемыми.
Если вы совершенно не разбираетесь в этом, мы уже писали статью на эту тему:Холодные и горячие наблюдаемые. (Примечание переводчика: если вы хотите узнать о холодных и горячих Observables, вы также можете порекомендовать прочитатьэта статья)
Такое поведение усложняет реализацию механизмов кэширования с помощью Observables. Простые подходы, как правило, требуют изрядного количества шаблонного кода, и мы можем обойти RxJS, что возможно, но не рекомендуется, если мы хотим, наконец, использовать мощь Observables. Проще говоря, мы не хотим ездить на Феррари со скутерным двигателем, верно?
нужно
Прежде чем погрузиться в код, давайте сначала сформулируем требования для реализации расширенного механизма кэширования.
Приложение, которое мы хотим разработать, называется Joke World. Это простое приложение, которое просто случайным образом представляет шутки на основе заданной категории. Чтобы сделать приложение более простым и целенаправленным, мы установили только одну категорию.
Приложение состоит из трех компонентов:AppComponent
,DashboardComponent
а такжеJokeListComponent
.
AppComponent
Компонент является точкой входа приложения, он отображает панель инструментов и<router-outlet>
, который заполняет содержимое на основе текущего состояния маршрутизатора.
DashboardComponent
Компонент отображает только список категорий. Здесь вы можете перейти кJokeListComponent
Компонент, отвечающий за вывод списка шуток на экран.
Шутка должна использовать угловойHttpClient
Служба вытягивается с сервера. Чтобы обязанности компонентов оставались едиными и концептуально разделенными, мы хотим создатьJokeService
нести ответственность за запрос данных. Затем компонент может получить доступ к данным через свой общедоступный API, просто внедрив эту службу.
Выше приведена архитектура нашего приложения, и кеширование пока не задействовано.
При переходе со страницы списка категорий на страницу списка шуток мы предпочитаем запрашивать последние данные в кеше, а не каждый раз делать запрос на сервер. Кэшированные базовые данные автоматически обновляются каждые 10 секунд.
Конечно, для приложений производственного уровня опрос новых данных каждые 10 секунд не является хорошим вариантом, и, как правило, используется более зрелый способ обновления кеша (например, push-обновления веб-сокетов). Но здесь мы не будем усложнять, чтобы сосредоточиться на самом кеше.
Мы будем получать уведомления об обновлениях в той или иной форме. Для этого приложения нам не нужен пользовательский интерфейс (JokeListComponent
Data) обновляются автоматически, но ждут, пока пользователь выполнит обновление пользовательского интерфейса. Зачем это делать? Представьте, пользователь может прочитать барную шутку, а потом вдруг из-за автоматического обновления данных эта шутка исчезла. В результате из-за этого плохой пользовательский опыт, что позволяет пользователям очень злиться. Поэтому наш подход заключается в том, что каждый раз, когда новому пользователю предлагается обновить данные.
Для большего удовольствия мы также хотим, чтобы пользователи могли принудительно обновлять кэш. Это отличается от простого обновления пользовательского интерфейса, поскольку принудительное обновление означает запрос последних данных с сервера, обновление кеша, а затем соответствующее обновление пользовательского интерфейса.
Подводя итог пунктам содержания, мы разработаем:
- Приложение имеет два компонента A и B, при переходе от A к B оно должно запрашивать данные B из кеша, а не каждый раз запрашивать сервер.
- Кэш автоматически обновляется каждые 10 секунд
- Данные в пользовательском интерфейсе не обновляются автоматически, но требуют, чтобы пользователь выполнил действие обновления.
- Пользователь может принудительно выполнить обновление, которое отправит HTTP-запрос на обновление кеша и пользовательского интерфейса.
Ниже приведен предварительный просмотр приложения:
Реализовать базовое кэширование
Мы начинаем с простого, а затем продвигаемся к окончательному, полноценному решению.
Первым шагом является создание новой службы.
Далее добавим два интерфейса, один — описаниеJoke
Структура данных, другая используется для обеспечения соблюдения типа ответа на HTTP-запрос. Это сделает TypeScript счастливым, но, самое главное, его будет проще и чище использовать разработчикам.
export interface Joke {
id: number;
joke: string;
categories: Array<string>;
}
export interface JokeResponse {
type: string;
value: Array<Joke>;
}
Теперь давайте реализуемJokeService
. Мы не хотим раскрывать детали реализации относительно того, поступают ли данные из кеша или с сервера, поэтому мы раскрываем толькоjokes
свойство, которое возвращает Observable, содержащий список шуток.
Чтобы инициировать HTTP-запрос, нам нужно обязательно внедрить в конструктор службыHttpClien
Служить.
НижеJokeService
кадр:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable()
export class JokeService {
constructor(private http: HttpClient) { }
get jokes() {
...
}
}
Далее мы реализуем закрытый методrequestJokes()
, он будет использоватьHttpClient
инициироватьGET
Запрос списка шуток.
import { map } from 'rxjs/operators';
@Injectable()
export class JokeService {
constructor(private http: HttpClient) { }
get jokes() {
...
}
private requestJokes() {
return this.http.get<JokeResponse>(API_ENDPOINT).pipe(
map(response => response.value)
);
}
}
После этого шага нам остается толькоjokes
Метод получения не выполняется.
Простой способ - просто вернутьсяthis.requestJokes()
, но это не работает. Из начала статьи мы уже знаемHttpClient
Все открытые методы, такие какget
Возвращенные — это холодные Observables. Это означает, что весь поток данных перевыпускается для каждого подписчика, что приводит к многочисленным HTTP-запросам. Ведь идея кэширования заключается в том, чтобы ускорить загрузку приложений и ограничить количество сетевых запросов до минимума.
Вместо этого мы хотим, чтобы поток был горячим. Не только это, мы также хотим, чтобы каждый новый подписчик получал последние кэшированные данные. Есть очень удобный операторshareReplay
. Observable, который он возвращает, будет иметь одну подписку на базовый источник данных, в этом случаеthis.requestJokes()
Возвращенный Observable .
Помимо,shareReplay
Также получает необязательный параметрbufferSize
, что очень удобно для нашего случая.bufferSize
Определяет максимальное количество элементов в буфере воспроизведения, то есть количество кэшированных и воспроизводимых элементов для каждого нового подписчика. Для нашего сценария мы хотим воспроизвести только последнюю версию, поэтомуbufferSize
будет установлено значение 1 .
Давайте посмотрим на код и используем то, что мы только что узнали:
import { Observable } from 'rxjs/Observable';
import { shareReplay, map } from 'rxjs/operators';
const API_ENDPOINT = 'https://api.icndb.com/jokes/random/5?limitTo=[nerdy]';
const CACHE_SIZE = 1;
@Injectable()
export class JokeService {
private cache$: Observable<Array<Joke>>;
constructor(private http: HttpClient) { }
get jokes() {
if (!this.cache$) {
this.cache$ = this.requestJokes().pipe(
shareReplay(CACHE_SIZE)
);
}
return this.cache$;
}
private requestJokes() {
return this.http.get<JokeResponse>(API_ENDPOINT).pipe(
map(response => response.value)
);
}
}
Хорошо, большую часть кода выше мы уже обсудили. Но подождите, эта частная собственностьcache$
и в методе полученияif
Что делает заявление? Ответ прост. Если вы вернетесь прямоthis.requestJokes().pipe(shareReplay(CACHE_SIZE))
, то каждая подписка создаст экземпляр кэша. Но мы хотим, чтобы все подписчики использовали один и тот же экземпляр. Поэтому мы сохраняем этот общий экземпляр в частном свойстве.cache$
и инициализируйте его при первом вызове метода получения. Все последующие потребители будут совместно использовать этот экземпляр без повторного создания кеша каждый раз.
Давайте более интуитивно посмотрим на то, что мы только что реализовали, с помощью следующей схемы:
На изображении выше мы видим описание объектов, задействованных в нашей сцене.Диаграмма последовательности, т.е. очередь для запроса списка приколов и обмена сообщениями между объектами. Давайте разберем его, чтобы лучше понять, что мы делаем.
мы начинаем сDashboardComponent
Перейдите кJokeListComponent
Начни говорить.
Angular вызовет после инициализации компонентаngOnInit
Хук жизненного цикла, здесь мы будем называтьJokeService
незащищенныйjokes
Метод получения для запроса списка шуток. Поскольку это первый запрос данных, сам кеш не был инициализирован, а значитJokeService.cache$
даundefined
. Внутренне мы позвонимrequestJokes()
Он вернет сервер, который отправит наблюдаемые данные. Мы также применилиshareReplay
оператора, чтобы получить желаемый эффект.
shareReplay
оператор автоматически создаетReplaySubject
. Как только число подписчиков увеличится с 0 до 1,Subject
Подключитесь к базовому источнику Observable и транслируйте все его значения воспроизведения. Все последующие подписчики будут общаться с посредникомSubject
устанавливать соединения, поэтому базовый холодный Observable имеет только одну подписку. Это многоадресная рассылка, и она является основой нашего простого механизма кэширования. (Примечание переводчика: если вы хотите узнать больше о многоадресной рассылке, рекомендуетсяэта статья)
Как только сервер вернет данные, данные будут кэшированы.
Обратите внимание, что на диаграмме последовательностиCache
представляет собой самостоятельный объект, который представляется какReplaySubject
, который находится между потребителем (подписчиком) и базовым источником данных (HTTP-запросом).
когда сноваJokeListComponent
Когда компонент запрашивает данные, кэш воспроизводит последнее значение и отправляет его потребителю. Таким образом, дополнительные HTTP-запросы не выполняются.
Очень просто, не так ли?
Для получения более подробной информации нам нужно сделать еще один шаг и посмотреть, как кэширование работает на уровне Observable. Поэтому мы будем использоватьПорно(мраморная диаграмма), чтобы визуализировать, как работает поток:
Мраморная диаграмма выглядит очень ясно, а лежащий в основе Observable действительно имеет толькоподписка, все потребители подписываются на этот общий Observable, то естьReplaySubject
.我们还可以看到只有第一个订阅者触发了 HTTP 请求,而其他订阅者获得的只是缓存重放的最新值。
Наконец, давайте посмотрим наJokeListComponent
и как представить данные. Первый – это инъекцияJokeService
. затем вngOnInit
жизненный циклjokes$
Свойство инициализируется, и начальным значением является Observable, возвращаемый методом получения, предоставляемым службой.Array<Joke>
, а это именно те данные, которые нам нужны.
@Component({
...
})
export class JokeListComponent implements OnInit {
jokes$: Observable<Array<Joke>>;
constructor(private jokeService: JokeService) { }
ngOnInit() {
this.jokes$ = this.jokeService.jokes;
}
...
}
Обратите внимание, что мы не подписываемся императивноjokes$
, вместо этого используйте в шаблонеasync
Трубка, потому что трубка такая захватывающая. очень любопытный? Вы можете обратиться к этой статье:Три вещи, которые вам нужно знать об AsyncPipe
<mat-card *ngFor="let joke of jokes$ | async">...</mat-card>
прохладно! Это наш простой кеш. Чтобы убедиться, что запрос был сделан только один раз, откройте инструменты разработчика Chrome, затем перейдите на вкладку «Сеть» и выберите XHR. Начните со страницы со списком категорий, перейдите на страницу со списком шуток, а затем несколько раз вернитесь на страницу со списком категорий.
Онлайн-демонстрация фазы 1:Нажмите, чтобы просмотреть.
автоматическое обновление
Пока что мы разработали простой механизм кэширования с небольшим количеством кода, большую часть грязной работы выполняетshareReplay
оператор, отвечающий за кэширование и воспроизведение последнего значения.
Сейчас он работает отлично, но источник данных в фоновом режиме никогда не обновляется. Что, если данные могут меняться каждые несколько минут? Мы не хотим заставлять пользователя обновлять всю страницу, чтобы получить последние данные с сервера.
Было бы неплохо, если бы наш кеш мог обновляться каждые 10 секунд в фоновом режиме? полностью согласен! Как пользователь, нам не нужно перезагружать страницу, если данные изменятся, пользовательский интерфейс будет соответствующим образом обновлен. Повторюсь, в реальных приложениях мы в основном используем не опросы, а серверные push-уведомления. Но для нашего небольшого демонстрационного приложения достаточно таймера каждые 10 секунд.
Это также довольно просто реализовать. Подводя итог, мы хотим создать Observable, который выдает серию значений, разнесенных в соответствии с заданным интервалом времени, или, проще говоря, мы хотим генерировать значение каждые x миллисекунд. У нас есть несколько реализаций.
Первый вариант заключается в использованииinterval
. Этот оператор получает необязательный параметрperiod
, который определяет временной интервал между каждым испускаемым значением. См. пример ниже:
import { interval } from 'rxjs/observable/interval';
interval(10000).subscribe(console.log);
Здесь мы настраиваем Observable, который выдает бесконечную последовательность целых чисел с 10-секундным интервалом между каждым испускаемым значением. То есть первое значение будет выдано через 10 секунд. Для лучшей демонстрации давайте посмотрим наinterval
Мраморная диаграмма оператора:
А, это так. Первое значение выдается «с задержкой», а это не то, что нам нужно. Почему ты это сказал? Потому что, если мы перейдем со страницы со списком категорий на страницу со списком шуток, нам придется подождать 10 секунд, прежде чем сделать запрос данных на сервер для отображения страницы.
Мы можем сделать это, введя другое имяstartWith(value)
оператор, чтобы исправить это, так что данныйvalue
, то есть начальное значение. Но мы можем лучше!
Если бы я сказал вам, что есть еще один оператор, который сначала выдает значение на основе заданного времени (начальная задержка), а затем на основе интервала (обычный таймер), он будет продолжать выдавать значения.timer
Узнать о.
Мраморный момент!
Круто, но действительно ли это решает нашу проблему? Да все верно. Если мы установим начальную задержку как0, и установите интервал равным10 секундТак что его поведениеinterval(10000).pipe(startWith(0))
то же самое, но использует только один оператор.
давайте использоватьtimer
оператора и использовать его в нашем существующем механизме кэширования.
Нам нужно установить таймер, а затем сделать HTTP-запрос, чтобы получать последние данные с сервера каждый раз, когда время истекло. То есть для каждой временной точки нам нужно использоватьswitchMap
чтобы переключиться на Observable, который получает список шуток. использоватьswtichMap
Приятным побочным эффектом является то, что условия гонки избегаются. Это связано с природой этого оператора, он отменяет подписку на предыдущий внутренний Observable, а затем выдает только значение из последнего внутреннего Observable.
Остальные из нас кэшируются, остаются прежними, мы по-прежнему транслируем многоадресную рассылку, все подписчики используют один и тот же базовый источник данных.
такой же,shareReplay
Последнее значение отправляется существующим подписчикам и воспроизводится для последующих подписчиков.
Как показано на мраморной диаграмме,timer
Значение выдается каждые 10 секунд. Каждое значение будет преобразовано во внутренний Observable, который извлекает данные. потому что с помощьюswitchMap
, мы можем избежать состояния гонки, поэтому потребитель получит только значение1
а также3
. Значение второго внутреннего наблюдаемого, «пропущена», потому что мы фактически отписались от него, когда выделяется новое значение.
Давайте применим, мы только что научилисьJokeService
середина:
import { timer } from 'rxjs/observable/timer';
import { switchMap, shareReplay } from 'rxjs/operators';
const REFRESH_INTERVAL = 10000;
@Injectable()
export class JokeService {
private cache$: Observable<Array<Joke>>;
constructor(private http: HttpClient) { }
get jokes() {
if (!this.cache$) {
// 设置每 X 毫秒发出值的定时器
const timer$ = timer(0, REFRESH_INTERVAL);
// 每个时间点都会发起 HTTP 请求来获取最新数据
this.cache$ = timer$.pipe(
switchMap(_ => this.requestJokes()),
shareReplay(CACHE_SIZE)
);
}
return this.cache$;
}
...
}
прохладно! Хотите попробовать сами? Почаще пробуйте представленную ниже онлайн-демонстрацию. Перейдите со страницы списка категорий на страницу списка шуток и станьте свидетелем чуда. Терпеливо подождите несколько секунд, чтобы увидеть обновление данных. Помните, что хотя кеш на10 секундОбновите один раз, но вы можете свободно менять в онлайн-демонстрацииREFRESH_INTERVAL
ценность .
Фаза 2 Онлайн-демонстрация:Нажмите, чтобы просмотреть.
Отправлять уведомления об обновлениях
Давайте кратко рассмотрим, что мы уже разработали.
КогдаJokeService
При запрашивании данных мы всегда хотим запросить последние данные в кэше, а не запрашивать сервер каждый раз. Кэшированные базовые данные обновляются каждые 10 секунд и распространяя данные для компонента, приведет к автоматическому обновлению UI.
Это какой-то провал. Представьте, что мы являемся пользователем, и когда мы смотрим шутку, она внезапно исчезает, потому что пользовательский интерфейс автоматически обновляется. Этот плохой пользовательский опыт может разозлить пользователей.
Следовательно, уведомление должно быть отправлено, чтобы предупредить пользователя о появлении новых данных. Другими словами, мы хотим, чтобы пользователь выполнял обновления пользовательского интерфейса.
На самом деле, для этого нам не нужно модифицировать сервисный уровень. Логика довольно проста. В конце концов, наш сервисный уровень не должен заботиться об отправке уведомлений, а когда и как обновлять данные на экране, за это должен отвечать уровень представления.
Во-первых, нам нужноПервоначальный значениечтобы показать его пользователю, иначе экран будет пустым до тех пор, пока кеш не будет обновлен в первый раз. Мы увидим, почему через мгновение. Задать поток начальных значений так же просто, как вызвать геттер-метод. Кроме того, поскольку нас интересует только первое значение, мы можем использоватьtake
оператор.
Сделать логику можно повторно использовать, мы создаем помощник методаgetDataOnce()
.
import { take } from 'rxjs/operators';
@Component({
...
})
export class JokeListComponent implements OnInit {
...
ngOnInit() {
const initialJokes$ = this.getDataOnce();
...
}
getDataOnce() {
return this.jokeService.jokes.pipe(take(1));
}
...
}
По требованию мы хотим обновлять пользовательский интерфейс только тогда, когда пользователь действительно выполняет обновление, а не автоматически. Так как же пользователи реализуют обновление, о котором вы просите? Это делается только тогда, когда мы нажимаем кнопку в пользовательском интерфейсе с надписью «Обновить». На данный момент нам не нужно думать об уведомлениях и следует сосредоточиться на логике обновления при нажатии кнопки.
Для этого нам нужен способ создания Observable, полученного из событий DOM, в данном случае событий нажатия кнопки. Существует несколько способов его создания, но наиболее распространенным является использованиеSubject
Как между шаблоном и логикой класса компонентовмост. вкратце,Subject
является одновременнымObserver
(наблюдатель) иObservable
тип. Observables определяют потоки данных и производят данные, а наблюдатели могут подписываться на Observables и получать данные.
Преимущество Subject в том, что мы можем использовать привязку события непосредственно в шаблоне, а затем вызывать его, когда событие срабатывает.next
метод. Это передает указанное значение всем наблюдателям, прослушивающим значение. Обратите внимание, что если тип темыvoid
, мы также можем опустить это значение. Собственно, это и есть наш реальный сценарий.
Давайте создадим новый Subject .
import { Subject } from 'rxjs/Subject';
@Component({
...
})
export class JokeListComponent implements OnInit {
update$ = new Subject<void>();
...
}
Затем мы можем использовать его в шаблоне.
<div class="notification">
<span>There's new data available. Click to reload the data.</span>
<button mat-raised-button color="accent" (click)="update$.next()">
<div class="flex-row">
<mat-icon>cached</mat-icon>
UPDATE
</div>
</button>
</div>
Давайте посмотрим, как мы используемпривязка событийсинтаксис для захвата<button>
в событии клика? Когда кнопка нажата, мы просто распространяем фантомное значение, чтобы уведомить всех активных наблюдателей. Мы называем это фантомным значением, потому что на самом деле значение не передается или тип переданного значенияvoid
.
Другой способ - использовать@ViewChild()
Декораторы и RxJSfromEvent
оператор.但是,这需要我们在组件类中“混入” DOM 并从视图中查询 HTML 元素。使用 Subject 的话,我们只需要将两者桥接即可,除了我们在按钮上添加的事件绑定之外,根本不会触及 DOM 。
Что ж, с настроенным представлением мы можем переключиться на логику, которая обрабатывает обновления пользовательского интерфейса.
Итак, что значит обновить пользовательский интерфейс? Кэш обновляется автоматически в фоновом режиме, и мы хотим, чтобы последнее значение из кеша отображалось при нажатии кнопки, верно? Это означает, что нашаисточникПотоки — это Subject s. каждый разupdate$
Когда значение испускается, мы помещаем егокартав Observable, дающий последнее кешированное значение. Другими словами, мы используемНаблюдаемый более высокого порядка (наблюдаемый более высокого порядка), Observable, который испускает Observables.
До этого мы должны знатьswitchMap
Как раз для решения этой проблемы. Но на этот раз мы будем использоватьmergeMap
. он ведет себя сswitchMap
Точно так же он не отписывается от предыдущего внутреннего Observable, а вместо этого объединяет испускаемые значения внутреннего Observable в выходной Observable.
На самом деле, к моменту запроса последнего значения из кэша HTTP-запрос уже выполнен и кэш успешно обновлен. Поэтому мы не сталкиваемся с проблемой условий гонки. Хотя это может показаться асинхронным, в некотором смысле этоСинхронный, потому что значение выдается в том же тике.
import { Subject } from 'rxjs/Subject';
import { mergeMap } from 'rxjs/operators';
@Component({
...
})
export class JokeListComponent implements OnInit {
update$ = new Subject<void>();
...
ngOnInit() {
...
const updates$ = this.update$.pipe(
mergeMap(() => this.getDataOnce())
);
...
}
...
}
прохладно! Каждый раз, когда мы «обновляем», мы запрашиваем последнее значение из кеша, используя вспомогательный метод, который мы реализовали ранее.
На данный момент поток, отвечающий за вывод шутки на экран, находится всего в нескольких шагах. Все, что нам нужно сделать, это объединитьinitialJokes$
а такжеupdate$
эти два потока.
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { merge } from 'rxjs/observable/merge';
import { mergeMap } from 'rxjs/operators';
@Component({
...
})
export class JokeListComponent implements OnInit {
jokes$: Observable<Array<Joke>>;
update$ = new Subject<void>();
...
ngOnInit() {
const initialJokes$ = this.getDataOnce();
const updates$ = this.update$.pipe(
mergeMap(() => this.getDataOnce())
);
this.jokes$ = merge(initialJokes$, updates$);
...
}
...
}
Используем вспомогательные методыgetDataOnce()
Важно сопоставить каждое событие обновления с последним кэшированным значением. Напомним, что внутри этого метода используетсяtake(1)
он принимает только первое значение, а затемЗаканчиватьпоток. Это очень важно, иначе вы получите незавершенное или активное подключение к кэшированному потоку. В этом случае наша логика в основном нарушается, чтобы выполнять обновления пользовательского интерфейса только нажатием кнопки «Обновить».
Кроме того, поскольку базовый кеш является многоадресным, совершенно безопасно всегда повторно подписываться на кеш для получения последнего значения.
Прежде чем продолжить процесс уведомления, давайте сделаем паузу и посмотрим на мраморную диаграмму логики, которую мы только что реализовали.
Как видно на рисунке,initialJokes$
Критически важно, потому что без него мы бы видели список шуток на экране только после нажатия кнопки «Обновить». Хотя данные обновляются каждые 10 секунд в фоновом режиме, мы вообще не можем нажать кнопку обновления. Потому что сама кнопка является частью уведомления, но мы никогда не показываем ее пользователю.
Итак, восполним этот пробел и реализуем недостающий функционал.
Нам нужно создать Observable, который отвечает за отображение/скрытие уведомлений. По сути, нам нуженtrue
илиfalse
поток. При обновлении значение, которое мы хотим, равноtrue
, когда пользователь нажимает кнопку «обновить», значение, которое мы хотим, равноfalse
.
Кроме того, мы также хотимперепрыгниКэшировать первое (начальное) сгенерированное значение, так как это не новые данные.
Если использовать потоковое мышление, мы можем разделить его на несколько потоков, а затем разделить их.сливатьсяв один Observable . Окончательный поток будет иметь желаемое поведение для отображения или скрытия уведомлений.
До сих пор теория! Давайте посмотрим на код:
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { skip, mapTo } from 'rxjs/operators';
@Component({
...
})
export class JokeListComponent implements OnInit {
showNotification$: Observable<boolean>;
update$ = new Subject<void>();
...
ngOnInit() {
...
const initialNotifications$ = this.jokeService.jokes.pipe(skip(1));
const show$ = initialNotifications$.pipe(mapTo(true));
const hide$ = this.update$.pipe(mapTo(false));
this.showNotification$ = merge(show$, hide$);
}
...
}
这里,我们跳过了缓存的第一个值,然后监听它剩下所有的值,这样做的原因是第一个值不是新数据。 мы будемinitialNotifications$
Каждое испускаемое значение сопоставляется сtrue
для отображения уведомлений. Как только мы нажмем кнопку «Обновить» в уведомлении,update$
создаст значение, которое мы можем сопоставить сfalse
чтобы отключить уведомления.
мы вJokeListComponent
Компоненты, используемые в шаблонеshowNotification$
чтобы переключить класс для отображения/закрытия уведомлений.
<div class="notification" [class.visible]="showNotification$ | async">
...
</div>
Да! В настоящее время мы очень близки к окончательному решению. Прежде чем двигаться дальше, давайте попробуем онлайн-демонстрацию. Не волнуйтесь, давайте рассмотрим код шаг за шагом.
Фаза 3 Онлайн-демонстрация:Нажмите, чтобы просмотреть.
Извлекайте новые данные по запросу
прохладно!一路走来我们已经为我们的缓存实现了一些很酷的功能。要结束本文并将缓存再提升一个等级的话,我们还需要做一件事。作为用户,我们想要能够在任何时间点来обязательныйобновить данные.
В этом нет ничего сложного, но для этого нам нужно модифицировать и компонент, и сервис.
Давайте начнем с сервиса. Нам нужна публичная облицовка API для заставить кэш для перезагрузки данных. Технически, мы быЗаканчиватьтекущий кеш и установите для него значениеnull
. Это означает, что в следующий раз, когда мы будем настраивать новый кэш данных из запроса на обслуживание, он будет извлекать данные с сервера и сохранять их для последующего обслуживания подписчиков. Создать новый кеш не составляет большой проблемы каждый раз принудительно обновлять его, потому что старый кеш со временем будет заполнен и сборка мусора. На самом деле, при этом возникает полезный побочный эффект, т.сброс настроекС таймером это решение является тем эффектом, который нам нужен. Допустим, ждем 9 секунд и нажимаем кнопку «Принудительно обновить». Мы ожидаем, что данные обновятся, но мы не хотим, чтобы уведомление об обновлении появлялось через 1 секунду. Мы хотим, чтобы таймер запускался заново, чтобы он не срабатывал до 10 секунд после принудительного обновления.автоматическое обновление.
Другая причина разрушения кэша сравнивается с кэшированной версией не разрушается, его сложность намного меньше. Если последнее является случай, кэш должен знать, если данные перегружены насильственными.
Давайте создадим тему, которая уведомит кеш о завершении. Здесь мы используемtakeUnitl
оператора и добавить его вcache$
Поток. Кроме того, мы также внедрили общедоступный API, который использует тему для трансляции события и устанавливает кешnull
.
import { Subject } from 'rxjs/Subject';
import { timer } from 'rxjs/observable/timer';
import { switchMap, shareReplay, map, takeUntil } from 'rxjs/operators';
const REFRESH_INTERVAL = 10000;
@Injectable()
export class JokeService {
private reload$ = new Subject<void>();
...
get jokes() {
if (!this.cache$) {
const timer$ = timer(0, REFRESH_INTERVAL);
this.cache$ = timer$.pipe(
switchMap(() => this.requestJokes()),
takeUntil(this.reload$),
shareReplay(CACHE_SIZE)
);
}
return this.cache$;
}
forceReload() {
// 调用 `next` 以完成当前缓存流
this.reload$.next();
// 将缓存设置为 `null`,这样下次调用 `jokes` 时
// 就会创建一个新的缓存
this.cache$ = null;
}
...
}
Реализация в сервисах сама по себе не помогает, нам также нужно реализоватьJokeListComponent
использовать его. Для этого реализуем функциюforceReload()
, который вызывается при нажатии кнопки «Принудительное обновление». Кроме того, нам также необходимо создать Subject как шину событий (Event Bus) для обновления UI и отображения уведомлений. Мы скоро увидим его в действии.
import { Subject } from 'rxjs/Subject';
@Component({
...
})
export class JokeListComponent implements OnInit {
forceReload$ = new Subject<void>();
...
forceReload() {
this.jokeService.forceReload();
this.forceReload$.next();
}
...
}
Таким образом, мы можемJokeListComponent
кнопку в шаблоне, чтобы кэш перезагрузил данные. Все, что нам нужно сделать, это использовать синтаксис привязки событий Angular для прослушиванияclick
событие, вызываемое при нажатии кнопкиforceReload()
.
<button class="reload-button" (click)="forceReload()" mat-raised-button color="accent">
<div class="flex-row">
<mat-icon>cached</mat-icon>
FETCH NEW JOKES
</div>
</button>
Это уже работает, но только если мы вернемся на страницу со списком категорий, а затем вернемся на страницу со списком шуток. Это определенно не тот результат, который нам нужен. При принудительной перезагрузке кеша данных мы хотим иметь возможностьнемедленноОбновить пользовательский интерфейс.
Помните поток, который мы реализовалиupdate$
?当我们点击“更新”按钮时,它会请求缓存中的最新数据。事实上,我们需要的也是同样的行为,因此我们可以继续使用并扩展此流。这意味着我们需要сливаться update$
а такжеforceReload$
, так как оба потока являются источниками данных для обновлений пользовательского интерфейса.
import { Subject } from 'rxjs/Subject';
import { merge } from 'rxjs/observable/merge';
import { mergeMap } from 'rxjs/operators';
@Component({
...
})
export class JokeListComponent implements OnInit {
update$ = new Subject<void>();
forceReload$ = new Subject<void>();
...
ngOnInit() {
...
const updates$ = merge(this.update$, this.forceReload$).pipe(
mergeMap(() => this.getDataOnce())
);
...
}
...
}
Это так просто, не так ли? Да, но это еще не конец. По сути, мы просто «сломаем» уведомление, делая это. Все работало нормально, пока мы не нажали кнопку «Принудительно обновить». После нажатия кнопки экран и данные в кеше по-прежнему обновляются, как обычно, но после ожидания в течение 10 секунд уведомление не появляется. Проблема в том, что принудительное обновление завершит поток кеша, а значит в компонент больше не будут поступать значения. поток уведомлений (initialNotifications$
) практически мертв. Это неправильный результат, так как мы это исправим?
Довольно легко! мы слушаемforceReload$
Испускаемое событие, которое переключает каждое из своих испускаемых значений в новый поток уведомлений. здесьОтменаправильноПредыдущийПодписки на стримы важны. Мелодия звенит в ушах? как бы говоря нам, что нам нужно использоватьswitchMap
.
Давайте испачкаем руки!
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { merge } from 'rxjs/observable/merge';
import { take, switchMap, mergeMap, skip, mapTo } from 'rxjs/operators';
@Component({
...
})
export class JokeListComponent implements OnInit {
showNotification$: Observable<boolean>;
update$ = new Subject<void>();
forceReload$ = new Subject<void>();
...
ngOnInit() {
...
const reload$ = this.forceReload$.pipe(switchMap(() => this.getNotifications()));
const initialNotifications$ = this.getNotifications();
const show$ = merge(initialNotifications$, reload$).pipe(mapTo(true));
const hide$ = this.update$.pipe(mapTo(false));
this.showNotification$ = merge(show$, hide$);
}
getNotifications() {
return this.jokeService.jokes.pipe(skip(1));
}
...
}
Это оно. в любое времяforceReload$
Когда значение испускается, мы отписываемся от предыдущего Observable и переключаемся на совершенно новый поток уведомлений. Обратите внимание, что здесь есть строка кода, которую нам нужно вызвать дважды, т. е.this.jokeService.jokes.pipe(skip(1))
. Чтобы избежать повторения, мы создали функциюgetNotifications()
, который возвращает поток списков шуток, но пропускает первое значение. Наконец, мы будемinitialNotifications$
а такжеreload$
слился вshow$
поток. Этот поток отвечает за отображение уведомлений на экране. Также нет необходимости отменитьinitialNotifications$
, поскольку он завершится до повторного создания кеша. Остальные остаются прежними.
Что ж, мы сделали. Давайте на минутку взглянем на мраморную карту того, что мы только что реализовали.
Как видно на рисунке, для отображения уведомленийinitialNotifications$
Очень важный.如果没有这个流的话,我们只能在强制缓存更新后才有机会看到通知。也就是说,当我们按需请求最新数据时,我们必须不断地切换成新的通知流,因为前一个(旧的) Observable 已经完成并不再发出任何值。
Вот и все! Мы реализовали сложный механизм кэширования с помощью инструментов, предоставляемых RxJS и Angular. Напомним, наш сервис предоставляет поток, который предоставляет нам список шуток. Каждые 10 секунд запускается HTTP-запрос для обновления кеша. Чтобы улучшить взаимодействие с пользователем, мы предоставляем уведомления об обновлениях, чтобы пользователи могли выполнять действия по обновлению пользовательского интерфейса. Кроме того, мы также предоставляем пользователям возможность запрашивать последние данные по запросу.
чудесный! Это полное решение. Потратьте несколько минут, чтобы снова посмотреть на код. Затем попробуйте разные сценарии, чтобы увидеть, все ли работает.
Фаза 4 Онлайн-демонстрация:Нажмите, чтобы просмотреть.
Перспектива
Вот несколько идей для улучшения, если вы хотите сделать домашнее задание или развить свой мозг позже:
- Добавить обработку ошибок
- Рефакторинг логики из компонентов в сервисы, чтобы сделать их повторно используемыми
Особая благодарность
Отдельное спасибоKwinten PismanПомогите мне закончить писать код. Я также хотел бы поблагодаритьBen Leshа такжеBrian TronconeДайте мне ценную обратную связь и предложите некоторые пункты улучшения. Кроме того, большое спасибоChristoph BurgdorfДля статей и обзоров кода.