Доступ к данным с помощью клиента gRPC-web от Improbable¶
Написать клиент для gRPC-web легко. Improbable предлагает общую фукцию для управления как унарными вызовами, так и серверными потоками, метод grpc.invoke, который вызывается так:
MethodDescriptor — это метод, который вы хотите вызвать. Методы определены в сгенерированном файле {ProtobuFname}_pb_services.d.ts как свойства класса, обозначающего сервис gRPC-web:
Вот сервис
CountryServiceBrowser и метод CountryServiceBrowserCreate. Если мы захотим вызвать метод Create, то MethodDescriptor-ом будет CountryServiceBrowser.Create.Второй параметр,
InvokeRpcOptions, имеет следующие свойства:
host— URI сервера, напримерhttps://localhost:5001;request— сообщение, отправляемое на сервер, например,new Empty();metadata— трейлеры, которые будут отправлены на сервер, например,new grpc.Metadata({"TrailerKey": "TrailerValue"});onHeaders— коллбэк, обрабатывающий заголовки, пришедшие с сервера, например,(headers: grpc.Metadata) => { const headersValue = headers.get("HeaderKey"); };onMessage— коллбэк, обрабатывающий сообщения, пришедшие с сервера, например,(countryReply: CountryReply) => { const country => countryReply.toObject(); }. Обратите внимание, что каждое сообщение должно быть десериализовано методомtoObject(). Коллбэк вызывается один раз в случае унарного вызова или на каждое сообщение в случае серверного потока;onEnd— коллбэк, вызываемый в конце вызова и позволяющий обработать данные, полученные от сервера в завершение вызова: gRPC-статус, трейлеры, и строковое сообщение; например,(code: grpc.Code, msg: string | undefined, trailers: grpc.Metadata, endMessage: String) => { if (code !== grpc.Code.Ok) { ... } else { ... } };transport— опциональное свойство, позволяет отправлять куки на сервер вместе с cross-origin запросами; например,grpc.CrossBrowserHttpTransport({ withCredentials: true });1debug— опциональное свойство, позволяющее выводить отладочную информацию в консоль.
Обратите внимание, что метод grpc.invoke() возвращает объект Request, содержащий метод close(). Если вызвать этот метод так, как показано, то запрос будет отменён, а соединение с сервером закрыто:
Теперь рассмотрим реализацию countryService, которую мы поместим в файл src/app/services/countryService.ts:
import { grpc } from "@improbable-eng/grpc-web";
import { Empty } from "google-protobuf/google/protobuf/empty_pb";
import { CountryServiceBrowser } from "../generated/country.browser_pb_service";
import { CountriesCreationRequest, CountryCreationReply, CountryIdRequest, CountryReply, CountryUpdateRequest } from "../generated/country.shared_pb";
import { CountryCreationModelMapper } from "../mappers/countryCreationModelMapper";
import { CountryReplyMapper } from "../mappers/countryReplyMapper";
import { CountryCreationModel } from "../models/countryCreationModel";
import { CountryModel } from "../models/countryModel";
import { environment } from "../../environments/environment";
import { Injectable } from "@angular/core";
import { CountryUpdateModel } from "../models/countryUpdateModel";
import { UploadResultModel } from "../models/uploadResultModel";
@Injectable()
export class CountryService {
public GetAll(countries: CountryModel[]): void {
grpc.invoke(CountryServiceBrowser.GetAll,
{
request: new Empty(),
host: environment.host,
onMessage: (countryReply: CountryReply) => {
let country = new CountryModel();
CountryReplyMapper.Map(country, countryReply.toObject())
countries.push(country);
},
onEnd: (code: grpc.Code, msg: string | undefined, trailers: grpc.Metadata) => this.onEnd(code,
msg,
trailers,
"All countries have been downloaded")
});
}
public Create(countriesToCreate: CountryCreationModel[], uploadResult: UploadResultModel, callback: Function): void {
let countriesCreationRequest = new CountriesCreationRequest();
CountryCreationModelMapper.Maps(countriesCreationRequest, countriesToCreate);
grpc.invoke(CountryServiceBrowser.Create,
{
request: countriesCreationRequest,
host: environment.host,
onEnd: (code: grpc.Code, msg: string | undefined, trailers: grpc.Metadata) => {
uploadResult.isProcessing = false;
callback();
this.onEnd(code, msg, trailers, "All countries have been created")
}
});
}
public Delete(id: number): void {
let request = new CountryIdRequest();
request.setId(id);
grpc.invoke(CountryServiceBrowser.Delete,
{
request: request,
host: environment.host,
onEnd: (code: grpc.Code, msg: string | undefined, trailers: grpc.Metadata) => this.onEnd(code, msg, trailers, 'Country with Id ${id} has been deleted')
});
}
public Get(id: number, country: CountryModel): void {
let request = new CountryIdRequest();
request.setId(id);
grpc.invoke(CountryServiceBrowser.Get,
{
request: request,
host: environment.host,
onMessage: (countryReply: CountryReply) => {
CountryReplyMapper.Map(country, countryReply.toObject());
},
onEnd: (code: grpc.Code, msg: string | undefined, trailers: grpc.Metadata) => this.onEnd(code,
msg,
trailers,
'Country with Id ${id} was successfully found')
});
}
public Update(countryUpdateModel: CountryUpdateModel): void {
let request = new CountryUpdateRequest();
request.setId(countryUpdateModel.id);
request.setDescription(countryUpdateModel.description);
grpc.invoke(CountryServiceBrowser.Update,
{
request: request,
host: environment.host,
onEnd: (code: grpc.Code, msg: string | undefined, trailers: grpc.Metadata) => this.onEnd(code,
msg,
trailers,
'Country with Is ${countryUpdateModel.id} was successfully updated')
});
}
private onEnd(code: grpc.Code, msg: string | undefined, trailers: grpc.Metadata, endMessage: String): void {
if (code == grpc.Code.OK) {
console.log(endMessage);
} else {
console.log('Hit an error status: ${grpc.Code[code]}');
if (msg) {
console.log('message: ${msg}');
}
trailers.forEach(trailer => {
console.log('with the trailer ${trailer}: ${trailers.get(trailer)}');
});
}
}
}
Здесь:
- общий метод
onEnd, в котором выполняется логирование; GetAll()получает массивCountryModel, в который складывает полученные из потока сообщенийCountryReplyв методе, указанном вonMessage. Мы преобразуем каждое сообщение в объект моделиCountryModelв статическом методеCountryReplyMapper.Map, который мы рассмотрим чуть позже;Create()получает массив объектовCountryCreationModel, которые преобразовывает вCountryCreationRequest, а также переменнуюUploadResultModel, которую мы заполняем в методеonEnd, отмечая, что загрузка выполнена. Последний параметр —callback, это функция, которую вызывает методonEndпосле окончания загрузки. Преобразование выполняется в статическом методеCountryCreationModelMapper.Maps;Deleteполучает идентификатор записи, заполняет объектCountryIdRequestи отправляет запрос на удаление;Getполучает идентификатор записи, которую требуется загрузить, и переменную, в которую нужно сложить результат. Преобразование выполняется в статическом методеCountryReplyMapper.Map;Update()получает объектCountryUpdateModel, заполняет объектCountryUpdateRequestи отправляет запрос.
Теперь приведём использованные модели, файлы которых разместим в папке src/app/models.
import { ActionResultModel } from "./actionResultModel";
import { CountryCreationModel } from "./countryCreationModel";
export class UploadResultModel extends ActionResultModel {
payload!: CountryCreationModel[];
isProcessing!: boolean;
}
export class CountryModel {
id!: number;
name!: String;
description!: String;
capitalCity!: String;
anthem!: String;
languages!: String[];
flagUri!: String;
}
Для
CountryCreationModel нам потребуется ещё одна библиотека, ts-json-object. Эта библиотека позволяет добавлять аннотации к объявлению свойств класса с данными, например, пометить свойство как обязательное. При этом, если свойство не будет заполнено, возникнет исключение.Чтобы всё работало, ваш класс должен быть унаследован от класса
JSONObject, который также предоставляет конструктор, в который можно передать объект анонимного класса, из которого будут заполнены свойства.Вот класс
countryCreationModel с аннотированными свойствами.import { JSONObject } from "ts-json-object"
export class CountryCreationModel extends JSONObject {
@JSONObject.required
name!: string;
@JSONObject.required
description!: string;
@JSONObject.required
capitalCity!: string;
@JSONObject.required
anthem!: string;
@JSONObject.required
flagUri!: string;
@JSONObject.required
languages!: number[];
}
В завершение раздела приведём код мапперов
CountryCreationMapper и CountryReplyMapper.import { CountriesCreationRequest, CountryCreationRequest } from "../generated/country.shared_pb";
import { CountryCreationModel } from "../models/countryCreationModel";
export class CountryCreationModelMapper {
public static Map(countryCreationRequest: CountryCreationRequest, countryCreationModel: CountryCreationModel) {
if(!countryCreationModel)
return;
countryCreationRequest.setName(countryCreationModel.name);
countryCreationRequest.setDescription(countryCreationModel.description);
countryCreationRequest.setAnthem(countryCreationModel.anthem);
countryCreationRequest.setCapitalcity(countryCreationModel.capitalCity);
countryCreationRequest.setFlaguri(countryCreationModel.flagUri);
countryCreationRequest.setLanguagesList(countryCreationModel.languages);
}
public static Maps(countriesCreationRequest: CountriesCreationRequest, countriesCreationModel: CountryCreationModel[]) {
if(!countriesCreationModel)
return;
countriesCreationModel.map(x => {
let countryCreationRequest = new CountryCreationRequest();
CountryCreationModelMapper.Map(countryCreationRequest, x);
countriesCreationRequest.addCountries(countryCreationRequest);
});
}
}
import { CountryReply } from "../generated/country.shared_pb";
import { CountryModel } from "../models/countryModel";
export class CountryReplyMapper {
public static Map(country: CountryModel, countryReply: CountryReply.AsObject) {
if(country == null || countryReply == null)
return;
country.id = countryReply.id;
country.name = countryReply.name;
country.description = countryReply.description;
country.capitalCity = countryReply.capitalcity;
country.flagUri = countryReply.flaguri;
country.anthem = countryReply.anthem;
country.languages = countryReply.languagesList;
}
}
-
В этой главе свойство
transportиспользоваться не будет, поэтому интересующихся отсылаем к документации ↩
Последнее обновление :
2 августа 2023 г.
Дата создания : 2 августа 2023 г.
Дата создания : 2 августа 2023 г.