DevGang
Авторизоваться

Akita

Строгий менеджер состояний для приложений на javascript

Серверая пагинация

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

Обычно мы также хотим кэшировать страницы, которые уже были извлечены, чтобы избавить от необходимости дополнительного запроса.

Paginator API предоставляет две полезные функции:

  1. Кеширование уже извлеченных страниц.
  2. Функциональное разбиение на страницы, которое дает вам все необходимое для управления разбиением на страницы в приложении.

Базовая пагинация

Во-первых, нам нужно создать нового провайдера, скажем, нам нужен пагинатор для нашей страницы контактов:

import { Paginator } from '@datorama/akita';

export const CONTACTS_PAGINATOR = new InjectionToken('CONTACTS_PAGINATOR');

export function paginatorFactory(contactsQuery: ContactsQuery) {
  return new PaginatorPlugin<Contact>(contactsQuery).withControls().withRange();
}

export const contactsPaginatorProvider = {
  provide: CONTACTS_PAGINATOR,
  useFactory: paginatorFactory,
  deps: [ContactsQuery]
};

Вы уже должны быть знакомы с приведенным выше кодом. Это обычный процесс создания провайдера фабрики в Angular. Мы создаем новый Paginator(), передавая запрос, который мы хотим использовать в нашей нумерации страниц.

Не Angular приложения могут экспортировать плагин без использования DI. Например:

import { contactsQuery } from '../query';

export const contactsPaginatorProvider = new PaginatorPlugin(contactsQuery).withControls().withRange();

Мы вызываем withControls(), который даст нам массив страниц, поэтому мы используем ngFor для них, и withRange(), который даст нам значения from и to для отображения пользователю.

Теперь мы можем использовать его в нашем компоненте:

@Component({
  selector: 'app-contacts-page'
})
export class ContactsPageComponent implements OnInit {
  pagination$;

  constructor(@Inject(CONTACTS_PAGINATOR) public paginatorRef: Paginator<Contact>,
              private contactsService: ContactsService) {}

  ngOnInit() {
     this.pagination$ = this.paginatorRef.pageChanges.pipe(
       switchMap(( page ) => {
         const req = () => this.contactsService.getPage({
           page,
           perPage: 10
         });
         return this.paginator.getPage(req);
       })
     );
  }

  ngOnDestroy() {
    this.paginatorRef.destroy();
  }
}

Paginator предоставляет вам наблюдаемые pageChanges, чтобы вы могли прослушать изменения страницы и вызвать метод getPage(), передавая запрос. Paginator ожидает получить следующие поля как часть ответа от сервера:

{
  "perPage": 10,
  "lastPage": "10",
  "currentPage": "3",
  "data": [...]
}

Paginator также предоставляет все данные, которые вам нужно отобразить, а также методы управления страницей из пользовательского интерфейса:

<section>
  <h1>Contacts</h1>
  <loader *ngIf="paginatorRef.isLoading$ | async"><loader>

  <section *ngIf="(pagination$ | async) as pagination">
    <table>
      <thead>
        ..
      </thead>
      <tbody>
        <tr *ngFor="let contact of pagination.data">
          <td>{{contact.name}}</td>
          ...
        </tr>
      </tbody>
    </table>

    <p>{{pagination.from}} - {{pagination.to}} of {{pagination.total}}</p>

    <ul>
      <li [class.disabled]="paginatorRef.isFirst"
         (click)="paginatorRef.setFirstPage()">
         First page
      </li>

      <li [class.disabled]="paginatorRef.isFirst" (click)="paginatorRef.prevPage()">
         Prev
      </li>

      <li *ngFor="let page of pagination.pageControls"
          (click)="paginatorRef.setPage(page)"
          [class.active]="paginatorRef.isPageActive(page)">
          {{page}}
      </li>

      <li [class.disabled]="paginatorRef.isLast" (click)="paginatorRef.nextPage()">
         Next
      </li>

      <li [class.disabled]="paginatorRef.isLast" (click)="paginatorRef.setLastPage()">
         Last
      </li>
    </ul>
  </section>

</section>

Это все, что вам нужно, чтобы получить полностью работающую нумерацию страниц, включая кеширование.

Если вы не хотите, чтобы Paginator был одноэлементным, вы можете пропустить часть поставщика и просто создать новый экземпляр непосредственно в компоненте.

Продвинутая нумерация страниц:

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

@Component({
  selector: 'app-contacts-page'
})
export class ContactsPageComponent implements OnInit {
  pagination$;
  sortByControl: FormControl;
  perPageControl: FormControl;

  constructor(@Inject(CONTACTS_PAGINATOR) public paginatorRef: PaginatorPlugin<Contact>,
             private contactsService: ContactsService) {}

  ngOnInit() {
    this.sortByControl = new FormControl('price');
    this.perPageControl = new FormControl(10);

    const sort = this.sortByControl.valueChanges.pipe(startWith('price'));
    const perPage = this.perPageControl.valueChanges.pipe(startWith(10));

    this.pagination$ = combineLatest(
        this.paginatorRef.pageChanges,
        combineLatest(sort, perPage).pipe(
           tap(_ => this.paginatorRef.clearCache()
       ))).pipe(
        switchMap(([page, [sortBy, perPage]]) => {
          const req = () => this.contactsService.getPage({ page, sortBy, perPage });
          return this.paginatorRef.getPage(req);
      })
    );
  }

  ngOnDestroy() {
    this.paginatorRef.destroy();
  }
}

Когда каждый фильтр выдает новое значение, нам нужно сделать кеш недействительным, чтобы Paginator знал, что ему нужно повторно получить данные с сервера.

Метаданные пагинации:

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

@Component({
  selector: 'app-contacts-page'
})
export class ContactsPageComponent implements OnInit {
  pagination$;
  sortByControl: FormControl;
  perPageControl: FormControl;

  constructor(@Inject(CONTACTS_PAGINATOR) public paginatorRef: PaginatorPlugin<Contact>,
             private contactsService: ContactsService) {}

  ngOnInit() {
    const sortByInit = this.paginatorRef.metadata.get('sortBy') || 'name';
    const perPageInit = this.paginatorRef.metadata.get('perPage') || 10;

    this.sortByControl = new FormControl(sortByInit);
    this.perPageControl = new FormControl(perPageInit);

   const sort = this.sortByControl.valueChanges.pipe(startWith(sortByInit));
    const perPage = this.perPageControl.valueChanges.pipe(startWith(perPageInit));

    this.pagination$ = combineLatest(
        this.paginatorRef.pageChanges,
        combineLatest(sort, perPage).pipe(
           tap(_ => this.paginatorRef.clearCache()
       ))).pipe(
        switchMap(([page, [sortBy, perPage]]) => {
          const req = () => this.contactsService.getPage({ page, sortBy, perPage });
          this.paginatorRef.metadata.set('sortBy', sortBy);
          this.paginatorRef.metadata.set('perPage', perPage);

          return this.paginatorRef.getPage(req);
      })
    );
  }

  ngOnDestroy() {
    this.paginatorRef.destroy();
  }
}

API:

new Paginator({
  pagesControls: false, // создавать ли элементы управления страницей
  range: false, // следует ли генерировать диапазон ( from, to )
  startWith: 1, // начальная страница
  cacheTimeout: Observable<any> // когда вызываеть сброс кэша, например, interval(10000)
});
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться