Проблемы защиты роута в Angular

Проблемы защиты роута в Angular

От автора: Angular роутинг все еще не идеален. По крайней мере, это можно сказать про последнюю стабильную версию 4.3.6, о которой мы и поговорим в этой статье. Вы заметите это, когда попробуете прототипировать более сложную архитектуру роутинга. Вложенная структура будет полна resolve и canActivation, особенно при разрастании приложения. В этой статье я постараюсь пролить свет на сложности, с которыми я столкнулась при работе с Angular Router.

Ну а пока, начнем с основ. Представьте, что у нас есть страница с информацией о двух брендах машин: tesla и arrinera. Роутинг может быть таким:

/cars
/cars/tesla
/cars/arrinera

Давайте зададим роутинг:

{ 
 path: 'cars',
 component: CarsComponent,
 children: [
 { path: ':cid', component: CarComponent }
 ] 
}

Довольно просто, правда? Нам нужна информация о машинах, давайте создадим CarsResolver в качестве провайдера данных:

@Injectable()
class CarsResolver implements Resolve<any> {
 public resolve() {
 return Observable.of([{name: 'tesla'}, {name: 'arrinera'}]);
 }
}

И обновим объявление роута:

Практический курс по созданию веб-приложения на Angular4

Станьте профессиональным веб-разработчиком, создавая востребованные веб-приложения на Angular4.

Узнать подробнее
{ 
 path: 'cars',
 component: CarsComponent,
 resolve: { cars: CarsResolver },
 children: [
 { path: ':cid', component: CarComponent }
 ]
}

Теперь представьте, что нам нужно защитить наш роут :cid, чтобы он принимал лишь 2 значения: tesla и arrinera, передаваемых CarsResolver: Наша главная цель:

/cars/tesla -> ok!
/cars/arrinera -> ok!
/cars/ford -> нельзя!

Просто, подумаете вы и скажите «давайте используем CanActivate». ОК, давайте попробуем. Он должен проверять, доступен ли провайдер по бренду автомобиля (:cid) в разрешенном родительском списке роутов cars. Если да, позволяем инициализировать компонент, если нет – запрещаем.

{ 
 path: 'cars',
 component: CarsComponent,
 resolve: { cars: CarsResolve },
 children: [
 { 
 path: ':cid',
 canActivate: [ CarGuard ],
 component: CarComponent 
 }
 ]
}

Установка CarGuard:

@Injectable()
class CarGuard implements CanActivate<boolean> {
 public resolve(route: ActivatedRouteSnapshot) {
 const resolvedCars = route.parent.data.cars;
 const carId = route.params.cid;
 const car = resolvedCars.find(car => car.name === carId);
 
 return Observable.of(!!car);
 }
}

Довольно просто: получите доступ к данным cars, разрешенным CarsResolver на родительском роуте. Найдите в объекте списка марок автомобилей cid, предоставленный пользователем в URL, и разрешите его булево значение, которое показывает доступность авто.

Сохраните, запустите и бац! Не работает. route.parent.data.cars не определен. Почему? Потому что CarsResolve еще не запустился. Внутри Angular взывает CanActivate до процесса разрешения данных (resolve). То есть у нас нет доступа к разрешенным данным. Подход canActivate больше похож на: «Можно перейти по роуту? Если да, запусти резолв, потом инициализируй компонент». Что еще хуже, когда у нас 2 canActivate (один в дочернем роуте, другой в родительском), оба вызываются одновременно. Это известная проблема, и ее уже пофиксили в 5.0.0-beta.1. Но пока что забудьте про нее. Правда в том, что в определенным обстоятельствах мы не можем пользоваться преимуществами Angular Guards.

Нам нужен некий гибрид Resolve и canActivate. Resolve может достучаться до разрешенных данных родительского роута. Это огромное преимущество над canActivate. Guards, с другой стороны, могут предотвращать инициализацию компонентов, но как это сделать? Давайте покопаемся во фреймворке…

Когда заданный любым пользователем canActivate резолвит в false (см. здесь), вызывается функция canLoadFails. Эту функция вызывает другую общую функцию navigationCancelingError, которая генерирует ошибку Observable, который содержит интересный объект Error со свойством ngNavigationCancelingError, заданным в true. Такой хорошо сформированный Error вынуждает Angular не инициализировать компонент. Это нам и нужно для защиты роута! С помощью этого мы можем задать кастомный resolve, который будет вести себя одновременно как canActivate и resolve. Назовем его CarResolve.

Замените нерабочий canActivate на CarResolve:

{ 
 path: 'cars',
 component: CarsComponent,
 resolve: { cars: CarsResolve },
 children: [
 { 
 path: ':cid',
 resolve: { car: CarResolve },
 component: CarComponent 
 }
 ]
}

И установите CarResolve как:

@Injectable()
class CarResolve implements Resolve<any> {
 public resolve(route: ActivatedRouteSnapshot) {
 
 const resolvedCars = route.parent.data.cars;
 const cid = route.params.cid;
 const car = resolvedCars.find(car => car.name === cid);
 if (car) {
 return Observable.of(car);
 } else {
 return Observable.throw({ 
 ngNavigationCancelingError: true 
 });
 }
 }
}

Как это работает? Так как это resolve, у нас есть доступ к разрешенным данным от родительского списка роута cars. Бросая объект со свойством ngNavigationCancelingError, мы ведем себя как canActivate. Это вынуждает Angular отказаться от инициализации компонента. Роут событие NavigationCancel создается и отсылается. Оно нам пригодится позже, а сейчас давайте протестируем наши роутинг переходы от источника к цели:

/cars -> /cars/tesla // можн!
/cars/tesla -> /cars/arrinera // можно!
/cars/arrinera -> /cars/ford  // нельзя!

Магия для переходов состояний! Выше сказано, что начальное состояние уже активировано (/cars, /cars/tesla, /cars/arrinera), после чего мы пытаемся перейти на целевой роут. Переход может быть удачным или нет. Но что происходит, когда переход не удается? Вызывается функция resetUrlToCurrentUrlTree. Она лишь заменяет URL на адрес исходного роута. Когда состояние активировано (например, /cars), и мы пытаемся перейти по неверному роуту /cars/ford, после отмены перехода эта функция откатывает URL до последнего активного роута (/cars). С точки зрения пользователя ничего не изменится, мы останемся в исходном роуте.

Но что если у нас не активировано исходное состояние? А такое возможно? Да. Это можно сделать, открыв в браузере новую вкладку и перейдя по адресу http://localhost:4200/cars/ford. Angular роутер пытается активировать состояние /cars/ford, и у него не получается… К сожалению, так как у нас нет исходного активного состояния, будет показана пустая страница. По крайней мере, корневой элемент полностью пустой. Не знаю, должно ли быть так, но эта проблема возникает также для canActivate.

Как обойти эту проблему? Просто следя за исходным URL после отмены активации роута. Давайте применим подписку к событию NavigationCancel:

this.router.events
 .filter(event => event instanceof NavigationCancel)
 .subscribe(event => {
 const { url } = this.router.currentRouterState.snapshot;
 if (url === '') { 
 this.router.navigate(['/cars']);
 }
 });

Мы получили событие NavigationCancel, а свойство ActivatedRouteSnapshot url ведет на пустую строку, т.е. пользователь открыл urL (который ведет на неверное состояние приложения) в новой вкладке браузера. Мы можем перенаправлять пользователя на заданный роут, скрывая пустую страницу. Редирект можно поместить внутрь CarResolve, но мне кажется, resolve должен резолвить данные (и выбрасывать ошибки) без дополнительной логики редиректов.

Кликните по ссылке для перехода на plunker с примером приложения с описанными проблемами. Посмотрите код и обязательно кликните Launch the preview in a separate window, чтобы поиграться с возможностями роутинга в новой вкладке браузера. Посмотрите консоль.

Автор: Adam Płócieniak

Источник: https://medium.com/

Редакция: Команда webformyself.

Практический курс по созданию веб-приложения на Angular4

Станьте профессиональным веб-разработчиком, создавая востребованные веб-приложения на Angular4.

Узнать подробнее
Самые свежие новости IT и веб-разработки на нашем Telegram-канале

Angular 4 с Нуля до Профи

Angular 4 - полное руководство для современной веб-разработки

Научиться

Метки:

Похожие статьи:

Комментарии Вконтакте:

Комментарии Facebook:

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Можно использовать следующие HTML-теги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Я не робот.

Spam Protection by WP-SpamFree