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 訂閱物件

Observablesubscribe後會得到的東西,大部份用來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);
  }

}

系列文章

RxJs教學系列文章