[Angular 基础] - routing 路由(下)
之前部分 Angular 笔记:
使用 route
书接上回,继续折腾 routing
按照最初的 wireframe,它的实现是这样的:
之前为了简化一些实现,就直接采取了在 routes
下面声明一个新的路径,去采用重新渲染子组件的方式去进行重定向。这也会有几个比较麻烦的点:
- 数据渲染不完全
- 从
servers/:id
返回到servers
很麻烦
如果想要解决这个问题,将子组件重新渲染:
canDeactivate
canDeactivate
是一个在离开当前页面时会触发的 guard,一般可以用来检查未保存的内容,防止用户提前离开
具体实现方式如下:
-
创造一个 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();
}
}
-
在 routing module 中添加 guard:
const appRoutes: Routes = [
{
path: 'servers',
children: [
{
path: ':id/edit',
component: EditServerComponent,
canDeactivate: [CanDeactivateGuard],
},
],
},
];
-
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;
}
}
}
实现完成后的效果:
这里有 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
中的内容,最终的效果与实现的效果是一致的
最终效果:
可以看到渲染被延迟了大概一秒钟,然后输出了对应的 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
上去。但是这样做的一个问题就在于,如果想要做更多的报错处理,那么可能需要创造更多的报错页面。
下面提供一个可以复用
下面是步骤:
-
创建一个新的 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;
});
}
}
-
修改 app routing
const appRoutes: Routes = [
{
path: 'not-found',
component: ErrorPageComponent,
data: { message: 'Page not found!' },
},
{
path: '**',
redirectTo: '/not-found',
},
];
效果如下:
这样可以创建多个不同的路径,并传输不同的信息,实现使用一个 ErrorPageComponent
渲染不同的报错信息
如果搭配其他的 Observable,这里应该也是可以实现避开重复声明路由,而是直接用 **
wildcard 去渲染 ErrorPageComponent
,随后使用 Observable 获取报错信息