RxJs 基本名詞解釋
Observable 可觀察的物件
RxJs的基本單位,用以代表任意時間觸發後會取得的值
須訂閱(.subscribe()
),才會真正執行其功能,否則不會動作
在Angular來說,就是常見的this.http
,他就是一個Observable
this.http.get('<YOUR_DOMAIN>/api/user');
若無訂閱(.subscribe()
),在程式執行過程裡,不會有任何反應
executeObservableWithoutSubscribe() {
const name = '小明';
const observable = this.http.get('<YOUR_DOMAIN>/api/user'); // 不會真正打api去取得資料!因沒有`subscribe()`
console.log(name); // 小明
console.log(observable); // 得到RxJs的物件
}
Observer 觀察者物件
Observable
訂閱(.subscribe()
)後,用來處理各種情境(即next
, error
, complete
)的最後一步動作
就是一個單純的內含next
, error
, complete
的object
即subscribe(OBSERVER)
傳入的參數
{
next: (value: any) => void,
error: (error: Error) => void,
complete: (value: any) =>void
}
由Observable
執行過程中遇到的狀況決定要Call 哪個function
this.http.get('<YOUR_DOMAIN>/api/user').subscribe({
next: result => { /* do whatever you want */ },
error: error =>{ /* do whatever you want */ },
complete: result => { /* do whatever you want */ }
});
Subscription 訂閱物件
Observable
在subscribe
後會得到的東西,大部份用來unsubscribe()
以Angular為例,一個可能的應用情境如下
@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
this.http.get<user[]>('<YOUR_DOMAIN>/api/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()
)、亦可訂閱(subscribe()
)取得經流水線運算後的結果
就像是一個eventEmitter,除了可以發送event外,本身又可以Listen事件
在RxJs中,我們習慣用$結尾命名以代表他是一個Subject
,也避免變數名太過冗長
如
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
@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
@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);
}
}