iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 30
0
Modern Web

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

day30 Protocol Buffers 與 gRPC Web (二)

簡述

實作串接時會用到的CRUD。

檔案結構

-src
  |-app
  |-api
    |-file
        |-base.proto
        |-customer.proto
        ...
        |-store.proto
    |-web
        |-base_pb.d.ts
        |-base_pb.js
        |-customer_pb.d.ts
        |-customer_pb.js
        ...
        |-store_pb.d.ts
        |-store_pb.js
        |-StoreServiceClientPb.ts

實作

首先

  • 先下指令安裝。
npm install grpc-web -save
npm install google-protobuf -save
  • 把proto檔當成api文件放入專案裡。

  • 把proto轉譯過的ts跟js檔案放入專案裡。

(一) 基礎連線

通常跟後端連線以及資料處理會分兩個service:

  • web.service.ts:處理連線以及把資訊流(stream)轉成Observale

  • data.service.ts:把自定義的物件轉型成protobuf的物件,
    並送去WebService裡得到資訊流(stream)

web.service.ts

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

export type TFResult =
  | DbMessage
  | AdminFResult
  | CustomerFResult
  | LevelFResult
  | ProductFResult
  | TypeFResult
  | OrderFResult
  | CarFResult;

export interface IResult {
  errorcode: Status;
  res: TFResult;
}

@Injectable({
  providedIn: "root"
})
export class WebService {
  apiUrl = ApiUrl;
  store: StoreClient;

  constructor() {
    this.store = new StoreClient(this.apiUrl, {}, {});
  }

  /**Base */
  
  //把stream資訊流轉成Observable
  getResult(stream: ClientReadableStream<TFResult>)
  : Observable<IResult> 
  {
    return Observable.create((obs: Observer<IResult>) => {
      //後端傳回值
      stream.on("data", data =>
        obs.next({
          errorcode: null,
          res: data
        })
      );
      //後端傳送錯誤訊息
      stream.on("status", status =>
        obs.next({
          errorcode: status,
          res: null
        })
      );
      stream.on("error", err => obs.error(err));
      stream.on("end", () => obs.complete());
    });
  }

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

  getCustomerList(customerf: CustomerF): Observable<IData> {
    let stream = this.store.findCustomer(customerf, {}, null);
    let customers = <ICustomer[]>[];

    return this.getResult(stream).pipe(
      map((data: IResult) => {
        if (!!data.res) {
          let res = (<CustomerFResult>data.res).toObject();
          res.customerList.forEach(item => {
            let obj: ICustomer = { ...item };
            customers.push(obj);
          });
        }
        return this.resData(customers, 0);
      })
    );
  }

  ...
}

stream有兩個函式可以使用:

  • stream.on():資訊流建立起來後,就會一直傳送資料。

  • stream.cancel():把資訊流取消。

--

stream.on()有4個模式可以使用:

  • data

舉個例子:

service Store {
    ...
    // customer 修讀寫
    rpc FindCustomer(CustomerF) returns (CustomerFResult) {}
    rpc InsertCustomer(Customer) returns (DbMessage) {}
    rpc UpdateCustomer(Customer) returns (DbMessage) {}
    ...
}

後端傳回值的 CustomerFResultDbMessage
都會在 data模式 裡回來。

  • status: 後端自定義的錯誤,包成Status物件,
    會從 status模式 傳回來。

  • error:gRPC底層的錯誤或是連線的錯誤。

  • end:gRPC關閉。


(二) 列表搜尋

customer.proto

message Customer {
    int64 id = 1;
    string name = 2;
    string account = 3;
    string password = 4;
    int32 status = 5;
    int64 level_id = 6;
    string level_name = 7;
    int64 inserted = 8;
    int64 insert_by = 9;
    int64 updated = 10;
    int64 update_by = 11;
}


message CustomerF {
    Limit limit = 1;
    string name = 2;
    string account = 3;
    int32 status = 4;
    int64 level_id = 5;
}

message CustomerFResult {
    Limit limit = 1;
    repeated Customer customer = 2;
}
  • Customer:customer的物件組成。

  • CustomerF:對 customerlist 的篩選條件。

  • CustomerFResult:後端傳回的回應值,
    頁數以及 customerlist 陣列。

--

base.proto

message DbMessage {
    int64 inserted_id = 1;   // 新增獲得的ID
    int64 error_code = 2;   // 錯誤代碼
    int64 affected_row = 3; // 更新影響的行數
}

message Limit {
    int64 length = 1; // 總筆數
    int64 page_index = 2; // 第幾頁
    int64 page_size = 3; // 一頁幾筆
}
...

--

store.proto

此為service檔案。
前面寫 rpc 代表是gRPC的method。

service Store {
    ...
    // customer 修讀寫
    rpc FindCustomer(CustomerF) returns (CustomerFResult) {}
    rpc InsertCustomer(Customer) returns (DbMessage) {}
    rpc UpdateCustomer(Customer) returns (DbMessage) {}
    ...
}
  • FindCustomer:是搜尋 customerlist
    需要代入 CustomerF 的參數,並會回傳 CustomerFResult 物件。

  • InsertCustomer:是新增customer。
    需要代入 Customer 的參數,並會回傳 DbMessage 物件。

  • UpdateCustomer:是更新customer。
    需要代入 Customer 的參數,並會回傳 DbMessage 物件。

--

app.component.ts

  constructor(private dataService: DataService) {}

  ngOnInit() {
    this.dataService.getData("customers").subscribe(val => {
      console.log(val);
    });
  }

--

data.service.ts

@Injectable({
  providedIn: "root"
})
export class DataService {
  constructor(private webService: WebService) {}
  setCustomerFilter(filters?: IFilter[], pageObj?: Limit)
  : CustomerF 
  {
    const customerF = new CustomerF();
    if (!!filters) {
      filters.forEach((item: IFilter) => {
        switch (item.key) {
          case "name":
            customerF.setName(<string>item.val);
            break;
          case "account":
            customerF.setAccount(<string>item.val);
            break;
          case "status":
            customerF.setStatus(<number>item.val);
            break;
          case "levelId":
            customerF.setLevelId(<number>item.val);
            break;
        }
      });
    }
    if (!!pageObj) {
      customerF.setLimit(pageObj);
    }
    return customerF;
  }

  getData(
    position: string, 
    id?: number, 
    filters?: IFilter[], 
    pageObj?: IPage
  )
  : Observable<IData> 
  {
    let list: Observable<IData> = null;
    switch (position) {
      case "customers":
        let customerf = this.setCustomerFilter(filters);
        list = this.webService.getCustomerList(customerf);
        break;
      ...
    }
    return list;
  }
}

ICustomer 為自定義物件。
CustomerF 為proto檔轉成的ts或js檔裡的物件。

其實proto檔轉成的ts或js檔,要使用裡面的物件,
必須都要寫相對應的setXXX(),所以統一放在這直接做轉接口。


(三) 新增或更新資料

app.component.ts

let customer: ICustomer = {
    name: "cus787",
    account: "cus787",
    password: "cus787",
    levelId: 1,
    status: 1,
    insertBy: 1,
    inserted: new Date().getTime(),
    updateBy: 1,
    updated: new Date().getTime()
};
this.dataService.insertOne("customers",customer)
.subscribe(val => {
  console.log(val);
});

--

data.component.ts

insertOne(position: string, obj: IModel): Observable<IResult> {
    let data: Observable<IResult> = null;
    switch (position) {
      case "customers":
        let customer = this.setCustomer(<ICustomer>obj);
        data = this.webService.insertCustomer(customer);
        break;
    }
    return data;
  }

  updateOne(position: string, obj: IModel): Observable<IResult> {
    let data: Observable<IResult> = null;
    switch (position) {
      case "customers":
        let customer = this.setCustomer(<ICustomer>obj);
        data = this.webService.updateCustomer(customer);
        break;
    }
    return data;
}


setCustomer(obj: ICustomer): Customer {
    const customer = new Customer();
    if(!!obj.id){
      customer.setId(obj.id);
    }
    if(!!obj.name){
      customer.setName(obj.name);
    }
    if(!!obj.account){
      customer.setAccount(obj.account);
    }
    if(!!obj.password){
      customer.setPassword(obj.password);
    }
    if(!!obj.levelId){
      customer.setLevelId(obj.levelId);
    }
    if(!!obj.status){
      customer.setStatus(obj.status);
    }
    if(!!obj.insertBy){
      customer.setInsertBy(obj.insertBy);
    }
    if(!!obj.inserted){
      customer.setInserted(obj.inserted);
    }
    if(!!obj.updateBy){
      customer.setUpdateBy(obj.updateBy);
    }
    if(!!obj.updated){
      customer.setUpdated(obj.updated);
    }
    return customer;
}

其實多半做的事情大概就是認真塞要用的物件,
流程一樣是利用proto產生的service,來產生一個資訊流(stream),
再把資訊流轉成Observable使用。

--

web.component.ts

...

/**Insert */

  insertCustomer(customer: Customer): Observable<IResult> {
    let stream = this.store.insertCustomer(customer, {}, null);

    return this.getResult(stream).pipe(
      map((data: IResult) => {
        //do something

        let m: DbMessage = <DbMessage>data.res;
        console.log("insert id", m.getInsertedId());
        return data;
      })
    );
  }

  /**Update */

  updateCustomer(customer: Customer): Observable<IResult> {
    let stream = this.store.updateCustomer(customer, {}, null);

    return this.getResult(stream).pipe(
      map((data: IResult) => {
        //do something

        let m: DbMessage = <DbMessage>data.res;
        console.log("insert id", m.getAffectedRow());
        return data;
      })
    );
}

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

尚未有邦友留言

立即登入留言