Angular: как маршрутизировать разные компоненты в зависимости от ответа API?

Мне интересно, можно ли/как реализовать конкретную стратегию маршрутизации в Angular 10.

Чтобы объяснить, возьмем этот упрощенный пример:

У меня есть приложение Angular с 4 компонентами:

  • StudentListComponent: отображает список всех учащихся.
  • CourseListComponent: отображает список всех курсов
  • StudentComponent: отображает сведения об одном конкретном студенте.
  • CourseComponent: отображает сведения об одном конкретном курсе.

Я хотел бы реализовать следующую стратегию маршрутизации

  • myapp.com/students маршрутов к StudentListComponent
  • myapp.com/courses маршрутов к CourseListComponent
  • myapp.com/millerh направляет к StudentComponent и отображает сведения об ученике с именем пользователя millerh
  • myapp.com/math1 направляет к CourseComponent и отображает сведения о курсе с коротким названием math1

Имена millerh и math1 уникальны, и не существует идентификатора, который сопоставляется и студенту, и курсу. (EDIT: также не будет студентов или курсов, называемых студентами или курсами)

Фон: Ссылки будут отображаться в другом приложении, где ссылки не будут интерактивными и не могут быть скопированы/вставлены, поэтому я хочу, чтобы пути были как можно короче. Кроме того, опытные пользователи предпочтут ввести URL-адрес напрямую, а не переходить на страницы со списками и искать студента/курс, сведения о котором они хотят просмотреть.

Для этого я хочу сделать вызов бэкенда/API во время маршрутизации. В ответе API будет указано, является ли Миллер студентом, курсом или ни одним из двух. В зависимости от этого я хочу перейти к соответствующему компоненту и передать имя студента/курса компоненту. . Я хотел бы избежать перенаправления, то есть с myapp.com/millerh на myapp.com/students/millerh, чтобы избежать нескольких допустимых путей для одного и того же ресурса.

Как я мог добиться этого, начиная с готового модуля угловой маршрутизации?

Большое спасибо за любые подсказки и предложения!


person Finrod_Amandil    schedule 15.12.2020    source источник
comment
в этих случаях неправильно использовать AuthGuard, но вы можете найти обходной путь. Вы можете сделать вызов API и перемещать пользователя туда, куда хотите. Но мне нравится ответ Эльмехди, вы можете попробовать так.   -  person Ashot Aleqsanyan    schedule 15.12.2020
comment
Что делать, если у вас есть студент с именем пользователя students или курс с коротким названием courses?   -  person Andrei Tătar    schedule 15.12.2020
comment
Серверная часть гарантирует отсутствие конфликтов между именами студентов, названиями курсов и именами статических маршрутов. Конечно, это потенциальный источник непреднамеренного поведения, но я знаю об этом и позабочусь об отсутствии конфликтов. Вариант использования, представленный здесь, является лишь примером, фактический вариант использования имеет другую область, в которой я могу выбрать эти имена самостоятельно.   -  person Finrod_Amandil    schedule 15.12.2020
comment
Вам, вероятно, не нужен маршрутизатор, если вы не показываете компоненты на основе формата маршрута.   -  person Andrei Tătar    schedule 15.12.2020
comment
также @Finrod_Amandil, как насчет преобразователей маршрутов? digitalocean.com/community/tutorials/angular-route-resolvers и angular.io/api/router/Resolve   -  person Ashot Aleqsanyan    schedule 15.12.2020
comment
@AshotAleqsanyan спасибо за подсказки Guards и Resolvers - это интересные идеи для выполнения пользовательского кода во время маршрутизации. Однако, как я понимаю, оба они привязаны к конкретному маршруту, сопоставленному с конкретным компонентом. Однако это невозможно, так как я еще не знаю, какой маршрут относится к какому компоненту в то время, когда я хочу сделать вызов API. Может быть, было бы возможно иметь маршрут совпадения с подстановочными знаками, который ссылается на поддельный компонент, присоединить Guard/Resolver к этому маршруту, а затем выбрать компонент в логике Guard/Resolver?   -  person Finrod_Amandil    schedule 15.12.2020
comment
Что-то вроде { path: '*', component: FakeComponent, resolve: { route: RouteResolver } }   -  person Finrod_Amandil    schedule 15.12.2020
comment
вы можете хранить данные для каждого маршрута и получать их в стороже или распознавателе. angular.io/api/router/Route#data   -  person Ashot Aleqsanyan    schedule 15.12.2020
comment
О, конечно :) так что путь, вероятно, будет просто чем-то вроде path: ':route', и тогда я смогу просто работать с именем маршрута в распознавателе/защите. Это звучит как очень многообещающий подход, я попробую, спасибо!   -  person Finrod_Amandil    schedule 15.12.2020
comment
Я имею в виду, что вы можете, например, иметь {path: '/students', component: ......, data: {key: someNecessary data which needed in the guard } }, а в гвардии вы можете получить его route.data.   -  person Ashot Aleqsanyan    schedule 15.12.2020


Ответы (2)


Я не думаю, что в маршрутизаторе Angular есть какие-либо средства различения между millerh, являющимся студентом, и math1, являющимся курсом, я думаю, вам следует использовать что-то вроде:
myapp.com/students/millerh
myapp.com/courses/math1

РЕДАКТИРОВАТЬ

Представьте, что один из ваших пользователей хочет найти студента, он наберет myapp.com/students/<studentName>, затем приложение перейдет к компоненту students, и studentName будет доступно в качестве параметров маршрутизации, затем вы можете использовать хук жизненного цикла ngOnInit, чтобы сделать свой API вызов конечной точки студентов вашего бэкэнда.

РЕДАКТИРОВАТЬ номер 2

Решение без необходимости вводить студентов/курсы в URL-адрес, но они все равно будут у вас после разрешения URL-адреса.

Допустим, сначала вы не хотите различать маршрут студентов и маршрут курсов.
Представьте тип вашего пользователя myapp.com/<someValue>, пока вы не знаете, что он ищет, студента или курса, сначала он собирается приземлитесь на корневой компонент, в этом компоненте вы можете использовать ngOnInit для выполнения вызовов API.
Начните с вызова API к конечной точке вашего ученика, если там есть ученик, сделайте так, чтобы ваше приложение программно перешло к myapp.com/students/<someValue>.
Если студент не найден, сделайте API-вызов конечной точки вашего курса. Если курс есть, сделайте так, чтобы ваше приложение программно перешло к myapp.com/courses/<someValue>.
В конечном итоге в URL-адресе будет ссылка на любого из студентов курсов, но вашим пользователям будет проще, потому что они могут напрямую вводить myapp.com/<someValue>.

Решение без необходимости вводить студентов/курсы в URL-адресе, и вы также не будете иметь их в URL-адресе в конце

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

person Elmehdi    schedule 15.12.2020
comment
Спасибо за ваш ответ! Конечно, Frontend/Angular не может определить это сам по себе. Во время маршрутизации я хочу сделать вызов своему бэкенду, который может проверить базу данных, чтобы выяснить, является ли конкретное имя студентом или курсом (или ни тем, ни другим). В основном это суть моего вопроса: как я могу сделать вызов API во время маршрутизации. - person Finrod_Amandil; 15.12.2020
comment
@Finrod_Amandil Я добавил некоторые пояснения к своему ответу, скажите, нужна ли вам живая демонстрация, так как мне потребуется немного времени, чтобы сделать это. - person Elmehdi; 15.12.2020
comment
Спасибо еще раз. Я знаком с тем, как добиться описанного вами шаблона маршрутизации, и я буду использовать его, если не получу стратегию маршрутизации, описанную в вопросе, для работы. Мне особенно интересно, можно ли/как добиться маршрутизации без части /students/, чтобы сократить URL-адреса (см. Вопрос для мотивации) - person Finrod_Amandil; 15.12.2020
comment
@Finrod_Amandil Я добавляю еще несколько сценариев :) - person Elmehdi; 15.12.2020
comment
Большое спасибо за ваши предложения! Они оба определенно звучат как работающие решения. Второе в настоящее время является моим любимым решением, и я могу попробовать его, если шаблон корневого компонента выглядит примерно так: <student-detail *ngIf="isStudent"></student-detail><course-detail *ngIf="isCourse"></course-detail> Это определенно хакерский подход, и он работает только в том случае, если Angular действительно не загружает подкомпоненты, для которых ngIf имеет значение false. - person Finrod_Amandil; 15.12.2020
comment
@Finrod_Amandil, не забудьте принять мой ответ, если он оказался вам полезен :) - person Elmehdi; 16.12.2020
comment
Я не забыл! Сначала я хочу реализовать рабочее решение, которое я сделаю очень скоро. Затем я либо приму ваш ответ, если мое окончательное решение основано на ваших предложениях, либо напишу другой ответ, если это другое решение. - person Finrod_Amandil; 16.12.2020

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

Окончательное решение использует в своей основе защиту, реализующую CanActivate, в сочетании с router.resetConfig() и router.navigate() внутри защиты.

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

файл: app-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './features/home/home.component';
import { StudentListComponent} from './features/student-list/student-list.component';
import { CourseListComponent} from './features/course-list/course-list.component';
import { LoadingComponent } from './features/loading/loading.component';
import { NotFoundComponent } from './features/not-found/not-found.component';
import { DynamicRouteGuard } from './guards/dynamic-route.guard';

const routes: Routes = [
  { 
    path: '/', 
    component: HomeComponent
  },
  {
    path: 'students',
    component: StudentListComponent
  },
  {
    path: 'courses',
    component: CourseListComponent
  },
  {
    path: 'not-found',
    component: NotFoundComponent
  },
  {
    path: '**',
    canActivate: [ DynamicRouteGuard ],
    component: LoadingComponent,
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Пути /, студенты, курсы и не найдены — это обычные статические маршруты, как и в большинстве приложений Angular. Любой маршрут, который не соответствует им, будет обработан подстановочным знаком внизу. Этот маршрут загружает компонент, который может содержать счетчик загрузки. Он будет виден, пока Guard асинхронно выполняет вызов API к серверной части, чтобы определить, какой компонент загрузить.

файл: dynamic-route.guard.ts

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { EntityTypeService } from '../../services/entity-type.service';
import { IEntityTypeModel } from '../../models/entity-type.model';
import { EntityType } from '../../models/entity-type.enum';
import { StudentDetailComponent } from '../features/student-detail/student-detail.component';
import { CourseDetailComponent } from '../features/course-detail/course-detail.component';

@Injectable({
  providedIn: 'root'
})
export class DynamicRouteGuard implements CanActivate {
  constructor(
    private entityTypeService : EntityTypeService,
    private router : Router) { }

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): 
    Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {

    // At this point, only routes with a single path element are considered valid routes.
    if (route.url.length != 1) {
      this.router.navigate(['not-found']);
      return false;
    }

    let path = route.url[0].toString();

    // Ask backend, what kind of entity the requested path matches to
    this.entityTypeService.getEntityTypeForPath(path).subscribe(response => {
      let entityTypeModel = response as IEntityTypeModel;

      // If backend did not recognize path --> redirect to page not found component.
      if (entityTypeModel.entityType === EntityType.Unknown) {
        this.router.navigate(['not-found']);
      }

      // Build a new routes array. Slicing is required as wildcard route
      // should be omitted from the new routes array (risk of endless loop)
      let routes = this.router.config;
      let newRoutes = routes.slice(0, routes.length - 1);

      // Add a new route for the requested path to the correct component.
      switch(entityTypeModel.entityType) {
        case EntityType.Student:
          newRoutes.push({ path: path, component: StudentDetailComponent, data: { resourceName: path } });
          break;
        case EntityType.Course:
          newRoutes.push({ path: path, component: CourseDetailComponent, data: { resourceName: path } });
          break;
        default:
          this.router.navigate(['not-found']);
          return;
      }

      // Reload routes and navigate.
      this.router.resetConfig(newRoutes);
      this.router.navigate([path]);
    });

    // Guard always returns true and loads LoadingComponent while API 
    // request is being executed.
    return true;
  }
}

В стороже запрошенный маршрут доступен, и можно легко внедрить службу API. Используя эту службу, серверная часть ищет путь в базе данных и возвращает значение перечисления, указывающее, является ли это студентом, курсом или ни тем, ни другим. В зависимости от этого в массив Routes добавляется новый маршрут для этого конкретного имени студента/курса, связанный с соответствующим компонентом. После этого маршруты перезагружаются, и маршрутизатор может напрямую перейти к нужному компоненту.

person Finrod_Amandil    schedule 17.12.2020