# Output() 软件设计中有一个重要的原则是:高内聚、低耦合。那么什么是高内聚低耦合呢?简单来说就是:能不求人时,尽量别求人。 我们以Login组件为例,在其中注入了Index组件。这就意味着若要成功的运行Login组件,当前模块必须提供一个Index组件,也就是Login组件与Index组件是绑定在一起的。我们把这种Login组件与Index组件的绑定关系称为**耦合**。而这种关系越多,就说明耦合度越高;这种关系越少,说明耦合度越低。 在父子组件这层关系中,父组件是离不开子组件的,而子组件完全的可以脱离父组件。这从我们的开发顺序上便能够完全体现出来:我们在开发登录组件时并没有父组件Index,但Login组件同样被运行了起来;而在开发父组件Index时,就必须有这个Login组件。 父组件就像是一辆汽车,它是离不开发动机的。而子组件就像是发动机,没有汽车发动机同样运转。用户购买汽车的同时,必然要连同发动机一起卖给客户;但如果用户只想购买一个发动机,你却要打包将汽车一起销售给客户就显得不合理了。 而当前Login组件依赖于Index组件的情况,便属于这种买发动机捆绑销售汽车的不合理。 ## 解绑 现实生活中的汽车与发动机是靠规定的连接规则连在一起的,我们把这个规则统称为**接口**。正是有了这些接口的存在,我们现实生活中的汽车实现了同一车型可以搭载不同排量,甚至是燃烧不同油品的发动机。 ![image-20210305142211529](https://img.kancloud.cn/a2/1a/a21a0234b95398c436abe4b7c0501d93_1510x818.png) 在Angular中同样可以为组件定义接口,该接口规定好子组件向外发送的数据格式,父组件再去通过一定的方式接收该数据。从而达到子组件与父组件解绑的目的同,解绑后父组件仍然依赖于子组件,但子组件已经不再依赖于父组件了。 ## Output() 我们可以在Login组件中以`@Output()`定义相关属性,继而通过该属性将登录成功的消息发送给父组件。首先,我们还原Login组件中相关的历史方法。 ```typescript +++ b/first-app/src/app/login/login.component.ts @@ -1,6 +1,5 @@ import {Component, OnInit} from '@angular/core'; import {HttpClient, HttpHeaders} from '@angular/common/http'; -import {IndexComponent} from '../index/index.component'; @Component({ selector: 'app-login', @@ -13,8 +12,7 @@ export class LoginComponent implements OnInit { password: string }; - constructor(private httpClient: HttpClient, - private indexComponent: IndexComponent) { + constructor(private httpClient: HttpClient) { } ngOnInit(): void { @@ -33,7 +31,7 @@ export class LoginComponent implements OnInit { .get( 'http://angular.api.codedemo.club:81/teacher/login', {headers: httpHeaders}) - .subscribe(teacher => this.indexComponent.login = true, + .subscribe(teacher => console.log('success'), error => console.log('发生错误, 登录失败', error)); } } ``` 然后增加用于通知父组件登录成功的属性,该属性的类型为`EventEmmiter`,即:事件弹射器,弹射意为由下向上发射,在发射过程中可以发射多个,也可以发射一个,当然也可以不发射。以当前登录为例,我们在每次登录成功时发射一次数据,该数据将被所以观察它的**人**的获取到。 这有点像历史上节日里的烟花。烟花在点燃后,将一个个彩蛋弹射升空,所有欣赏该烟花的人都会目睹它绽放时的风采。 ```typescript +++ b/first-app/src/app/login/login.component.ts @@ -1,4 +1,4 @@ -import {Component, OnInit} from '@angular/core'; +import {Component, EventEmitter, OnInit} from '@angular/core'; import {HttpClient, HttpHeaders} from '@angular/common/http'; @Component({ @@ -12,6 +12,8 @@ export class LoginComponent implements OnInit { password: string }; + beLogin = new EventEmitter<void>(); + constructor(private httpClient: HttpClient) { } ``` 需要注意的是当前的环境中存在多个`EventEmitter`类型,而我们在此需要的位于`'@angular/core'`中的`EventEmitter`。 最后我们加入`@Output()`注解,以表明该属性用于向父组件弹射数据。 ```typescript +++ b/first-app/src/app/login/login.component.ts @@ -1,4 +1,4 @@ -import {Component, EventEmitter, OnInit} from '@angular/core'; +import {Component, EventEmitter, OnInit, Output} from '@angular/core'; import {HttpClient, HttpHeaders} from '@angular/common/http'; @Component({ @@ -12,6 +12,7 @@ export class LoginComponent implements OnInit { password: string }; + @Output() beLogin = new EventEmitter<boolean>(); constructor(private httpClient: HttpClient) { ``` ## 父组件获取数据 其实我们早早地便学会了在父组中如何获取子组件弹射的数据了,比如我们在获取表单点击事件时使用的`<form (ngSubmit)="onSubmit()">`。在Index组件中可以如下获取Login组件弹射出的数据: ```html +++ b/first-app/src/app/index/index.component.html @@ -1,2 +1,2 @@ <app-root *ngIf="login"></app-root> -<app-login *ngIf="!login" ></app-login> +<app-login *ngIf="!login" (beLogin)="onLogin()" ></app-login> ``` 没错,该方法与前面我们写过的`(ngSubmit)="onSubmit()">`写法完全一致。 接下来结合C层的相关方法,便可以在子组件向上弹射数据时执行相关的代码: ```typescript +++ b/first-app/src/app/index/index.component.ts @@ -15,4 +15,7 @@ export class IndexComponent implements OnInit { ngOnInit(): void { } + onLogin(): void { + console.log(new Date().toTimeString(), '子组件进行了空数据弹射'); + } } ``` ### 弹射测试 为了更清楚的弄清楚数据弹射的过程,我们在Login组件的`ngOnInit()`方法(该方法中的代码会在组件初始化完毕后被自动执行1次)中增加如下测试代码: ```typescript +++ b/first-app/src/app/login/login.component.ts @@ -19,6 +19,8 @@ export class LoginComponent implements OnInit { } ngOnInit(): void { + // 每1秒钟向上弹出一个空数据 + setInterval(() => this.beLogin.emit(), 1000); } onSubmit(): void { ``` ![image-20210305150646626](https://img.kancloud.cn/46/e6/46e6d277f6d216a555c1162b179c9703_1758x342.png) ### 弹射非空数据 既然是数据弹射,必然可以弹射非空值。这取决于我们为弹射器定义的**泛型**。所谓泛型,就说类型比较宽泛,我们指定它是什么它就是什么,同时也只能是什么。 在此我们将请求登录时获取到的教师信息作为数据弹射时泛型的类型: ```typescript +++ b/first-app/src/app/login/login.component.ts @@ -13,7 +13,7 @@ export class LoginComponent implements OnInit { }; @Output() - beLogin = new EventEmitter<void>(); + beLogin = new EventEmitter<{ username: string, name: string, email: string, sex: boolean }>(); constructor(private httpClient: HttpClient) { } ``` 然后在模拟数据弹射时,弹射出一个教师: ```typescript +++ b/first-app/src/app/login/login.component.ts @@ -20,7 +20,12 @@ export class LoginComponent implements OnInit { ngOnInit(): void { // 每1秒钟向上弹出一个空数据 - setInterval(() => this.beLogin.emit(), 1000); + setInterval(() => this.beLogin.emit({ + username: 'zhangsan', + name: '张三', + email: 'zhangsan@yunzhiclub.com', + sex: true + }), 1000); } onSubmit(): void { ``` 在父组件中,接收该弹射值: ```typescript +++ b/first-app/src/app/index/index.component.html @@ -1,2 +1,2 @@ <app-root *ngIf="login"></app-root> -<app-login *ngIf="!login" (beLogin)="onLogin()" ></app-login> +<app-login *ngIf="!login" (beLogin)="onLogin($event)" ></app-login> ``` 👀 **注意**:在接收弹射值时,使用`$event`关键字。 在父组件的C层中接收该值,并进行打印。 ```typescript +++ b/first-app/src/app/index/index.component.ts @@ -15,7 +15,7 @@ export class IndexComponent implements OnInit { ngOnInit(): void { } - onLogin(): void { - console.log(new Date().toTimeString(), '子组件进行了空数据弹射'); + onLogin(teacher: { username: string, name: string, email: string, sex: boolean }): void { + console.log(new Date().toTimeString(), '子组件进行了数据弹射', teacher); } } ``` ![image-20210305154019725](https://img.kancloud.cn/7e/ba/7ebaa59752e88216daccea86f9a7d5d7_1748x352.png) ## 完成功能 让我们去除测试的代码,完成登录组件向父组件弹出登录教师的功能: ```typescript +++ b/first-app/src/app/login/login.component.ts @@ -19,13 +19,6 @@ export class LoginComponent implements OnInit { } ngOnInit(): void { - // 每1秒钟向上弹出一个空数据 - setInterval(() => this.beLogin.emit({ - username: 'zhangsan', - name: '张三', - email: 'zhangsan@yunzhiclub.com', - sex: true - }), 1000); } onSubmit(): void { @@ -38,10 +31,10 @@ export class LoginComponent implements OnInit { httpHeaders = httpHeaders.append('Authorization', 'Basic ' + authToken); this.httpClient - .get( + .get<any>( 'http://angular.api.codedemo.club:81/teacher/login', {headers: httpHeaders}) - .subscribe(teacher => console.log('success'), + .subscribe(teacher => this.beLogin.emit(teacher), error => console.log('发生错误, 登录失败', error)); } } ``` 父组件: ```typescript +++ b/first-app/src/app/index/index.component.ts @@ -17,5 +17,6 @@ export class IndexComponent implements OnInit { onLogin(teacher: { username: string, name: string, email: string, sex: boolean }): void { console.log(new Date().toTimeString(), '子组件进行了数据弹射', teacher); + this.login = true; } } ``` 🙂 测试成功! ![image-20210305121257833](https://img.kancloud.cn/0a/7b/0a7b7f1208bab18e46c08a1e5692296d_1416x542.png) 👇 ![image-20210305121333839](https://img.kancloud.cn/0e/a2/0ea2f865f7fc06074f606dc60eb72118_1520x514.png) 👇 ![image-20210305154501621](https://img.kancloud.cn/f3/bc/f3bc0799f5726cd2ea7bdc10b1d80beb_1768x406.png) 最后,引入`RouterTestingModule`以消除控制台关于`router-outlet`的错误: ```typescript +++ b/first-app/src/app/index/index.component.spec.ts @@ -5,6 +5,7 @@ import {AppComponent} from '../app.component'; import {LoginComponent} from '../login/login.component'; import {HttpClientModule} from '@angular/common/http'; import {FormsModule} from '@angular/forms'; +import {RouterTestingModule} from '@angular/router/testing'; describe('IndexComponent', () => { let component: IndexComponent; @@ -13,7 +14,7 @@ describe('IndexComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [IndexComponent, AppComponent, LoginComponent], - imports: [HttpClientModule, FormsModule] + imports: [HttpClientModule, FormsModule, RouterTestingModule] }) .compileComponents(); }); ``` ## 作业 自学TypeScript关于[类](https://www.tslang.cn/docs/handbook/classes.html)的一节。建立教师`Teacher`类,并使用该类替换本节中`{ username: string, name: string, email: string, sex: boolean }`相关的代码。 | 名称 | 地址 | 备注 | | --------------------------- | ------------------------------------------------------------ | ---- | | @Output()把数据发送给父组件 | [https://angular.cn/guide/inputs-outputs#output](https://angular.cn/guide/inputs-outputs#output) | | | EventEmitter | [https://angular.cn/api/core/EventEmitter](https://angular.cn/api/core/EventEmitter) | | | 高内聚低耦合 | [https://baike.baidu.com/item/%E9%AB%98%E5%86%85%E8%81%9A%E4%BD%8E%E8%80%A6%E5%90%88](https://baike.baidu.com/item/%E9%AB%98%E5%86%85%E8%81%9A%E4%BD%8E%E8%80%A6%E5%90%88) | | | setInterval | [https://www.runoob.com/jsref/met-win-setinterval.html](https://www.runoob.com/jsref/met-win-setinterval.html) | | | Date | [https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Date](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Date) | | | 本节源码 | [https://github.com/mengyunzhi/angular11-guild/archive/step3.5.zip](https://github.com/mengyunzhi/angular11-guild/archive/step3.5.zip) | |