RxJs 基本名詞解釋

Observable 可觀察的物件

RxJs的基本單位,用以代表任意時間觸發後會取得的值
須訂閱(.subscribe()),才會真正執行其功能,否則不會動作

在Angular來說,就是常見的this.http,他就是一個Observable

1
this.http.get('api/<YOUR_DOMAIN>/user');

若無訂閱(.subscribe()),在程式執行過程裡,不會有任何反應

1
2
3
4
5
6
executeObservableWithoutSubscribe() {
const name = '小明';
const observable = this.http.get('api/<YOUR_DOMAIN>/user'); // 不會真正打api去取得資料!因沒有`subscribe()`
console.log(name); // 小明
console.log(observable); // 得到RxJs的物件
}

Observer 觀察者物件

Observable訂閱(.subscribe())後,用來處理各種情境(即next, error, complete)的最後一步動作
就是一個單純的內含next, error, complete的object

subscribe(OBSERVER)傳入的參數

1
2
3
4
5
{
next: (value: any) => void,
error: (error: Error) => void,
complete: (value: any) =>void
}

Observable執行過程中遇到的狀況決定要Call 哪個function

1
2
3
4
5
this.http.get('api/<YOUR_DOMAIN>/user').subscribe({
next: result => { /* do whatever you want */ },
error: error =>{ /* do whatever you want */ },
complete: result => { /* do whatever you want */ }
});

Subscription 訂閱物件

Observablesubscribe後會得到的東西,大部份用來unsubscribe()
以Angular為例,一個可能的應用情境如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component({
selector: 'app-users',
templateUrl: './users.component.html',
styleUrls: ['./users.component.scss']
})
export class UsersComponent implements OnInit, OnDestroy {

usersSubscription: Subscription;
users: User[] = [];
constructor(private readonly http: HttpClient) { }

ngOnInit(): void {
this.usersSubscription = this.http.get('api/<DOMAIN>/user')
.subscribe(users => this.users = users);
}

ngOnDestroy(): void {
// 在`Component`消滅後,取消訂閱
this.usersSubscription.unsubscribe();
}

}

這也是為什麼Angular有提供| async管道
就是為了減少在Component裡寫各種unsubscribe()
由框架承擔取消訂閱的冗餘程式碼,減輕開發者的負擔

Operators 運算子

即Pipe裡所接的任何運算函式,如filter, map, mergeMap, take, groupBy, toArray …等
就是工廠流水線裡的「工人」,此處傳入函式決定要執行的邏輯

如下方的filter, map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
this.http.get<user[]>('api/<YOUR_DOMAIN>/user').pipe(
filter(user => user.length !== 0), // 當無資料則不處理
map(users => { // 如收到資料皆為民國年,須於前端自行轉西元年
for (user of users) {
user.birth = this.transToAd(user.birth); // transToAd()為轉西元年函式,省略相關邏輯
}
return users;
}),
).subscribe({
next: users => this.users = users, // 成功取得,賦值
error: error => {
this.users = []; // 將目前users清空
this.notify.error(`無法取得USER! statusCode = ${error.statusCode}`); // 畫面跳通知訊息
}
});

Subject 主體物件

是一個複合體,本身同時可以用來發出資料(next())、亦可訂閱(subscribr())取得經流水線運算後的結果
就像是一個eventEmitter,除了可以發送event外,本身又可以Listen事件

在RxJs中,我們習慣用$結尾命名以代表他是一個Subject,也避免變數名太過冗長

1
2
3
4
const user$ = new BehaviorSubject<any>(undefined); 

// 不會用下列命名方式,因太長反而不易閱讀
const userSubject = new BehaviorSubject<any>(undefined); // don't name like this!

Angular的重點功能!
就是Subject讓Angular可以很輕鬆的跨越多層Component傳遞資料
並使每個用到的Component得到資料同時保有自己的流水線處理邏輯

其進階應用就是NgRx(相當於Vue.Js裡的vuex、React.Js的redux)

筆者認為在Angular裡,不像另2大框架,專案稍大一些就得用上store
只是NgRx仿照了其他框架store架構做出了一樣的東西給Angular使用

Angular對NgRx的依賴相當的低!

因Angular天生的依賴注入架構
只要增一Service就可以很輕鬆的成為store

透過Service + RxJs Subject即可很簡單的做出全域store
建在SharedModule export出來就是全域使用的store
建在各自Module,就是該模組自用小store,可明確規範出他的使用範圍
整體使用方式還比NgRx簡單多了!

越少的三方套件,代表著越輕鬆的 migration!

ps: Redux 作者也寫了一篇You Might Not Need Redux來說明使用 Redux 是一種tradeoff(權衡之下的取捨,帶來了好處,也增加了複雜度)。故在使用 NgRx 之前,先想清楚的業務場景是否真的需要。以 Angular 來說,自身的 Service 幾乎足以負擔常見的需求。若評估後仍覺得需要,再安裝 NgRx 輔助專案

一個簡單的Subject例子

做為store用的Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Injectable({
providedIn: 'root'
})
export class UserStoreService {

private _user = {name: '小明', birth: '1990/06/25'};

user$ = new BehaviorSubject<any | undefined>(undefined); // 首次初始化時傳遞undefined,便於各Component判斷
constructor() { }

public get user() {
return this._user;
}
public set user(value) {
this._user = value;
}

}

Component使用
為避免範例太過冗長,把訂閱(subscribe())、發送(next())寫在同一個Component

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@Component({
selector: 'app-users',
templateUrl: './users.component.html',
styleUrls: ['./users.component.scss']
})
export class UsersComponent implements OnInit {

username = '未登入';
constructor(private readonly userStoreService: UserStoreService) { }

/**
* subscribe範例
*/
ngOnInit() {
this.userStoreService.user$.pipe(
filter(u => user !== undefined), // 收到undefined,表示已登出或首次初始化,不必處理。html畫面則使用「預設的username='未登入'」之文字
).subscribe(user => {
// Component 初始化後,取得user,做相應的賦值、邏輯處理
// 如顯示登入姓名、依權限調整相關選單…之類的
this.username = user.name;
});
}

/**
* next範例
* 更新基本資料
*/
updateUserInfo() {
const users: any = {name: '王大美', birth: '1989/11/17'};
// ......
// ...處理完相關檢核邏輯後...
// ......

// 如按下登出按鈕、或權限控制頁面調整相關權限後,通知其他`Component`目前最新的狀態
this.userStoreService.user$.next(users);
// 所有有`subscribe()`的`Component`皆會接收到最新的user
}
/**
* next範例
* 登出
*/
logout() {
this.username = '未登入';
// 通知其他`Component`使用者已登出,請依各自邏輯做相應畫面渲染
this.userStoreService.user$.next(undefined);
}

}

系列文章

RxJs教學系列文章