angular8教程(6)-路由复用策略

上一篇的教程中,我们讲述了如何创建一个简单的列表,满足增删改查的功能,其中改的功能是直接在列表中修改。这种情况是基于要展示的对象字段较少,可以直接完全在列表中展示出来的情况。但是实际的项目中,往往会遇到列表中不完全展示所有字段,而是在点击某一行时跳转到编辑页面展示并且在编辑页面进行修改。这时对于单页应用,我们就会遇到这样的问题,由于单页应用默认路由跳转的时候都会重新加载该路由页面,即刷新页面。但是当我们从列表页选择某一行跳转到详情页然后返回列表页的时候不想刷新怎么办呢?例如用户浏览到第5页的时候进入其中一条详情页后返回列表页,这时候如果刷新了页面,那么就会回到第一页,这对用户来说非常不友好。这个问题该怎么解决呢?在angular框架中就提供了解决方案,就是路由复用策略——RouteReuseStrategy。
我们先按照上一篇教程的步骤构建一个教师的列表页面和一个新增和修改教师共用的页面。

  1. 在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) {}
}
  1. 利用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;
  }
}
  1. 在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>
  1. 修改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]]
    });
  }
}
  1. 最后把列表页面,新增教师和修改教师的路由添加到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个抽象方法可以重写来控制路由的复用。

  1. abstract shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean;
    这个方法决定路由是否被复用,每次路由切换都会调用。如果这个方法返回true,那么就不会跳转页面,所以这个方法一般会写成
return future.routeConfig === curr.routeConfig;

即两个路由完全一致时就不跳转,否则就需要跳转。

  1. abstract shouldDetach(route: ActivatedRouteSnapshot): boolean;
    这个方法在即将离开路由的时候调用,当它返回true时会调用store方法。
  2. abstract store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle | null): void;
    这个方法会缓存即将要离开的路由下的组件,当store了一个null的值即清空之前缓存的组件。
  3. abstract shouldAttach(route: ActivatedRouteSnapshot): boolean;
    这个方法在进入路由的时候调用,当它返回true时会调用retrieve方法。
  4. 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345