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


之前部分 Angular 笔记:


使用 route

书接上回,继续折腾 routing

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

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

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

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

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

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();
      }
    }
    
  2. 在 routing module 中添加 guard:

    const appRoutes: Routes = [
      {
        path: 'servers',
        children: [
          {
            path: ':id/edit',
            component: EditServerComponent,
            canDeactivate: [CanDeactivateGuard],
          },
        ],
      },
    ];
    
  3. EditServerComponent 实现 canDeactivate 函数

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

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

    这也是为什么 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;
        }
      }
    }
    

实现完成后的效果:

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

这里有 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<CanComponentDeactivate>
// {
//   canDeactivate(
//     component: CanComponentDeactivate,
//     currentRoute: ActivatedRouteSnapshot,
//     currentState: RouterStateSnapshot,
//     nextState: RouterStateSnapshot
//   ): boolean | Observable<boolean> | Promise<boolean> {
//     return component.canDeactivate();
//   }
// }
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;
      }
    }
    

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

  • 更新 routing module

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

    const routes = (Routes = [
      {
        path: 'servers',
        children: [
          {
            path: ':id',
            component: ServerComponent,
            resolve: { server: ServerResolverService },
          },
        ],
      },
    ]);
    

    这一步操作会将获取的 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;
        });
      }
    }
    

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

最终效果:

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

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


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

export const serverResolver: ResolveFn<Server> = (route) => {
  const serverId = parseInt(route.params.id);

  return inject(ServersService).getServer(serverId);
};

⚠️:getServer 返回的是一个 Server

传输数据

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

下面提供一个可以复用

下面是步骤:

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

    • V 层
    <h4>{{ errorMessage }}</h4>
    
  • 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. 修改 app routing

    const appRoutes: Routes = [
      {
        path: 'not-found',
        component: ErrorPageComponent,
        data: { message: 'Page not found!' },
      },
      {
        path: '**',
        redirectTo: '/not-found',
      },
    ];
    

效果如下:

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

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

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

03-06 11:46