# Output()
軟件設計中有一個重要的原則是:高內聚、低耦合。那么什么是高內聚低耦合呢?簡單來說就是:能不求人時,盡量別求人。
我們以Login組件為例,在其中注入了Index組件。這就意味著若要成功的運行Login組件,當前模塊必須提供一個Index組件,也就是Login組件與Index組件是綁定在一起的。我們把這種Login組件與Index組件的綁定關系稱為**耦合**。而這種關系越多,就說明耦合度越高;這種關系越少,說明耦合度越低。
在父子組件這層關系中,父組件是離不開子組件的,而子組件完全的可以脫離父組件。這從我們的開發順序上便能夠完全體現出來:我們在開發登錄組件時并沒有父組件Index,但Login組件同樣被運行了起來;而在開發父組件Index時,就必須有這個Login組件。
父組件就像是一輛汽車,它是離不開發動機的。而子組件就像是發動機,沒有汽車發動機同樣運轉。用戶購買汽車的同時,必然要連同發動機一起賣給客戶;但如果用戶只想購買一個發動機,你卻要打包將汽車一起銷售給客戶就顯得不合理了。
而當前Login組件依賴于Index組件的情況,便屬于這種買發動機捆綁銷售汽車的不合理。
## 解綁
現實生活中的汽車與發動機是靠規定的連接規則連在一起的,我們把這個規則統稱為**接口**。正是有了這些接口的存在,我們現實生活中的汽車實現了同一車型可以搭載不同排量,甚至是燃燒不同油品的發動機。

在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 {
```

### 彈射非空數據
既然是數據彈射,必然可以彈射非空值。這取決于我們為彈射器定義的**泛型**。所謂泛型,就說類型比較寬泛,我們指定它是什么它就是什么,同時也只能是什么。
在此我們將請求登錄時獲取到的教師信息作為數據彈射時泛型的類型:
```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);
}
}
```

## 完成功能
讓我們去除測試的代碼,完成登錄組件向父組件彈出登錄教師的功能:
```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;
}
}
```
?? 測試成功!

??

??

最后,引入`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) | |
- 序言
- 第一章 Hello World
- 1.1 環境安裝
- 1.2 Hello Angular
- 1.3 Hello World!
- 第二章 教師管理
- 2.1 教師列表
- 2.1.1 初始化原型
- 2.1.2 組件生命周期之初始化
- 2.1.3 ngFor
- 2.1.4 ngIf、ngTemplate
- 2.1.5 引用 Bootstrap
- 2.2 請求后臺數據
- 2.2.1 HttpClient
- 2.2.2 請求數據
- 2.2.3 模塊與依賴注入
- 2.2.4 異步與回調函數
- 2.2.5 集成測試
- 2.2.6 本章小節
- 2.3 新增教師
- 2.3.1 組件初始化
- 2.3.2 [(ngModel)]
- 2.3.3 對接后臺
- 2.3.4 路由
- 2.4 編輯教師
- 2.4.1 組件初始化
- 2.4.2 獲取路由參數
- 2.4.3 插值與模板表達式
- 2.4.4 初識泛型
- 2.4.5 更新教師
- 2.4.6 測試中的路由
- 2.5 刪除教師
- 2.6 收尾工作
- 2.6.1 RouterLink
- 2.6.2 fontawesome圖標庫
- 2.6.3 firefox
- 2.7 總結
- 第三章 用戶登錄
- 3.1 初識單元測試
- 3.2 http概述
- 3.3 Basic access authentication
- 3.4 著陸組件
- 3.5 @Output
- 3.6 TypeScript 類
- 3.7 瀏覽器緩存
- 3.8 總結
- 第四章 個人中心
- 4.1 原型
- 4.2 管道
- 4.3 對接后臺
- 4.4 x-auth-token認證
- 4.5 攔截器
- 4.6 小結
- 第五章 系統菜單
- 5.1 延遲及測試
- 5.2 手動創建組件
- 5.3 隱藏測試信息
- 5.4 規劃路由
- 5.5 定義菜單
- 5.6 注銷
- 5.7 小結
- 第六章 班級管理
- 6.1 新增班級
- 6.1.1 組件初始化
- 6.1.2 MockApi 新建班級
- 6.1.3 ApiInterceptor
- 6.1.4 數據驗證
- 6.1.5 教師選擇列表
- 6.1.6 MockApi 教師列表
- 6.1.7 代碼重構
- 6.1.8 小結
- 6.2 教師列表組件
- 6.2.1 初始化
- 6.2.2 響應式表單
- 6.2.3 getTestScheduler()
- 6.2.4 應用組件
- 6.2.5 小結
- 6.3 班級列表
- 6.3.1 原型設計
- 6.3.2 初始化分頁
- 6.3.3 MockApi
- 6.3.4 靜態分頁
- 6.3.5 動態分頁
- 6.3.6 @Input()
- 6.4 編輯班級
- 6.4.1 測試模塊
- 6.4.2 響應式表單驗證
- 6.4.3 @Input()
- 6.4.4 FormGroup
- 6.4.5 自定義FormControl
- 6.4.6 代碼重構
- 6.4.7 小結
- 6.5 刪除班級
- 6.6 集成測試
- 6.6.1 惰性加載
- 6.6.2 API攔截器
- 6.6.3 路由與跳轉
- 6.6.4 ngStyle
- 6.7 初識Service
- 6.7.1 catchError
- 6.7.2 單例服務
- 6.7.3 單元測試
- 6.8 小結
- 第七章 學生管理
- 7.1 班級列表組件
- 7.2 新增學生
- 7.2.1 exports
- 7.2.2 自定義驗證器
- 7.2.3 異步驗證器
- 7.2.4 再識DI
- 7.2.5 屬性型指令
- 7.2.6 完成功能
- 7.2.7 小結
- 7.3 單元測試進階
- 7.4 學生列表
- 7.4.1 JSON對象與對象
- 7.4.2 單元測試
- 7.4.3 分頁模塊
- 7.4.4 子組件測試
- 7.4.5 重構分頁
- 7.5 刪除學生
- 7.5.1 第三方dialog
- 7.5.2 批量刪除
- 7.5.3 面向對象
- 7.6 集成測試
- 7.7 編輯學生
- 7.7.1 初始化
- 7.7.2 自定義provider
- 7.7.3 更新學生
- 7.7.4 集成測試
- 7.7.5 可訂閱的路由參數
- 7.7.6 小結
- 7.8 總結
- 第八章 其它
- 8.1 打包構建
- 8.2 發布部署
- 第九章 總結