iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 28
0
Modern Web

用Angular打造完整後台系列 第 28

day28 Firebase(二)應用

簡述

本篇開始把Demo替換成firebase。

替換流程

  • WebService
  • DataService
  • 修改list中的關聯式

實作

(一) web.service.ts

首先是基本的CRUD連線方式。

export class WebService {
  constructor(private logger: LoggerService) {}

  getF<T>(afc: AngularFirestoreCollection): Observable<T[]> {
    return afc.snapshotChanges().pipe(
      map((actions: DocumentChangeAction<T>[]) => {
        return actions.map(a => {
          const data = a.payload.doc.data();
          const id = a.payload.doc.id;
          return { id, ...data };
        });
      }),
      tap(res => this.logger.print("response", res)),
      catchError(this.handleError)
    );
  }

  getO<T>(afc: AngularFirestoreCollection, id: string): Observable<T> {
    return afc
      .doc(id)
      .valueChanges()
      .pipe(
        map((action: T) => {
          return action;
        }),
        tap(res => this.logger.print("response one", res)),
        catchError(this.handleError)
      );
  }

  postF<T>(afd: AngularFirestoreDocument, data: T): Observable<T> {
    return from(afd.set(data)).pipe(
      tap(res => this.logger.print("create", res)),
      catchError(this.handleError)
    );
  }

  patchF<T>(afd: AngularFirestoreDocument, data: T): Observable<T> {
    return from(afd.update(data)).pipe(
      tap(res => this.logger.print("update", res)),
      catchError(this.handleError)
    );
  }

  delF<T>(afd: AngularFirestoreDocument): Observable<T> {
    return from(afd.delete()).pipe(
      tap(res => this.logger.print("delete", res)),
      catchError(this.handleError)
    );
  }

  private handleError(error: Response | any): Observable<any> {
    console.error(`${error.status}`);
    return of(null);
  }
}

(二) data.service.ts

export interface IFilter {
  key: string;
  val: string | number;
}

export interface ICount {
  count: number;
}

@Injectable({
  providedIn: "root"
})
export class DataService {
  constructor(
    private webService: WebService, 
    private afs: AngularFirestore
  ) {}

  connect(token: string): Observable<IData> {
    let link = {
      join: "holds",
      key: "adminId"
    };
    let query = this.setQuery([{ key: "token", val: token }]);
    return this.webService.getF(this.afs.collection("admins", query))
    .pipe(
      this.docJoinFilter(link),
      map((res: IModel[]) => {
        let errocode = 0;

        if (!res || !res.length) {
          errocode = 2;
        }
        if (!!res[0]) {
          let user = <IUser>res[0];
          if (user.status != 1) {
            errocode = 3;
          }
        }
        return this.resData(res, errocode);
      })
    );
  }

  resData(res: IModel[] | IModel, errocode: number): IData {
    return <IData>{
      res: res,
      errorcode: errocode
    };
  }

  setQuery(filters: IFilter[]): QueryFn {
    let query: QueryFn = null;
    filters.filter((filter: IFilter) => {
      switch (filter.key) {
        case "inserted_gte":
          query = query => query.where(
                              "inserted", 
                              ">", 
                              <number>filter.val
                            );
          break;
        case "inserted_lte":
          query = query => query.where(
                              "inserted", 
                              "<", 
                              <number>filter.val
                            );
          break;
        default:
          query = query => query.where(filter.key, "==", filter.val);
          break;
      }
    });
    return query;
  }

  getData(
    url: string,
    id?: number,
    filters?: IFilter[],
    pageObj?: IPage,
    link?: { join: string; key: string }
  ): Observable<IData> {
    if (!!id) {
      return this.getOne(url, id, link);
    }

    if (!!filters && !!filters.length) {
      let query = this.setQuery(filters);
      return this.getList(
                    this.afs.collection(url, query), 
                    pageObj, 
                    link
                  );
    }

    return this.getList(this.afs.collection(url), pageObj, link);
  }

  getOne(url: string, id: number, link?: { join: string; key: string })
  : Observable<IData> 
  {
    return this.webService
    .getO(this.afs.collection(url), id.toString())
    .pipe(
      map((res: IModel) => {
        if (!res) {
          res = <IModel>{};
        }
        return res;
      }),
      this.docJoinFilterOne(id, link),
      map((res: IModel) => {
        res.id = id;
        return res;
      }),
      map((res: IModel) => {
        return this.resData(res, 0);
      })
    );
  }

  getList(
    afc: AngularFirestoreCollection,
    pageObj?: IPage,
    link?: { join: string; key: string }
  ): Observable<IData> {
    return this.webService.getF(afc).pipe(
      map((res: IModel[]) => {
        if (!!pageObj) {
          pageObj.length = res.length;
          res["pageObj"] = pageObj;
          res = this.rangeData(res, pageObj);
        }
        return res;
      }),

      this.docJoin(link),

      map((res: IModel[]) => {
        res.forEach((model: IModel, index: number) => {
          model.id = +model.id;
        });
        return res;
      }),

      map((res: IModel[]) => {
        return this.resData(res, 0);
      })

    );
  }

  insertOne(url: string, obj: IModel): Observable<IData> {
    let countAfc = this.afs.collection("counters");
    let afc = this.afs.collection(url);
    const count$ = this.webService.getO(countAfc, url)
    .pipe(
      map((action: ICount) => {
        return action.count;
      }),
      take(1)
    );

    const countUpdate$ = count$.pipe(
      concatMap((id: number) => {
        return this.webService
        .patchF(countAfc.doc(url), { count: ++id })
        .pipe(
          map(() => {
            return id;
          })
        );
      })
    );

    const insert$ = countUpdate$.pipe(
      concatMap((id: number) =>
        this.webService
        .postF(afc.doc(id.toString()), obj)
        .pipe(
          map(() => {
            let obj=<IModel>{
              id:id
            }
            return this.resData(obj, 0);
          })
        )
      )
    );

    return insert$;
  }

  updateOne(url: string, obj: IModel, id: number): Observable<IData> {
    return this.webService
    .patchF(this.afs.collection(url).doc(id.toString()), obj)
    .pipe(
      map(() => {
        return this.resData(obj, 0);
      })
    );
  }

  deleteOne(url: string, id: number): Observable<IData> {
    return this.webService
    .delF(this.afs.collection(url).doc(id.toString()))
    .pipe(
      map(() => {
        return this.resData(null, 0);
      })
    );
  }

  //加上新增或是更新日期
  checkData(obj: IModel, operatorId: number, isInsert = true): IModel {
    let insert = <IModel>{
      insertBy: +operatorId,
      inserted: new Date().getTime()
    };
    let update = <IModel>{
      updateBy: +operatorId,
      updated: new Date().getTime()
    };
    if (isInsert) {
      obj = { ...obj, ...insert, ...update };
    } else {
      //update
      obj = { ...obj, ...update };
    }
    return obj;
  }

  rangeData(obj: IModel[], pageObj: IPage): IModel[] {
    let start = pageObj.pageIndex * pageObj.pageSize;
    let end = start + pageObj.pageSize;
    if (pageObj.length < end) {
      end = pageObj.length;
    }
    return obj.slice(start, end);
  }

  makeToken(account: string): string {
    return <string>Md5.hashStr(`${account}`);
  }

  /**Join */
  docJoin = 
  (link?: { join: string; key: string })=> (obs: Observable<IModel[]>)
   => obs.pipe(
      switchMap((res: IModel[]) => {
        if (!!res && !!res.length && !!link) {

          return combineLatest(
            res.map((model: IModel) =>
              this.webService
                .getO(
                  this.afs.collection<IModel>(link.join), 
                  model[link.key].toString()
                )
                .pipe(
                  map((joinObj: IModel) => {
                    let o = {};
                    let t = link.join.substring(0, link.join.length - 1);
                    o[t] = joinObj;
                    return { ...model, ...o };
                  })
                )
            )
          );

        } else {
          return of(res);
        }
      })
    );

  docJoinFilter = 
  (link?: { join: string; key: string }) => (obs: Observable<IModel[]>)
   => obs.pipe(
      switchMap((res: IModel[]) => {
        if (!!res && !!res.length && !!link) {

          return combineLatest(
            res.map((model: IModel) => {
              let query = this.setQuery(
                [{ key: link.key, val: +model.id }]
              );

              return this.webService
              .getF(this.afs.collection(link.join, query))
              .pipe(
                map((joinObj: IModel[]) => {
                  let o = {};
                  o[link.join] = joinObj;
                  return { ...model, ...o };
                })
              );
            })
          );

        } else {
          return of(res);
        }
      })
    );

  docJoinFilterOne = 
  (id: number, link?: { join: string; key: string })
  => ( obs: Observable<IModel>) 
  => obs.pipe(
      switchMap((res: IModel) => {
        if (!!res && !!link) {
          let query = this.setQuery([{ key: link.key, val: id }]);
          return this.webService
          .getF(this.afs.collection(link.join, query))
          .pipe(
            map((joinObj: IModel[]) => {
              let o = {};
              o[link.join] = joinObj;
              return { ...res, ...o };
            })
          );
        } else {
          return of(res);
        }
      })
    );
}

首先 DataService 要分幾塊說明:

  • getData() 多了一個參數:link?: { join: string; key: string }
    之前json_server的關聯式是直接合併在 filters 裡。如下例:
//購物車表關聯商品表,關鍵字為expand
setCars() {
    let filters: IFilter[] = [
      { key: "orderId", val: this.data.order.id },
      { key: "_expand", val: "product" }
    ];
    this.dataService.getData("cars", null, filters)
    .subscribe((data: IData) => {
      let carsArr = <IOrderCar[]>data.res;
      this.data.oldCars = carsArr.slice(0);
      this.cars = carsArr;
      this.setArrs();
    });
}

但因為firebase是nosql資料庫,所以無法關聯其他表,
因此得自己手動寫一個,如docJoin()
傳送回來的list把內部每一個都送去拿關聯式的值。

getOne() 利用的關聯式function為docJoinFilterOne()
docJoin()是把list中有指定某筆送去找關聯式資料,
目前只有 connect() 用到

  • getData() 內部作分流,根據參數是否有送id,來決定是送list函式還是送one函式。

  • setQuery() 就是把filters拆出來變成firebase的query,可以多種篩選值。

  • insertOne() 內部流程改變,因不需要firebase自己產生的id,
    又需要產生自動流水號,所以利用counterstable內的紀錄編號,
    只要觸發 insertOne(),count+1後再新增資料。


(三) 修改list中的關聯式

需要改掉list中搜尋的關聯式,
在json-server裡的關聯式是 _embed_expand
所以直接在vscode中直接搜尋,
並且改成如下:

let link = {
    join: "customers",
    key: "customerId"
}
.getData("orders", null, this.setFilters(), this.tab.pageObj,link )

舉個例子:

//查看購物車資訊。
//car.component.ts
setDatas() {
    let filters: IFilter[] = [
      { key: "orderId", val: this.order.id }
      //  { key: "_expand", val: "product" }
    ];
    let link = {
      join: "products",
      key: "productId"
    };
    this.dataService.getData("cars", null, filters, null, link)
    .subscribe((data: IData) => {
      if (!data.errorcode && !!data.res) {
        this.cars = <IOrderCar[]>data.res;
      }
    });
}

上一篇
day27 Firebase(一)
下一篇
day29 Protocol Buffers 與 gRPC Web (一)
系列文
用Angular打造完整後台30

尚未有邦友留言

立即登入留言