上一篇的教程中,我们讲述了如何创建一个简单的列表,满足增删改查的功能,其中改的功能是直接在列表中修改。这种情况是基于要展示的对象字段较少,可以直接完全在列表中展示出来的情况。但是实际的项目中,往往会遇到列表中不完全展示所有字段,而是在点击某一行时跳转到编辑页面展示并且在编辑页面进行修改。这时对于单页应用,我们就会遇到这样的问题,由于单页应用默认路由跳转的时候都会重新加载该路由页面,即刷新页面。但是当我们从列表页选择某一行跳转到详情页然后返回列表页的时候不想刷新怎么办呢?例如用户浏览到第5页的时候进入其中一条详情页后返回列表页,这时候如果刷新了页面,那么就会回到第一页,这对用户来说非常不友好。这个问题该怎么解决呢?在angular框架中就提供了解决方案,就是路由复用策略——RouteReuseStrategy。
我们先按照上一篇教程的步骤构建一个教师的列表页面和一个新增和修改教师共用的页面。
- 在service目录下面新建一个teacher.service.ts,在其中写入增改查和获取详情的四个api访问方法。
import {Injectable} from '@angular/core';
import {Observable} from 'rxjs';
import {HttpClient, HttpHeaders, HttpParams} from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class TeacherService {
teacherUrl = '/studentmanage/teacher/';
getTeachers(
pageIndex: number = 0,
pageSize: number = 10,
name: string
): Observable<any> {
const params = new HttpParams()
.append('index', `${pageIndex}`)
.append('size', `${pageSize}`)
.append('name', name);
return this.http.get(`${this.teacherUrl}teachers`, {
params
});
}
editTeacher(
id: string,
name: string,
gender: number,
age: number,
subjectIds: any[]
): Observable<any> {
const options = { id, name, gender, age, subjectIds };
const formData = new FormData();
formData.append('id', id);
formData.append('name', name);
formData.append('gender', gender.toString());
formData.append('subjectIds', subjectIds.toString());
formData.append('age', age.toString());
return this.http.post(`${this.teacherUrl}editTeacher`, formData, {});
}
addTeacher(
name: string,
gender: number,
age: number,
subjectIds: []
): Observable<any> {
// const options = { name, gender, age, subjectIds };
const formData = new FormData();
formData.append('name', name);
formData.append('gender', gender.toString());
formData.append('subjectIds', subjectIds.toString());
formData.append('age', age.toString());
return this.http.post(`${this.teacherUrl}addTeacher`, formData, {});
}
getTeacherDetail(
id: string
): Observable<any> {
const params = new HttpParams()
.append('id', id);
return this.http.get(`${this.teacherUrl}teacherDetail`, {
params
});
}
constructor(private http: HttpClient) {}
}
- 利用idea的快捷命令创建一个teacher的component,就是教师列表页面,然后修改teacher.component.html
<nz-page-header [nzTitle]="title">
<nz-breadcrumb nz-page-header-breadcrumb [nzAutoGenerate]="true">
</nz-breadcrumb>
</nz-page-header>
<button nz-button nzType="primary" class="add-button" routerLink="addTeacher">新增</button>
<nz-table #ajaxTable
nzShowSizeChanger
[nzFrontPagination]="false"
[nzData]="teachers"
[nzLoading]="loading"
[nzTotal]="total"
[(nzPageIndex)]="pageIndex"
[(nzPageSize)]="pageSize"
(nzPageIndexChange)="searchData()"
(nzPageSizeChange)="searchData(true)" class="my-table">
<thead>
<tr>
<th nzWidth="5%">#</th>
<th nzCustomFilter nzWidth="30%">姓名</th>
<th nzWidth="12%">性别</th>
<th nzWidth="10%">年龄</th>
<th>科目</th>
<th nzWidth="20%">操作</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let data of ajaxTable.data;let i = index">
<td>{{i+1}}</td>
<td>{{ data.name }}</td>
<td>
<div *ngIf="data.gender === 1">
男
</div>
<div *ngIf="data.gender === 0">
女
</div>
</td>
<td>{{data.age}}</td>
<td>{{data.subjectNames}}</td>
<td>
<div class="editable-row-operations">
<a routerLink="editTeacher/{{data.id}}">编辑</a>
</div>
</td>
</tr>
</tbody>
</nz-table>
修改teacher.component.ts
import { Component, OnInit } from '@angular/core';
import {TeacherService} from '../service/teacher.service';
@Component({
selector: 'app-teacher',
templateUrl: './teacher.component.html',
styleUrls: ['./teacher.component.scss']
})
export class TeacherComponent implements OnInit {
title = '教师';
teachers = [];
pageIndex = 1;
pageSize = 5;
total = 1;
loading = true;
searchValue = '';
constructor(private teacherService: TeacherService) {
}
ngOnInit(): void {
this.searchData();
}
searchData(reset: boolean = false): void {
if (reset) {
this.pageIndex = 1;
}
this.loading = true;
this.teacherService.getTeachers(this.pageIndex - 1, this.pageSize, this.searchValue)
.subscribe(result => this.onSuccess(result));
}
onSuccess(result: any) {
this.loading = false;
console.log('result: ' + JSON.stringify(result));
const data = result.data;
this.teachers = data.teachers;
this.total = data.total;
}
}
- 在teacher的目录下面创建一个teacherForm的component,就是新增和修改教师共用的页面。然后修改teacher-form.component.html
<nz-page-header nzBackIcon [nzTitle]="title">
</nz-page-header>
<form nz-form [formGroup]="teacherForm" (ngSubmit)="submitForm()">
<nz-form-item>
<nz-form-label [nzSpan]="3" nzRequired nzFor="name">姓名</nz-form-label>
<nz-form-control [nzSpan]="8" nzErrorTip="请输入教师姓名">
<input type="text" nz-input formControlName="name" />
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label [nzSpan]="3" nzRequired nzFor="gender">性别</nz-form-label>
<nz-form-control [nzSpan]="8" nzErrorTip="请选择性别">
<nz-radio-group formControlName="gender">
<label nz-radio [nzValue]="1">男</label>
<label nz-radio [nzValue]="0">女</label>
</nz-radio-group>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label [nzSpan]="3" nzRequired nzFor="age">年龄</nz-form-label>
<nz-form-control [nzSpan]="8" nzErrorTip="请输入年龄">
<nz-input-number formControlName="age" [nzMin]="20" [nzMax]="70" [nzStep]="1"></nz-input-number>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label [nzSpan]="3" nzRequired nzFor="subjectIds">科目</nz-form-label>
<nz-form-control [nzSpan]="8" nzErrorTip="请选择科目">
<nz-select
style="width: 100%"
nzMode="multiple"
nzPlaceHolder="请选择"
[nzDropdownStyle]="{height: 50}"
(nzScrollToBottom)="loadMore()"
formControlName="subjectIds"
>
<nz-option *ngFor="let subject of subjects" [nzLabel]="subject.name" [nzValue]="subject.id"></nz-option>
<nz-option *ngIf="isLoading" nzDisabled nzCustomContent>
<i nz-icon nzType="loading" class="loading-icon"></i> 加载中...
</nz-option>
</nz-select>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-control [nzSpan]="8" [nzOffset]="4">
<button nz-button nzType="primary">提交</button>
</nz-form-control>
</nz-form-item>
</form>
- 修改teacher-form.component.ts
import { Component, OnInit } from '@angular/core';
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
import {SubjectService} from '../../service/subject.service';
import { Location } from '@angular/common';
@Component({
selector: 'app-subject-form',
templateUrl: './subject-form.component.html',
styleUrls: ['./subject-form.component.scss']
})
export class SubjectFormComponent implements OnInit {
title = '新增科目';
subjectForm: FormGroup;
submitForm(): void {
for (const i in this.subjectForm.controls) {
this.subjectForm.controls[i].markAsDirty();
this.subjectForm.controls[i].updateValueAndValidity();
if (this.subjectForm.controls[i].invalid) {
return;
}
}
this.subjectService.addSubject(this.subjectForm.value.name)
.subscribe(result => this.location.back());
}
constructor(private fb: FormBuilder, private subjectService: SubjectService,
private location: Location) {}
ngOnInit(): void {
this.subjectForm = this.fb.group({
name: [null, [Validators.required]]
});
}
}
- 最后把列表页面,新增教师和修改教师的路由添加到app-routing.module.ts中,完整的routes就为
const routes: Routes = [
{ path: '', pathMatch: 'full', redirectTo: '/home' },
{ path: 'home', component: HomeComponent, canActivate: [LoginGuard], children: [
{path: 'subject', component: SubjectComponent},
{path: 'subject/addSubject', component: SubjectFormComponent},
{path: 'teacher', component: TeacherComponent},
{path: 'teacher', component: TeacherComponent},
{path: 'teacher/addTeacher', component: TeacherFormComponent},
{path: 'teacher/editTeacher/:id', component: TeacherFormComponent}] },
{ path: 'login', component: LoginComponent}
];
运行程序,测试发现其他都正常,但是就出现了我上面提到的问题,即如果你从教师列表的第2页点击其中一条编辑进入详情页,之后点击返回回到列表页,列表就会刷新,又回到了第一页。
接下来我们就来说明如何解决这个问题。angular提供了一个自定义路由复用策略的抽象类RouteReuseStrategy,这个类里面有5个抽象方法可以重写来控制路由的复用。
- abstract shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean;
这个方法决定路由是否被复用,每次路由切换都会调用。如果这个方法返回true,那么就不会跳转页面,所以这个方法一般会写成
return future.routeConfig === curr.routeConfig;
即两个路由完全一致时就不跳转,否则就需要跳转。
- abstract shouldDetach(route: ActivatedRouteSnapshot): boolean;
这个方法在即将离开路由的时候调用,当它返回true时会调用store方法。 - abstract store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle | null): void;
这个方法会缓存即将要离开的路由下的组件,当store了一个null的值即清空之前缓存的组件。 - abstract shouldAttach(route: ActivatedRouteSnapshot): boolean;
这个方法在进入路由的时候调用,当它返回true时会调用retrieve方法。 - abstract retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null;
这个方法会恢复store方法缓存的路由下的组件。
因为我们的需求是当从教师列表页面进入详情页面返回后保持教师列表页面的状态,所以可以新建一个CustomRouteReuseStrategy类继承RouteReuseStrategy,然后按如下代码实现
import {ActivatedRouteSnapshot, DetachedRouteHandle, RouteReuseStrategy} from '@angular/router';
export class CustomRouteReuseStrategy implements RouteReuseStrategy {
storedRouteHandles = new Map<string, DetachedRouteHandle>();
from = '';
to = '';
shouldReuseRoute(from: ActivatedRouteSnapshot, to: ActivatedRouteSnapshot): boolean {
console.log(from.routeConfig, to.routeConfig);
console.log('shouldReuseRoute', from.routeConfig === to.routeConfig);
if (from.routeConfig) {
this.from = this.getPath(from);
}
if (to.routeConfig) {
this.to = this.getPath(to);
}
return from.routeConfig === to.routeConfig;
}
shouldDetach(route: ActivatedRouteSnapshot): boolean {
console.log('to: ' + this.to);
const f = this.to.indexOf('edit') > 0;
console.log('shouldDetach', f, this.from, this.to, route);
return f;
}
store(route: ActivatedRouteSnapshot, detachedTree: DetachedRouteHandle): void {
console.log('store', detachedTree);
this.storedRouteHandles.set(this.getPath(route), detachedTree);
}
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {
console.log('retrieve', this.storedRouteHandles.get(this.getPath(route)) as DetachedRouteHandle);
return this.storedRouteHandles.get(this.getPath(route)) as DetachedRouteHandle;
}
shouldAttach(route: ActivatedRouteSnapshot): boolean {
const path = this.getPath(route);
console.log('shouldAttach', this.storedRouteHandles.has(path), route);
return !!this.storedRouteHandles.get(path);
}
private getPath(route: ActivatedRouteSnapshot): string {
if (route.routeConfig !== null && route.routeConfig.path !== null) {
return route.routeConfig.path;
}
return '';
}
}
解释一下这个代码就是我在shouldDetach判断一下即将进入的路由地址是否包含edit,即是否是从列表页进入详情页,如果是就返回true,进入store方法保存列表页的组件。然后shouldAttach方法返回!!this.storedRouteHandles.get(path),即当前路由是否有缓存,如果有就调用retrieve方法,最后在retrieve方法中返回缓存的组件即恢复缓存的组件。
接着需要在app.module.ts中配置一下这个服务,添加到providers中
providers: [{ provide: NZ_I18N, useValue: zh_CN }, { provide: HTTP_INTERCEPTORS, useClass: MyInterceptor, multi: true },
{
provide: RouteReuseStrategy,
useClass: CustomRouteReuseStrategy
}],
再运行一下,测试一下从教师列表第二页进入某一条详情页后返回,发现果然返回后还是第二页,目标达成。
但是,这样还有一个问题,如果在详情页修改了其中某一项如姓名,那么返回列表页后就需要刷新当前页的数据。这是因为我上面在ngOnInit中调用了获取当前页数据的请求方法searchData。而路由被缓存后,返回时不再调用ngOnInit方法,所以数据不会被刷新。
为了解决这个问题,我们可以把searchData的方法放在进入列表页必然会触发的事件方法中。通过查阅angular的文档可以得知angular提供了路由切换时会触发的事件即路由器事件,我们可以在导航结束时调用请求数据的方法,即将teacher.component.ts中constructor方法修改如下
constructor(private teacherService: TeacherService, private router: Router) {
router.events.pipe(
filter(e => e instanceof NavigationEnd)
).subscribe(e => {
this.searchData();
});
}
并且删除ngOnInit中的searchData方法的调用。
这样再运行一下,发现修改了某一行教师数据后返回列表,列表就能每次都刷新数据了。
代码依然可以参考https://github.com/ahuadoreen/studentmanager-cli。