• [Angular 基础] - routing 路由(下)


    [Angular 基础] - routing 路由(下)


    之前部分 Angular 笔记:


    使用 route

    书接上回,继续折腾 routing

    按照最初的 wireframe,它的实现是这样的:

    在这里插入图片描述

    之前为了简化一些实现,就直接采取了在 routes 下面声明一个新的路径,去采用重新渲染子组件的方式去进行重定向。这也会有几个比较麻烦的点:

    1. 数据渲染不完全
    2. servers/:id 返回到 servers 很麻烦

    如果想要解决这个问题,将子组件重新渲染:

    <div class="col-xs-12 col-sm-4">
      <app-edit-server>app-edit-server>
      <hr />
      <app-server>app-server>
    div>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    又会遇到下面这些问题:

    1. 在主界面时代码抛出异常

      在这里插入图片描述

      有些情况 Cannot read properties of undefined 是会 break 页面,从而导致内容无法渲染的情况

      会有这个问题也是因为子页面尚未被选中,路径还是 http://localhost:4200/servers,自然无法获取对应的 server 数据

    2. 渲染了额外数据

      理想条件下,当在 /servers 这个路径下,代表着没有任何的服务器被选中,那么也不应该渲染对应的组件

    为了更加优雅的解决这个问题,而不是使用大量的 ngIf 去进行条件控制,Angular 提供了 child/nested routes 的解决方案

    child route

    突然想到, react-router v6 也实现了这个功能……果然这日子还是到了前端框架互相抄的日子了……

    这里具体修改的配置如下:

    • routing module

      const appRoutes: Routes = [
        {
          path: 'servers',
          component: ServersComponent,
          children: [
            {
              path: ':id',
              component: ServerComponent,
            },
            {
              path: ':id/edit',
              component: EditServerComponent,
            },
          ],
        },
      ];
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
    • servers V 层

      <div class="row">
        <div class="col-xs-12 col-sm-4">
          <div class="list-group">
            <a
              [routerLink]="['/servers', server.id]"
              [queryParams]="{ allowEdit: server.id === 3 ? '1' : '0' }"
              fragment="loading"
              class="list-group-item"
              *ngFor="let server of servers"
            >
              {{ server.name }}
            a>
          div>
        div>
        <div class="col-xs-12 col-sm-4">
          <router-outlet>router-outlet>
        div>
      div>
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18

    实现后的效果:

    在这里插入图片描述

    就像是在 appRoutes 中定义的一样,一旦路径为 servers/:id,那么就会渲染单独的 server component,如果路径是 servers/:id/edit,就会渲染 edit-server component

    我的理解是,一旦将对应的组件放到了 children 中,就会形成以父组件为基础的一个单独模块。Angular 就像处理其他模块一样,都需要通过一个 router-outlet 去进行监听,才能做出合适的反应

    如果 V 层没有添加 router-outlet 的话,那么 children 中的模块就不会被渲染

    更多 query param 的部分

    上面的动图其实还有一个问题,那就是当从 servers/:id 定向到 servers/:id/edit 时,后置的 query parameters 全都丢了,这也是 Angular 的默认行为,想要改变这个行为,需要在 click handler 中进行处理,下面是对于 edit server 按钮的事件处理:

    export class ServerComponent implements OnInit {
      onEdit() {
        this.router.navigate(['edit'], {
          relativeTo: this.route,
          queryParamsHandling: 'preserve',
        });
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    这里的 queryParamsHandling 有三个值:

    export declare type QueryParamsHandling = 'merge' | 'preserve' | '';
    
    • 1
    • merge

      会将当前组件有的 query parameters 与已经存在的 query parameter 进行合并

    • preserve

      会保留已经存在的 query parameter

    这时候的效果如下:

    在这里插入图片描述

    可以看到,allowEdit 这个参数被保留下来了,对应的 EditServerComponent 也可以通过当前的 query parameter 进行正常的逻辑处理

    ⚠️:EditServerComponent 有两个 subscription,一个 subscribe queryParams,另一个则是 subscribe 当前传来的 server id:

    export class EditServerComponent implements OnInit {
      ngOnInit() {
        this.route.queryParams.subscribe((queryParams: Params) => {
          this.allowEdit = queryParams.allowEdit === '1';
        });
        this.route.fragment.subscribe();
    
        const id = parseInt(this.route.snapshot.params.id);
    
        // subscribe route params to update the id if params change
        this.server = this.serversService.getServer(id);
        this.serverName = this.server.name;
        this.serverStatus = this.server.status;
    
        this.route.params.subscribe((params: Params) => {
          this.server = this.serversService.getServer(parseInt(params.id));
          this.serverName = this.server.name;
          this.serverStatus = this.server.status;
        });
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    添加 not found 页面

    根据当前的实现,如果用户意外输入了一些错误的路径,那么就会重新导航到首页,并且在 console 中输出报错信息:

    在这里插入图片描述

    这也是一个相对而言比较粗暴的处理方式,大多数的应用都会渲染一个 not found 页面,而不是直接显示一个空白屏幕。接下来就更新 app routing module 进行实现:

    const appRoutes: Routes = [
      // 其余处理不变
      { path: 'not-found', component: PageNotFoundComponent },
      { path: '**', redirectTo: '/not-found' },
    ];
    
    • 1
    • 2
    • 3
    • 4
    • 5

    ⚠️:这里假设 Not Found 页面已经通过 ng generate component 被创建了,也实现了对应的 V 层

    显示效果如下:

    在这里插入图片描述

    这里业务实现的逻辑也比较简单,首先是确定一个为 not-found 的路径,将对因渲染的组件设置为 PageNotFoundComponent。接下来就是终点的实现了,也就是一个 wildcard match,这个 match 必须 放在最下面,这样无法被之前路由 capture 的变化会落到这里,否则所有的页面都会渲染为 PageNotFoundComponent 组件。

    当当前路由找不到当前提供的路径时,它就会被重新定向到 /not-found 页面,从而渲染 PageNotFoundComponent 组件

    另一个稍微简单一点的处理方式就是直接将 **component: PageNotFoundComponent 进行绑定:{ path: '**', component: PageNotFoundComponent,这样就可以保留当前的路径,并渲染 PageNotFoundComponent 组件

    这里有一点像是 React Router Dom v5 里的 Switch,不过 React Router Dom 里面的 wildcard 时 *,这里是 **

    ⚠️:Agular 的路径 match 是通过前缀实现的,在 not found page 这个案例的情况是 wildcard,不会造成任何的问题。

    👀:一个使用情况可以从 '' 导航到 '/home,这时候配置可以这么写:{ path: '', redirectTo: 'home', patchMatch: 'full' },

    Guards

    guards 是用来控制路由的,博啊哭哦权限控制、是否能被访问等一些行为,每个 guard 都有不同的功能

    canActive

    这个 guard 用来控制当前路由是否可以被访问

    这里以 servers 为例,条件控制为只有登录的用户才可以访问 /servers,变动如下:

    • routing module

      const appRoutes: Routes = [
        {
          path: 'servers',
          canActivate: [AuthGuardService],
          component: ServersComponent,
        },
      ];
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
    • 新增 AuthGuardService

      @Injectable({
      providedIn: 'root',
      })
      export class AuthGuardService implements CanActivate, CanActivateChild {
      constructor(private authService: AuthService, private router: Router) {}
      
      canActivate(
          route: ActivatedRouteSnapshot,
          state: RouterStateSnapshot
      ): Observable<boolean> | Promise<boolean> | boolean {
      
          return this.authService.isAuthenticated().then((authenticated: boolean) => {
      
          if (authenticated) {
              console.log('authenthenciated');
              return true;
          } else {
            console.log('not authenthenciated');
              this.router.navigate(['/']);
              return false;
          }
          });
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23

    其中 isAuthenticated 的实现如下:

      isAuthenticated() {
        const promise = new Promise((resolve, reject) => {
          setTimeout(() => {
            resolve(this.login);
          }, 1000);
        });
    
        return promise;
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    展示效果如下:

    在这里插入图片描述

    这里 isAuthenticated 返回的永远是一个 promise,并且会在一秒钟后完成登陆,所以永远不会被重定向到首页。稍微代码,将 resolve 中改成 false,并修改一下冲定向的路径也可以实现一下效果:

    在这里插入图片描述

    这里都有一个 1s 的等待时间,这个是 setTimeout 决定的


    需要注意的是 CanActivate 已经 deprecated 了,原因似乎也是 Angular 要走走 functional guard,而 functional guard 的实现如下:

    这里的实现稍微复杂一些,

    1. 首先将 then 那一块代码抽出来,放到一个新的 service 中:

      @Injectable({
        providedIn: 'root',
      })
      export class PermissionsService {
        constructor(private authService: AuthService, private router: Router) {}
      
        canActivate(
          route: ActivatedRouteSnapshot,
          state: RouterStateSnapshot
        ): Observable<boolean> | Promise<boolean> | boolean {
          return this.authService
            .isAuthenticated()
            .then((authenticated: boolean) => {
              if (authenticated) {
                console.log('authenthenciated');
                return true;
              } else {
                console.log('not authenthenciated');
                this.router.navigate(['/forbidden']);
                return false;
              }
            });
        }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24

      ⚠️:这里使用 canActivate 只是一个约定俗成的规范,它可以改成任何一个名称,不是一定要用 canActivate

    2. 使用 functional guard

      export const authGuardFunc: CanActivateFn = (route, state) => {
        return inject(PermissionsService).canActivate(route, state);
      };
      
      • 1
      • 2
      • 3

      ⚠️:直接实现下面的代码,ide 不会报错,但是浏览器会报错:

      export const authGuardFunc: CanActivateFn = (route, state) => {
        return inject(AuthService)
          .isAuthenticated()
          .then((authenticated: boolean) => {
            if (authenticated) {
              console.log('authenthenciated');
              return true;
            } else {
              console.log('not authenthenciated');
              inject(PermissionsService).canActivate(route, state);
              return false;
            }
          });
      };
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14

      在这里插入图片描述

    canActiveChild

    有一个情况就是,用户可以访问 servers,但是不能访问 servers 的 children。一个解决方法是将 canActivate: [AuthGuardService], cv 到每一个 child component,不过当 child component 变多的时候,管理就会变得非常的麻烦。

    如果当前逻辑应用于所有的 child component,就可以使用 canActiveChild 去实现:

    @Injectable({
      providedIn: 'root',
    })
    export class AuthGuardService implements CanActivate, CanActivateChild {
      constructor(private authService: AuthService, private router: Router) {}
    
      // 新增部分,可以直接调用之前实现的 canActivate
      canActivateChild(
        childRoute: ActivatedRouteSnapshot,
        state: RouterStateSnapshot
      ): boolean | Observable<boolean> | Promise<boolean> {
        return this.canActivate(childRoute, state);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    并在 routing module 中实现对应的修改:

    const appRoutes: Routes = [
      {
        path: 'servers',
        canActivateChild: [AuthGuardService],
        // 其余部分省略
      },
    ];
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    效果如下:

    在这里插入图片描述


    同样,对应的 functional guard 的修改如下:

    1. permissions service 的修改:

      export class PermissionsService {
        constructor(private authService: AuthService, private router: Router) {}
      
        canActiveChild(
          route: ActivatedRouteSnapshot,
          state: RouterStateSnapshot
        ): Observable<boolean> | Promise<boolean> | boolean {
          return this.canActivate(route, state);
        }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
    2. func guard 的修改:

      export const authGuardFunc: CanActivateFn = (route, state) => {
        return inject(PermissionsService).canActivate(route, state);
      };
      
      export const authGuardChildFunc: CanActivateChildFn = (route, state) => {
        return inject(PermissionsService).canActiveChild(route, state);
      };
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7

    调用的时候则是直接在 canActivateChild 中放入 authGuardChildFunc 即可

    鉴于代码完全一致,放入 authGuardFunc 也没问题啦……

    canDeactivate

    canDeactivate 是一个在离开当前页面时会触发的 guard,一般可以用来检查未保存的内容,防止用户提前离开

    具体实现方式如下:

    1. 创造一个 service,具体实现如下:

      export interface CanComponentDeactivate {
        canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
      }
      
      @Injectable({
        providedIn: 'root',
      })
      export class CanDeactivateGuard
        implements CanDeactivate<CanComponentDeactivate>
      {
        canDeactivate(
          component: CanComponentDeactivate,
          currentRoute: ActivatedRouteSnapshot,
          currentState: RouterStateSnapshot,
          nextState: RouterStateSnapshot
        ): boolean | Observable<boolean> | Promise<boolean> {
          return component.canDeactivate();
        }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
    2. 在 routing module 中添加 guard:

      const appRoutes: Routes = [
        {
          path: 'servers',
          children: [
            {
              path: ':id/edit',
              component: EditServerComponent,
              canDeactivate: [CanDeactivateGuard],
            },
          ],
        },
      ];
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
    3. EditServerComponent 实现 canDeactivate 函数

      如果不实现的话,在离开当前页面会报错,也就 break project 了:

      在这里插入图片描述

      这也是为什么 Angular 的实现这么复杂……主要还是为了类型检查,以及 添加的功能都必须实现 这样的检查

      具体实现如下:

      @Component({
        selector: 'app-edit-server',
        templateUrl: './edit-server.component.html',
        styleUrls: ['./edit-server.component.css'],
      })
      export class EditServerComponent implements CanComponentDeactivate {
        serverName = '';
        serverStatus = '';
        allowEdit = false;
        changesSaved = false;
      
        canDeactivate(): boolean | Promise<boolean> | Observable<boolean> {
          if (!this.allowEdit) {
            return true;
          }
      
          if (
            (this.serverName !== this.server.name ||
              this.serverStatus !== this.server.status) &&
            !this.changesSaved
          ) {
            return confirm('Do you want to discard the changes?');
          } else {
            return true;
          }
        }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27

    实现完成后的效果:

    在这里插入图片描述

    这里有 3 种情况:

    1. 当用户没有编辑权限

      ✅ 直接允许重定向

    2. 当用户有编辑权限,但是用户 没有 编辑内容

      ✅ 直接允许重定向

    3. 当用户有编辑权限,并且用户 已经 编辑内容

      ❌ 不允许直接冲定向

      这里的具体操作是跳出一个 confirm,当用户确认后,即可重新定向


    这里也提出 functional guard 的实现方式,鉴于其他的变量名不变,所以这里只需要修改 CanDeactivateGuard service 即可:

    export interface CanComponentDeactivate {
      canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
    }
    
    export const CanDeactivateGuard: CanDeactivateFn<CanComponentDeactivate> = (
      component: CanComponentDeactivate
    ): Observable<boolean> | boolean => {
      if (component.canDeactivate && component.canDeactivate()) return true;
    };
    
    // @Injectable({
    //   providedIn: 'root',
    // })
    // export class CanDeactivateGuard
    //   implements CanDeactivate
    // {
    //   canDeactivate(
    //     component: CanComponentDeactivate,
    //     currentRoute: ActivatedRouteSnapshot,
    //     currentState: RouterStateSnapshot,
    //     nextState: RouterStateSnapshot
    //   ): boolean | Observable | Promise {
    //     return component.canDeactivate();
    //   }
    // }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    resolve

    canActivate 是控制用户允许访问当前页面, canDeactivate 是控制用户不允许访问当前页面,resolve 则是允许等待一段时间(如获取数据的异步操作),在完成操作后渲染组件

    实现如下:

    • 创建一个新的 resolver service

      interface Server {
        id: number;
        name: string;
        status: string;
      }
      
      @Injectable({
        providedIn: 'root',
      })
      export class ServerResolverService implements Resolve<Server> {
        constructor(private serversService: ServersService) {}
      
        resolve(
          route: ActivatedRouteSnapshot,
          state: RouterStateSnapshot
        ): Server | Observable<Server> | Promise<Server> {
          const promise: Promise<Server> = new Promise((resolve) => {
            setTimeout(() => {
              console.log('resolving');
      
              resolve(this.serversService.getServer(parseInt(route.params.id)));
            }, 1000);
          });
      
          return promise;
        }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27

      这个 service 就是实现一个 resolver,即在组件渲染之前获取对应的 server。我这里用 setTimeout 模拟了一个异步操作

    • 更新 routing module

      这里制定要使用 resolver 的组件,即 servers/:id

      const routes = (Routes = [
        {
          path: 'servers',
          children: [
            {
              path: ':id',
              component: ServerComponent,
              resolve: { server: ServerResolverService },
            },
          ],
        },
      ]);
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12

      这一步操作会将获取的 server——resolve 的数据——存储到 server 这个变量名中

    • 更新 server component

      export class ServerComponent implements OnInit {
        server: { id: number; name: string; status: string };
      
        ngOnInit() {
          this.route.data.subscribe((data: Data) => {
            console.log(data);
      
            this.server = data.server;
          });
        }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11

      这里主要更新 ngOnInit 中的内容,最终的效果与实现的效果是一致的

    最终效果:

    在这里插入图片描述

    可以看到渲染被延迟了大概一秒钟,然后输出了对应的 server


    同样增添一下 functional guard 的实现:

    export const serverResolver: ResolveFn<Server> = (route) => {
      const serverId = parseInt(route.params.id);
    
      return inject(ServersService).getServer(serverId);
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    ⚠️:getServer 返回的是一个 Server

    传输数据

    之前在 canActivate guard 中创建了一个 forbidden 页面,这样每次页面报错都会重新导航到 /forbidden 上去。但是这样做的一个问题就在于,如果想要做更多的报错处理,那么可能需要创造更多的报错页面。

    下面提供一个可以复用

    下面是步骤:

    1. 创建一个新的 generic error 页面

      • V 层
      <h4>{{ errorMessage }}h4>
      
      • 1
    • VM 层

      @Component({
        selector: 'app-error-page',
        templateUrl: './error-page.component.html',
        styleUrl: './error-page.component.css',
      })
      export class ErrorPageComponent implements OnInit {
        errorMessage: string;
      
        constructor(private route: ActivatedRoute) {}
      
        ngOnInit() {
          this.errorMessage = this.route.snapshot.data['message'];
          this.route.data.subscribe((data: Data) => {
            this.errorMessage = data.message;
          });
        }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
    1. 修改 app routing

      const appRoutes: Routes = [
        {
          path: 'not-found',
          component: ErrorPageComponent,
          data: { message: 'Page not found!' },
        },
        {
          path: '**',
          redirectTo: '/not-found',
        },
      ];
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11

    效果如下:

    在这里插入图片描述

    这样可以创建多个不同的路径,并传输不同的信息,实现使用一个 ErrorPageComponent 渲染不同的报错信息

    如果搭配其他的 Observable,这里应该也是可以实现避开重复声明路由,而是直接用 ** wildcard 去渲染 ErrorPageComponent,随后使用 Observable 获取报错信息

  • 相关阅读:
    短视频直播带货app源码, 一套系统刷视频购物都能用
    一文弄懂Python中的Map、Filter和Reduce函数
    性能测试 —— 吞吐量和并发量的关系? 有什么区别?
    uniapp小程序解决不能上传文件/图片问题
    深度学习基础知识 学习率调度器的用法解析
    GPIO基本原理
    《微服务实战》 第十八章 Redis查看配置文件和数据类型
    Science adv | 转录因子SPIC连接胚胎干细胞中的细胞代谢与表观调控
    私有化输出的服务网格我们是这样做的
    verilog实现I2C控制器 (小梅哥思路)----详细解析
  • 原文地址:https://blog.csdn.net/weixin_42938619/article/details/136476466