# 定义菜单 在前面的章节中我们完成了教师管理、用户登录、欢迎页、个人中心四个主体功能。本节我们新建一个菜单组件,使用户在使用我们的教务系统时更加方便。 ## 初始化 首先我们使用angular cli快速初始化菜单组件,导航在术语中常被命名为`nav`,所以我们有时也会将其称为**导航**: ```bash panjiedeMacBook-Pro:app panjie$ pwd /Users/panjie/github/mengyunzhi/angular11-guild/first-app/src/app panjiedeMacBook-Pro:app panjie$ ng g c nav CREATE src/app/nav/nav.component.css (0 bytes) CREATE src/app/nav/nav.component.html (18 bytes) CREATE src/app/nav/nav.component.spec.ts (605 bytes) CREATE src/app/nav/nav.component.ts (263 bytes) UPDATE src/app/app.module.ts (1378 bytes) ``` bootstrap中内置了导航的样式,我们为其增加**首页**、**教师**管理菜单两个菜单: ```html <nav class="navbar navbar-expand-lg navbar-dark bg-primary"> <a class="navbar-brand mr-auto mr-lg-0" href="#">软小白教务系统</a> <button class="navbar-toggler p-0 border-0" type="button" data-toggle="offcanvas"> <span class="navbar-toggler-icon"></span> </button> <div class="navbar-collapse offcanvas-collapse" id="navbarsExampleDefault"> <ul class="navbar-nav mr-auto"> <li class="nav-item active"> <a class="nav-link" href="#">首页 <span class="sr-only">(current)</span></a> </li> <li class="nav-item"> <a class="nav-link" href="#">教师管理</a> </li> <li class="nav-item"> <a class="nav-link" href="#">班级管理</a> </li> <li class="nav-item"> <a class="nav-link" href="#">学生管理</a> </li> <li class="nav-item"> <a class="nav-link" href="#">个人中心</a> </li> </ul> <form class="form-inline my-2 my-lg-0"> <button class="btn btn-outline-light my-2 my-sm-0" type="submit">注销</button> </form> </div> </nav> ``` 最终效果如下: ![image-20210315134936667](https://img.kancloud.cn/df/f9/dff9c785f93b662a34ac894ed598884b_2008x212.png) ## 加入菜单 有了菜单,让我们把它添加到其应该存在的位置上。当前项目我们希望: 1. 用户未登录显示登录组件。 2. 用户登录成功或已登录,上方显示导航组件,下方根据路由值对应显示组件。 根据以上需求,我们将导航组件添加到`Index`组件中,添加的方法与我们在`Index`组件中添加`Login`组件的过程一致: 1. 获取nav组件的selector,当前为` 'app-nav'`。 2. 将`selector`添加到`Index`组件V层对应的位置上。 ```html +++ b/first-app/src/app/index/index.component.html @@ -1,2 +1,7 @@ +<!--登录成功后,在上面显示导航--> +<app-nav *ngIf="login"></app-nav> +<!--在下方显示路由对应的具体组件--> <router-outlet *ngIf="login"></router-outlet> + +<!--未登录时,显示登录窗口--> <app-login *ngIf="!login" (beLogin)="onLogin($event)" ></app-login> ``` ### 测试 在Index对应的单元测试中加入导航组件,已防止在进行测试中出现找不到`app-nav`对应的组件错误: ```typescript +++ b/first-app/src/app/index/index.component.spec.ts @@ -6,6 +6,7 @@ import {LoginComponent} from '../login/login.component'; import {HttpClientModule} from '@angular/common/http'; import {FormsModule} from '@angular/forms'; import {RouterTestingModule} from '@angular/router/testing'; +import {NavComponent} from '../nav/nav.component'; describe('IndexComponent', () => { let component: IndexComponent; @@ -13,7 +14,7 @@ describe('IndexComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [IndexComponent, AppComponent, LoginComponent], + declarations: [IndexComponent, AppComponent, LoginComponent, NavComponent], imports: [HttpClientModule, FormsModule, RouterTestingModule] }) .compileComponents(); @@ -25,7 +26,7 @@ describe('IndexComponent', () => { fixture.detectChanges(); }); - it('should create', () => { + fit('should create', () => { expect(component).toBeTruthy(); fixture.autoDetectChanges(); }); ``` 在单元测试中输入正确的用户名、密码后完成登录,显示导航组件。测试成功。 ## 添加路由 下一步,我们为其a标签添加对应的路由: ```html +++ b/first-app/src/app/nav/nav.component.html @@ -1,5 +1,5 @@ <nav class="navbar navbar-expand-lg navbar-dark bg-primary"> - <a class="navbar-brand mr-auto mr-lg-0" href="#">软小白教务系统</a> + <a class="navbar-brand mr-auto mr-lg-0" routerLink="/">软小白教务系统</a> <button class="navbar-toggler p-0 border-0" type="button" data-toggle="offcanvas"> <span class="navbar-toggler-icon"></span> </button> @@ -7,10 +7,10 @@ <div class="navbar-collapse offcanvas-collapse" id="navbarsExampleDefault"> <ul class="navbar-nav mr-auto"> <li class="nav-item active"> - <a class="nav-link" href="#">首页 <span class="sr-only">(current)</span></a> + <a class="nav-link" routerLink="/">首页 <span class="sr-only">(current)</span></a> </li> <li class="nav-item"> - <a class="nav-link" href="#">教师管理</a> + <a class="nav-link" routerLink="teacher">教师管理</a> </li> <li class="nav-item"> <a class="nav-link" href="#">班级管理</a> @@ -19,7 +19,7 @@ <a class="nav-link" href="#">学生管理</a> </li> <li class="nav-item"> - <a class="nav-link" href="#">个人中心</a> + <a class="nav-link" routerLink="personal-center">个人中心</a> </li> </ul> <form class="form-inline my-2 my-lg-0"> ``` 如上,我们删除了模板中的`href`属性,取而代之的是`routerLink`属性。 虽然可以在`ng t`中对路由进行测试,但这并不是`ng t`所擅长的领域。在此对导航进行测试,我们使用`ng s`: ![image-20210315140604543](https://img.kancloud.cn/5f/77/5f77c921760465e913a519cf1adad1f9_2352x548.png) 点击首页、教师管理、个人中心后均生效,成功! ## 随动点亮 有几个小的问题我们需要修正下,第一问题就是对应菜单的点亮效果。点我们点击教师管理后,菜单**首页**仍然处于点亮的状态,这是一个小的BUG。 ![image-20210315140830738](https://img.kancloud.cn/53/47/53473b8906ed0ec73cbba4d588d1ea47_1352x284.png) 由于这种**随动点亮**的效果基本上每个Angular项目都需要的基本功能,所以Angular为我们友好的内置了`routerLinkActive`来实现该功能: ```html +++ b/first-app/src/app/nav/nav.component.html @@ -6,10 +6,12 @@ <div class="navbar-collapse offcanvas-collapse" id="navbarsExampleDefault"> <ul class="navbar-nav mr-auto"> - <li class="nav-item active"> + <li class="nav-item" + routerLinkActive="active" + [routerLinkActiveOptions]="{exact: true}"> <a class="nav-link" routerLink="/">首页 <span class="sr-only">(current)</span></a> </li> - <li class="nav-item"> + <li class="nav-item" routerLinkActive="active"> <a class="nav-link" routerLink="teacher">教师管理</a> </li> <li class="nav-item"> @@ -18,7 +20,7 @@ <li class="nav-item"> <a class="nav-link" href="#">学生管理</a> </li> - <li class="nav-item"> + <li class="nav-item" routerLinkActive="active"> <a class="nav-link" routerLink="personal-center">个人中心</a> </li> </ul> ``` 此时,当我们点击对应的菜单时,仅有该菜单会处于点亮的状态: ![image-20210315142027957](https://img.kancloud.cn/40/7e/407e9fdd2025acbd287731e9cea6c491_1246x254.png) 所以`routerLinkActive`的作用时:当前路由如果对应了`routeLink`的值的话,`routerLinkActive`中的字符串将添加到该元素的`class`中: ![image-20210315142245647](https://img.kancloud.cn/79/f5/79f55697c2fd9a0c45e8e63553f24b55_1432x396.png) 从而实现了当路由为`/teacher`时,**教师管理**菜单被点亮的效果。 在上述代码中我们同时增加了` [routerLinkActiveOptions]="{exact: true}">`,正如我们前面提及过的使用`[xxx]`时,`=`后面对应的是typescript语句,在这里`{exact: true}`是一个对象,该对象中有一个属性`exact`,值为`true`。表示只有路由**完全**与`routeLink`中的值相同时,才为当元素增加`active`样式。 而至于为什么要这样做,我想以下两个实验能解决你心中的疑惑: 1. 去除**首页**菜单的`[routerLinkActiveOptions]`,再次点击各个菜单,看看会发生什么。 2. 在**教师**菜单上加入` [routerLinkActiveOptions]="{exact: true}"`,然后在教师管理中点击新增按钮,打开新增教师界面,看看又会发生什么。 以上两个实验完成后,相信你已经明白了为什么我们要那么做了。 ## 组件跳转 菜单有了以后,终于像那么回事了。下面,我们进一步的完成各个组件间的跳转功能。新增教师、编辑教师后均跳回教师列表组件。这好像是我们以前给大家布置的作业,你曾完成了吗? 我们已经掌握了在V层中使用`routerLink`的方式来完成路由间的跳转,除此以外,如果我们在C层想实现路由跳转则可以引入Angular的`Router`: ```typescript +++ b/first-app/src/app/add/add.component.ts @@ -1,5 +1,6 @@ import {Component, OnInit} from '@angular/core'; import {HttpClient} from '@angular/common/http'; +import {Router} from '@angular/router'; @Component({ selector: 'app-add', @@ -14,7 +15,8 @@ export class AddComponent implements OnInit { sex: true }; - constructor(private httpClient: HttpClient) { + constructor(private httpClient: HttpClient, + private router: Router) { } ngOnInit(): void { @@ -26,6 +28,7 @@ export class AddComponent implements OnInit { .post('http://angular.api.codedemo.club:81/teacher', this.teacher) .subscribe((result) => { console.log('接收到返回数据', result); + this.router.navigate(['teacher']); }, (error) => { console.log('请求失败', error); }); ``` Router中的navigate提供了路由跳转(导航)功能,该方法接收一个`数组`,数组中的第一项为跳转的路由位置。如此以来,我们便实现了教师添加成功后跳转回教师列表的功能。 测试过程略。 教程中教师编辑组件仍在依赖于App组件,这使得我们当前无法启用Edit组件,我们接下来共同解决掉这个问题: ```typescript +++ b/first-app/src/app/edit/edit.component.ts @@ -1,7 +1,6 @@ import {Component, OnInit} from '@angular/core'; import {ActivatedRoute} from '@angular/router'; import {HttpClient} from '@angular/common/http'; -import {AppComponent} from '../app.component'; @Component({ selector: 'app-edit', @@ -18,8 +17,7 @@ export class EditComponent implements OnInit { }; constructor(private activeRoute: ActivatedRoute, - private httpClient: HttpClient, - private appComponent: AppComponent) { + private httpClient: HttpClient) { } ngOnInit(): void { @@ -43,8 +41,7 @@ export class EditComponent implements OnInit { this.httpClient.put(url, this.teacher) .subscribe(data => { console.log('更新成功', data); - // 在此调用教师列表App组个的ngOnInit方法,即可实现更新后重新刷新教师列表的功能 - this.appComponent.ngOnInit(); + }, error => console.log('更新错误', error)); } ``` 然后如同add组件一样,引入Router并完成编辑后的跳转: ```typescript +++ b/first-app/src/app/edit/edit.component.ts @@ -1,5 +1,5 @@ import {Component, OnInit} from '@angular/core'; -import {ActivatedRoute} from '@angular/router'; +import {ActivatedRoute, Router} from '@angular/router'; import {HttpClient} from '@angular/common/http'; @Component({ @@ -17,7 +17,8 @@ export class EditComponent implements OnInit { }; constructor(private activeRoute: ActivatedRoute, - private httpClient: HttpClient) { + private httpClient: HttpClient, + private router: Router) { } ngOnInit(): void { @@ -41,7 +42,7 @@ export class EditComponent implements OnInit { this.httpClient.put(url, this.teacher) .subscribe(data => { console.log('更新成功', data); - + this.router.navigate(['teacher']); }, error => console.log('更新错误', error)); } ``` 测试过程略。如此以来,我们便真正的完成了一个具有教师管理、个人中心两个小模块的小的系统了。 相信大多数的同学,早早的便完成了我们刚刚进行的路由跳转功能。这绝对是一个应该让自己骄傲的好习惯。在学习的过程中,看的懂与会做完完全全是两码事,会做与会讲又是学习的两个不同的阶段。我们绝对应该自己远离**一看就会,一做就错**的学习怪圈,而实现这一目标的有效手段便是:多上手,多练,多用代码验证。 我们应该感谢历史上的自己为我们选择了这么一门好的专业,与其它专业不同,我们的测试环境则方便又安全,同时还几乎没有任何成本,在**练习**中发生问题,随时可以按自己的意愿随时的重新来过。 ## 力求完美 在项目开发过程中,应该遵循**先完成、再完美**的思想。当下我们差不多完成了基本的功能后,让我们再简单的完美一下: ![image-20210315151119924](https://img.kancloud.cn/f6/31/f63169095216154a8df62480700f728d_1448x268.png) ![image-20210315151131699](https://img.kancloud.cn/75/a9/75a96817b2bab651b83604cda001fb17_1338x412.png) 我们发现上述两个图片所示的位置好像都应该有那么一点点小的空隙,这样在视觉上更容易对模块进行划分。 ```html +++ b/first-app/src/app/nav/nav.component.html @@ -1,4 +1,4 @@ -<nav class="navbar navbar-expand-lg navbar-dark bg-primary"> +<nav class="navbar navbar-expand-lg navbar-dark bg-primary mb-3"> <a class="navbar-brand mr-auto mr-lg-0" routerLink="/">软小白教务系统</a> <button class="navbar-toggler p-0 border-0" type="button" data-toggle="offcanvas"> <span class="navbar-toggler-icon"></span> ``` 没错,这就么一个看似不起眼的`mb-3`却起到了在视觉上起到非常良好的效果: ![image-20210315151649645](https://img.kancloud.cn/68/f7/68f743073cc33de09dc2e90d55aa5084_1764x348.png) ## ng t `ng t`的最终目的是为了保障项目完整有效性,我们在未启用`ng t`的情况下做了一些调整。在继续下一节前,还需要确认`ng t`的每个功能都是正常的,为此我们移除所有的`fit`、`fdescribe`后执行`ng t`,以确保在解决一个BUG的同时不新增其它BUG。 ![image-20210315151840950](https://img.kancloud.cn/e5/d5/e5d5dc97faee5d56c8c535d3cbc00404_1542x412.png) 最后结果为发现了一个异常,此异常对应的`describe`为`AddComponent`,对应的`it`为`should create`,异常的原因是未获取到Router的提供者: ```typescript +++ b/first-app/src/app/add/add.component.spec.ts describe('AddComponent', () => { 👈 ... it('should create', () => { 👈 expect(component).toBeTruthy(); }); }); ``` 解决的方法当然添加有一个能力提供Router的模块了,在测试过程中,我们使用Angular提供的路由测试模块做为相应的provider: ```typescript +++ b/first-app/src/app/add/add.component.spec.ts @@ -3,6 +3,7 @@ import {ComponentFixture, TestBed} from '@angular/core/testing'; import {AddComponent} from './add.component'; import {FormsModule} from '@angular/forms'; import {HttpClientModule} from '@angular/common/http'; +import {RouterTestingModule} from '@angular/router/testing'; describe('AddComponent', () => { let component: AddComponent; @@ -11,7 +12,7 @@ describe('AddComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [AddComponent], - imports: [FormsModule, HttpClientModule] + imports: [FormsModule, HttpClientModule, RouterTestingModule] }) .compileComponents(); }); ``` 再次执行,错误消息: ![image-20210315152227073](https://img.kancloud.cn/f8/d1/f8d13d0833296ca46037104df7afdc24_1036x280.png) 说明我们未对历史的功能造成影响。 ## 本节作业 我们在下节中将实现用户注销功能,其对应的后台api为: ``` GET /teacher/logout ``` 在不参考下一小节内容的情况下,尝试实现该功能。 | 名称 | 链接 | | ------------- | ------------------------------------------------------------ | | bootstrap导航 | [https://getbootstrap.com/docs/5.0/components/navs-tabs/](https://getbootstrap.com/docs/5.0/components/navs-tabs/) | | 活动路由链路 | [https://angular.cn/guide/router#active-router-links](https://angular.cn/guide/router#active-router-links) | | 指定相对路由 | [https://angular.cn/guide/router#specifying-a-relative-route](https://angular.cn/guide/router#specifying-a-relative-route) | | 本节源码 | [https://github.com/mengyunzhi/angular11-guild/archive/step5.5.zip](https://github.com/mengyunzhi/angular11-guild/archive/step5.5.zip) |