[Разработка веб-сайтов, Open source, Angular] Angular: Показываем скелетон страницы за три шага

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
02-Мар-2021 13:30


Привет, меня зовут Олег. Я работаю в команде Тинькофф Бизнеса. В моей работе часто происходит ситуация, когда странице нужны данные с сервера. Пока мы запрашиваем эти данные, на странице надо что-нибудь показать. Но тут возникает проблема: в таком случае нам придется свой чистенький и красивый компонент превращать в страшненького квазисолидного уродца с примешанной логикой для индикации загрузки.Если вы сталкивались с подобными ситуациями, эта статья поможет вам все исправить за три простых шага.Типичный случай выглядит примерно так:
@Component({
   template: `
       <ng-container *ngIf="!loading; else skeleton">
           <h1>Hello, {{ user.name }}</h1>
       </ng-container>
       <ng-template #skeleton>
           <h1>Loading, please wait...</h1>
       </ng-template>
   `,
})
export class UserComponent implements OnDestroy {
   subscription = Subscription.EMPTY;
   loading = true;
   user: User | null = null;
   constructor(usersService: UsersService) {
       this.subscription = usersService.getCurrentUser().subscribe(user => {
           this.user = user;
           this.loading = false;
       });
   }
   ngOnDestroy() {
       this.subscription.unsubscribe();
   }
}
Что тут у нас? 
  • Лишние свойства у класса — для отслеживания статуса загрузки.
  • Подписочки-отписочки, от которых хотелось бы отказаться.
  • Чрезмерно усложнившийся шаблон с дополнительным локальным шаблоном и условием для индикации статуса загрузки. 
Вдобавок этот обслуживающий код будет копироваться от компонента к компоненту. Уф-ф-ф, этот код явно с проблемами! Мы-то хотели просто улучшить видимую производительность, показав скелетон страницы пользователю, пока загружаются данные…Может, получится отделить логику показа скелетона от самой страницы, поместить ее в отдельный компонент и переиспользовать? Давайте разберемся, как это можно исправить, сделав один дополнительный компонент, который будет показывать скелетон будущей страницы!Шаг первый: определяемся, когда мы хотим показывать скелетонКроме проблем, описанных ранее, есть еще одна: если оставить все как есть, мы не сможем использовать гарды и резолверы, поскольку они отрабатывают до того, как компонент создается. Это значит, что мы не сможем показать скелетон, находящийся внутри страницы. К счастью, фреймворк предоставляет нам события о том, на каком этапе навигации мы сейчас находимся, и мы можем использовать это!После некоторых размышлений я, кажется, пришел к единственному правильному выводу: показываем скелетон страницы с момента начала проверки на доступ к странице до момента завершения текущей навигации или ее отмены или ошибки в процессе навигации. Для воплощения этого замысла нам поможет документация, где описан порядок, в котором происходят события роутера. Запишем в конструкторе компонента:
const start = router.events.pipe(
    filter(event => event instanceof GuardsCheckStart),
);
const end = router.events.pipe(
    filter(
        event =>
            event instanceof NavigationEnd ||
            event instanceof NavigationCancel ||
            event instanceof NavigationError,
    ),
);
Шаг второй: получаем скелетон, соответствующий страницеЧасто случается так, что у нескольких страниц одинаковое строение и, соответственно, одинаковые скелетоны. Поэтому наиболее удачным решением будет выделить скелетон страницы в отдельный компонент и переиспользовать его по необходимости. А о том, как его передавать и получать, я сейчас расскажу!Как же нам получить скелетон, соответствующий странице, на которую происходит навигация? Очень просто. Мы положим скелетон рядом с самой страницей в конфигурации роута, вот так:
const route: Route = {
    path: '...',
    component: MyComponent,
    data: {
        skeleton: MySkeletonComponent,
    },
};
Положили, но как его достать при навигации? С этим нам опять поможет событие роутера, которое содержит информацию о странице, на которую переходит пользователь.На первый взгляд кажется, что снимок будущего состояния роутера не содержит информации об активированной странице. Но, присмотревшись получше, можно получить нужные данные. Свойство .firstChild указывает не на первый роут в массиве роутов, а на первый активированный из них, то есть тот, на который мы переходим. Запишем в конструкторе компонента:
const skeleton = router.events.pipe(
    filter(event => event instanceof RoutesRecognized),
    map((event: RoutesRecognized) => {
        let route = event.state.root;
        while (route.firstChild) {
            route = route.firstChild;
        }
        const component = route?.routeConfig?.data?.skeleton;
        return component ? {component} : null;
    }),
);
Шаг третий: катим в продОсталось только сложить первые два шага — и практически все готово. Вот код, который получился у меня, допишем его в конструктор компонента:
this.skeleton = skeleton.pipe(
    switchMap(skeleton =>
        skeleton
            ? concat(
                  start.pipe(
                      mapTo(skeleton),
                      takeUntil(end),
                  ),
                  of(null),
              )
            : of(null),
    ),
);
И добавим вот такой код в шаблон компонента:
<ng-container *ngIf="skeleton | async as config else content">
    <ng-container
        *ngComponentOutlet="config.component"
    ></ng-container>
</ng-container>
<ng-template #content>
    <ng-content></ng-content>
</ng-template>
Круто, правда?Шаг назад: шаг, который существует вопрекиБыло бы еще круче, если бы все сразу работало так, как хочется. Я столкнулся с тем, что некоторые наши роуты находятся в отдельных, ленивых модулях, которые загружаются по необходимости. Поэтому и скелетоны, связанные с этими роутами декларируются в этих модулях (инжекторах ленивых модулей), а это значит, что эти компоненты невозможно создать в корневом модуле (корневом инжекторе). Если декларировать их в корневом модуле, это будет увеличивать размер основного бандла. Это не есть хорошо.Чтобы решить эту проблему, нам надо добраться до инжектора ленивого модуля, для этого придется залезть под капот фреймворка, посмотреть, как создаются ленивые модули и куда записывается информация об этом. К счастью, это место не менялось с 2017 года. Мы можем воспользоваться этой информаций и слегка изменить код под эти требования:
function getRouteInjector(route: Route | null): Injector | null {
    return (route as InternalRoute)?._loadedConfig?.module?.injector || null
}
const skeleton = router.events.pipe(
    filter(event => event instanceof RoutesRecognized),
    map((event: RoutesRecognized) => {
        let route = event.state.root;
        let injector = getRouteInjector(route.routeConfig);
        while (route.firstChild) {
            route = route.firstChild;
            injector = getRouteInjector(route.routeConfig) || injector;
        }
        const component = route?.routeConfig?.data?.skeleton;
        return component ? {component, injector} : null;
    }),
);
И подправим шаблон:
<ng-container *ngIf="skeleton | async as config else content">
    <ng-container
        *ngComponentOutlet="config.component; injector: config.injector"
    ></ng-container>
</ng-container>
<ng-template #content>
    <ng-content></ng-content>
</ng-template>
Я понимаю, что это решение выглядит не очень, но другого выхода я не нашел. Если есть идеи, как это сделать не залезая под капот фреймворка, не стесняйтесь — пишите комментарий!ВыводТеперь у нас есть чистая страница пользователя, которая содержит код только для отображения страницы пользователя, в ней нет обслуживающего кода для индикации загрузки пользователя с сервера и у которой единственная ответственность это отображать страницу пользователя. И есть отдельный, чистый скелетон этой страницы с минимальным количеством зависимостей, единственная ответственность которого — отображаться, пока загружаются данные, нужные для страницы.Весь представленный код я собрал и выложил в небольшую библиотечку: вот тут ее можно поддержать на GitHub, а вот тут скачать из npm. Она весит ≈1,5 кб, так что можете смело добавлять в свой проект.
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_razrabotka_vebsajtov (Разработка веб-сайтов), #_open_source, #_angular, #_angular, #_navigation, #_skeleton, #_perfomance, #_router, #_rxjs, #_observable, #_blog_kompanii_tinkoff (
Блог компании TINKOFF
)
, #_razrabotka_vebsajtov (
Разработка веб-сайтов
)
, #_open_source, #_angular
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 07:35
Часовой пояс: UTC + 5