iT邦幫忙

2021 iThome 鐵人賽

DAY 26
0
Modern Web

做一個面試官無法拒絕的sideproject,當一個全能的前端系列 第 26

DAY26 - 展現成果,建立 firestore 動態與複雜的查詢

https://ithelp.ithome.com.tw/upload/images/20211011/20120107y2NxOjJwRo.png

建立動態查詢

還記得之幾篇一開始如何使用條件查詢 firestore 的資料嗎?

this.firestore.collection("fruits",ref=>ref
.where("price", ">=", 200)
.orderBy("price", "asc"))

這樣的查詢翻譯成白話文就是,我要查詢水果,價格要大於等於200,且且結果要升冪排列。但是這樣有一個缺點,有沒有注意到200這個數字是寫死的,如果想要改成低於300,且結果降冪排列的時候,就要重新寫一個 function ,並且是寫死的,無法動態查詢。

因此官方文件給了一個動態查詢的例子

const size$ = new Subject<string>();
const queryObservable = size$.pipe(
  switchMap(size => 
    afs.collection('items', ref => ref.where('size', '==', size)).valueChanges()
  )
);

// subscribe to changes
queryObservable.subscribe(queriedItems => {
  console.log(queriedItems);  
});

// trigger the query
size$.next('large');

// re-trigger the query!!!
size$.next('small');

說起來複雜不複雜,簡單也不簡單,如果要看懂上面的程式碼,就必須對 rxjs 有一定的了解,如果對 rxjs 了解的,就大約可以了解上面的行為流程

  1. 將每一個查詢的參數都包裝成 Subject
  2. 如果參數改變的話,就使用 subject的 next 去觸發新的資料流
  3. 而訂閱參數的可被觀察對象在收到新的資料流後,使用 switchMap 轉換資料流,轉換成查詢 firestore 的資料流,並且以最新的參數作為變數去查詢。

如此一來,就完成動態的查詢方法。

再回到 side project 也是使用同樣的原理來做動態查詢

頁面

頁面照樣利用套件的元件,所做的只是將資料綁定上去

<div class="container-fluid">
  <div class="row">
    <div class="col col12">
      <nb-tabset fullWidth (changeTab)="changeMode($event)">
        <nb-tab tabTitle="看看自己"> </nb-tab>
        <nb-tab tabTitle="看看大家">
          <div class="col col-12">
            <nb-select
              placeholder="選擇挑戰者"
              (selectedChange)="changeUser($event)"
              [(selected)]="selectedUserId"
            >
              <nb-option
                *ngFor="let user of userList$ | async"
                [value]="user.userId"
                >{{ user.name }}</nb-option
              >
            </nb-select>
            <input
              nbInput
              placeholder="選擇日期"
              [nbDatepicker]="dateTimePicker"
              [(ngModel)]="selectedDate"
              (ngModelChange)="changeDate()"
            />
            <nb-datepicker #dateTimePicker format="yyyy-MM-dd"></nb-datepicker>
            <button nbButton hero status="danger" (click)="clearFilter()">清除</button>
          </div>
        </nb-tab>
      </nb-tabset>
    </div>
  </div>
  <div class="row justify-content-start">
    <div
      class="col col-md-4 col-12"
      *ngFor="let checkin of checkinList$ | async"
    >
      <challenge90days-checkin-card
      [checkin]="checkin"
      ></challenge90days-checkin-card>
    </div>
  </div>
</div>

樣式

無,淋漓盡致地使用,一個自訂樣式都不寫。

邏輯

import { Component, OnInit } from '@angular/core';
import {
  AngularFirestore,
  AngularFirestoreCollection,
} from '@angular/fire/firestore';
import { Observable, Subject } from 'rxjs';
import { UserService } from '../../../../services/user.service';
import {
  debounceTime,
  distinctUntilChanged,
  switchMap,
} from 'rxjs/operators';
import { Checkin, UserInfo } from '@challenge90days/api-interfaces';

import { ActivatedRoute } from '@angular/router';
import { DateService } from '../../../../services/date.service';

@Component({
  selector: 'challenge90days-myself',
  templateUrl: './list.component.html',
  styleUrls: ['./list.component.scss'],
})
export class ListComponent implements OnInit {
  userId: string;
  userCollection: AngularFirestoreCollection<any>;
  userList$: Observable<UserInfo[]>;
  checkinList$: Observable<Checkin[]>;
  checkinListQuery$ = new Subject<unknown>();
  selectedDate: Date;
  selectedUserId: string;
  mode = false;
  constructor(
    private firestore: AngularFirestore,
    private userService: UserService,
    private dateService: DateService,
    private activatedRoute: ActivatedRoute
  ) {
    this.userId = this.userService.userId$.value;
  }

  ngOnInit(): void {
    console.log(this.activatedRoute.snapshot.params);
    this.getUserId();
    this.getUserList();
  }

  getUserId(): void {
    this.userService.userId$.subscribe((userId) => {
      console.log(userId);
      this.getCheckinListData();
    });
  }

  getUserList(): void {
    this.userList$ = this.firestore.collection<UserInfo>('user').valueChanges();
  }

  getCheckinListData(): void {
    this.checkinList$ = this.checkinListQuery$.pipe(
      debounceTime(300),
      distinctUntilChanged(),
      switchMap(() =>
        this.firestore
          .collection<Checkin>('checkin', (ref) => {
            if (this.mode) {
              let finalQuery = ref
                .where('type', '==', 1)
                .orderBy('time', 'desc')
              if (this.selectedUserId) {
                console.log(this.selectedUserId);
                finalQuery = finalQuery.where(
                  'userId',
                  '==',
                  this.selectedUserId
                );
              }
              if (this.selectedDate) {
                const { startOfDay, endOfDay } = this.dateService.getDayRange(
                  this.selectedDate
                );
                finalQuery = finalQuery
                  .where('time', '>', startOfDay)
                  .where('time', '<', endOfDay);
              }
              return finalQuery;
            } else {
              return ref
                .where('userId', '==', this.userId)
                .where('type', '==', 1)
                .limit(65)
                .orderBy('time', 'desc');
            }
          })
          .valueChanges()
      )
    );
  }

  changeMode(tab: any): void {
    this.mode = tab.tabTitle === '看看大家';
    this.checkinListQuery$.next(this.mode);
  }

  changeUser(userId: string) {
    this.checkinListQuery$.next(userId);
  }
  changeDate() {
    this.checkinListQuery$.next(this.selectedDate);
  }

  clearFilter() {
    this.selectedDate = null;
    this.selectedUserId = null;
    this.checkinListQuery$.next();
  }
}

getCheckinListData ,就是使用同樣的原理去查詢,只不過條件比較多一點,不是只有單一條件,所以看起來比較複雜,因為查詢的條件有:

  • 查詢自己或查詢他人
  • 查詢的日期
  • 查詢他人的特定使用者

有三個查詢條件綜合查詢起來,所以看起來會比較頭昏眼花一點,不過只要搞懂 rxjs 的資料流的概念之後,就會一點也不複雜囉!


上一篇
DAY25 - 展現成果,建立成果頁面
下一篇
DAY27 - line message API 計費魔鬼細節與使用心得
系列文
做一個面試官無法拒絕的sideproject,當一個全能的前端30

尚未有邦友留言

立即登入留言