Панель Управления Услугами. Часть 2. На Пути К Фронтенду



Введение.

Еще немного об API.

Панель управления услугами.
</p><p>
 Часть 2. На пути к фронтенду

Так, последний раз Мы остановились на описании процесса сборки API, с тех пор кое-что изменилось.

А именно, Grunt был заменен на Gulp. Основная причина такого изменения – скорость работы.

После перехода разница стала заметна невооруженным глазом (тем более, что gulp отображает время, потраченное на каждую задачу).

Такой результат достигается за счет того, что все задачи по умолчанию выполняются параллельно.

Это решение нас вполне устроило.

Некоторые части работы, которую проделывал Грант, были независимы друг от друга, а значит, их можно было выполнять одновременно.

Например, добавление отступов к файлам определений и путей.

К сожалению, были и недостатки.

Задачи, требующие завершения предыдущих, остаются.

Но у gulp обширная база пакетов на все случаи жизни, поэтому решение было найдено довольно быстро — пакет runSequence

  
  
  
  
  
  
  
  
  
  
   

gulp.task('client', (callback) => { runSequence( 'client:indent', 'client:concat', 'client:replace', 'client:validate', 'client:toJson', 'client:clean', callback ); });

То есть вместо стандартного объявления задачи для gulp в качестве аргумента передается обратный вызов, в котором задачи выполняются в указанном порядке.

В нашем случае порядок выполнения был важен только для 4 задач из 40, поэтому прирост скорости по сравнению с Grunt заметен.

Также gulp позволил нам отказаться от Coffeescript в пользу ES6. Код менялся минимально, но при нечастых изменениях конфигурации сборки api отпадала необходимость запоминать, как писать на Coffeescript, так как он больше нигде не использовался.

Пример некоторых конфигураций для сравнения:

Глоток



gulp.task('admin:indent_misc', () => { return gulp.src(`${root}/projects.yml`) .

pipe(indent({ tabs: false, amount: 2 })) .

pipe(gulp.dest(`${interimDir}`)) });



хрюканье



indent: admin_misc: src: [ '<%= admin_root %>/authorization.yml' '<%= admin_root %>/projects.yml' ] dest: '<%= admin_interim_dir %>/' options: style: 'space' size: 2 change: 1

Также стоит отметить небольшие грабли, на которые нам удалось наступить.

Они были следующими: после генерации файлов API и попытки запуска приложения Angular возникла ошибка при реэкспорте ResourceService. Отправной точкой поиска стал файл api/model/models.ts. Он содержит экспорт всех интерфейсов и сервисов, которые будут использоваться в будущем.

Здесь следует добавить небольшое отступление и рассказать, как swagger-codegen присваивает имена интерфейсам и сервисам.

Небольшое отступление Интерфейс Основанный на шаблон интерфейса , если свойство сущности имеет объектный тип, то для него создается отдельный интерфейс, который называется %Entity_NameProperty_Name%.

Услуга Основанный на шаблон услуги Имя сервиса состоит из имени тега и слова Service, например OrderService. Поэтому если в спецификации указать несколько тегов для пути, то этот метод попадет в несколько сервисов.

Такой подход позволяет импортировать только необходимый сервис в одном случае и избежать импорта нескольких сервисов в другом.

Итак, в файле models.ts на самом деле было два экспорта ResourceService: один представлял службу для доступа к методам сущности ресурса, а второй представлял интерфейс для свойства службы сущности ресурса.

Именно поэтому и возник такой конфликт. Решением было переименовать объект.

От API до фронтенда.



Панель управления услугами.
</p><p>
 Часть 2. На пути к фронтенду

Как я уже говорил, спецификация swagger позволяет генерировать необходимые файлы для работы с API как для бэкенда, так и для фронтенда.

В нашем случае генерация API-кода для Angular2 выполняется с помощью простой команды:

java -jar .

/swagger-codegen-cli.jar generate \ -i client_swagger.json \ -l typescript-angular \ -o .

/src/app/api \ -c .

/typescript_config.json

Параметры разбора:

  • java -jar .

    /swagger-codegen-cli.jargenerate — запустить jar-файл swagger-codegen

  • -i client_swagger.json — файл спецификации, полученный в результате работы Gulp.
  • -l typescript-angular – язык, для которого генерируется код
  • -o .

    /src/app/api — целевой каталог для файлов API

  • -c .

    /typescript_config.json — дополнительная настройка (чтобы исправить проблему с именованием переменных, о которой я говорил в первой части)

Учитывая, что количество языков, а соответственно шаблонов и кода для генерации огромно, время от времени в голове появляется идея пересобрать кодеген только под свои нужды и оставить только Typescript-Angular. Более того, сами разработчики предоставляют инструкции для добавления собственных шаблонов.

Таким простым способом мы получаем все необходимые модули, интерфейсы, классы и сервисы для работы с API. Пример одного из интерфейсов, полученных с помощью codegen: Входной файл спецификации service_definition.yaml

Service: type: object required: - id properties: id: type: integer description: Unique service identifier format: 'int32' readOnly: true date: type: string description: Registration date format: date readOnly: true start_date: type: string description: Start service date format: date readOnly: true expire_date: type: string description: End service date format: date readOnly: true status: type: string description: Service status enum: - 'empty' - 'allocated' - 'launched' - 'stalled' - 'stopped' - 'deallocated' is_primary: type: boolean description: Service primary state priority: type: integer description: Service priority format: 'int32' readOnly: true attributes: type: array description: Service attributes items: type: string primary_service: type: integer description: Unique service identifier format: 'int32' readOnly: true example: 138 options: type: array items: type: string order: type: integer description: Unique order identifier format: 'int32' readOnly: true proposal: type: integer description: Unique proposal identifier format: 'int32' readOnly: true resources: type: array items: type: object properties: url: type: string description: Resources for this service Services: type: array items: $ref: '#/definitions/Service'

На выходе получается интерфейс, понятный Angular.

import { ServiceOptions } from '.

/serviceOptions'; import { ServiceOrder } from '.

/serviceOrder'; import { ServicePrimaryService } from '.

/servicePrimaryService'; import { ServiceProposal } from '.

/serviceProposal'; import { ServiceResources } from '.

/serviceResources'; /** * Service entry reflects fact of obtaining some resources within order (technical part).

In other hand service points to proposal that was used for ordering (commercial part).

Service can be primary (ordered using tariff proposal) and non-primary (ordered using option proposal).

*/ export interface Service { /** * Record id */ id: number; /** * Service order date */ date?: string; /** * Service will only be launched after this date (if nonempty) */ start_date?: string; /** * Service will be stopped after this date (if nonempty) */ expire_date?: string; /** * Service current status. Meaning: * empty - initial status, not allocated * allocated - all option services and current service are allocated and ready to launch * launched - all option services and current one launched and works * stalled - service can be stalled in any time. Options also goes to the same status * stopped - service and option services terminates their activity but still stay allocated * deallocated - resources of service and option ones are released and service became piece of history */ status?: number; /** * Whether this service is primary in its order. Otherwise it is option service */ is_primary?: boolean; /** * Optional priority in order allocating process. The less number the earlier service will be allocated */ priority?: number; primary_service?: ServicePrimaryService; order?: ServiceOrder; proposal?: ServiceProposal; /** * Comment for service */ comment?: string; /** * Service's cost (see also pay_type, pay_period, onetime_cost) */ cost?: number; /** * Service's one time payment amount */ onetime_cost?: number; /** * Bill amount calculation type depending on service consuming */ pay_type?: Service.PayTypeEnum; /** * Service bill payment period */ pay_period?: Service.PayPeriodEnum; options?: ServiceOptions; resources?: ServiceResources; } export namespace Service { export enum PayTypeEnum { Fixed = <any> 'fixed', Proportional = <any> 'proportional' } export enum PayPeriodEnum { Daily = <any> 'daily', Monthly = <any> 'monthly', Halfyearly = <any> 'halfyearly', Yearly = <any> 'yearly' } }

Отрывок из файла спецификации service_path.yml

/dedic/services: get: tags: [Dedicated, Service] x-swagger-router-controller: app.controllers.service operationId: get_list security: - oauth: [] summary: Get services list parameters: - $ref: '#/parameters/limit' - $ref: '#/parameters/offset' responses: 200: description: Returns services schema: $ref: '#/definitions/Services' examples: application/json: objects: - id: 3 date: '2016-11-01' start_date: '2016-11-02' expire_date: '2017-11-01' status: 'allocated' is_primary: true priority: 3 primary_service: null options: url: " https://doc.miran.ru/api/v1/dedic/services/3/options " order: url: ' https://doc.miran.ru/api/v1/orders/3 ' comment: 'Test comment for service id3' cost: 2100.00 onetime_cost: 1000.00 pay_type: 'fixed' pay_period: 'daily' proposal: url: ' https://doc.miran.ru/api/v1/dedic/proposals/12 ' agreement: url: ' https://doc.miran.ru/api/v1/agreements/5 ' resorces: url: " https://doc.miran.ru/api/v1/dedic/services/3/resources " - id: 7 date: '2016-02-12' start_date: '2016-02-12' expire_date: '2016-02-12' status: 'stopped' is_primary: true priority: 2 primary_service: null options: url: " https://doc.miran.ru/api/v1/dedic/services/7/options " order: url: ' https://doc.miran.ru/api/v1/orders/7 ' comment: 'Test comment for service id 7' cost: 2100.00 onetime_cost: 1000.00 pay_type: 'fixed' pay_period: 'daily' proposal: url: ' https://doc.miran.ru/api/v1/dedic/proposals/12 ' agreement: url: ' https://doc.miran.ru/api/v1/agreements/2 ' resorces: url: " https://doc.miran.ru/api/v1/dedic/services/7/resources " total_count: 2 500: $ref: "#/responses/Standard500" post: tags: [Dedicated, Service] x-swagger-router-controller: app.controllers.service operationId: create security: - oauth: [] summary: Create service in order parameters: - name: app_controllers_service_create in: body schema: type: object additionalProperties: false required: - order - proposal properties: order: type: integer description: Service will be attached to this preliminary created order format: 'int32' minimum: 0 proposal: type: integer format: 'int32' description: Proposal to be used for service. Tariff will create primary service, not tariff - option one minimum: 0 responses: 201: description: Service successfully created 400: description: Incorrect order id (deleted or not found) or proposal id (expired or not found)

Отрывок из готового сервиса для Angular

/* tslint:disable:no-unused-variable member-ordering */ import { Inject, Injectable, Optional } from '@angular/core'; import { Http, Headers, URLSearchParams } from '@angular/http'; import { RequestMethod, RequestOptions, RequestOptionsArgs } from '@angular/http'; import { Response, ResponseContentType } from '@angular/http'; import { Observable } from 'rxjs/Observable'; import '.

/rxjs-operators'; import { AppControllersServiceCreate } from '.

/model/appControllersServiceCreate'; import { AppControllersServiceUpdate } from '.

/model/appControllersServiceUpdate'; import { InlineResponse2006 } from '.

/model/inlineResponse2006'; import { InlineResponse2007 } from '.

/model/inlineResponse2007'; import { InlineResponse2008 } from '.

/model/inlineResponse2008'; import { InlineResponse2009 } from '.

/model/inlineResponse2009'; import { InlineResponse401 } from '.

/model/inlineResponse401'; import { Service } from '.

/model/service'; import { Services } from '.

/model/services'; import { BASE_PATH, COLLECTION_FORMATS } from '.

/variables'; import { Configuration } from '.

/configuration'; @Injectable() export class ServiceService { protected basePath = ''; public defaultHeaders: Headers = new Headers(); public configuration: Configuration = new Configuration(); constructor( protected http: Http, @Optional()@Inject(BASE_PATH) basePath: string, @Optional() configuration: Configuration) { if (basePath) { this.basePath = basePath; } if (configuration) { this.configuration = configuration; this.basePath = basePath || configuration.basePath || this.basePath; } } /** * * Extends object by coping non-existing properties. * @param objA object to be extended * @param objB source object */ private extendObj<T1,T2>(objA: T1, objB: T2) { for(let key in objB){ if(objB.hasOwnProperty(key)){ (objA as any)[key] = (objB as any)[key]; } } return <T1&T2>objA; } /** * @param consumes string[] mime-types * @return true: consumes contains 'multipart/form-data', false: otherwise */ private canConsumeForm(consumes: string[]): boolean { const form = 'multipart/form-data'; for (let consume of consumes) { if (form === consume) { return true; } } return false; } /** * * @summary Delete service * @param id Unique entity identifier */ public _delete(id: number, extraHttpRequestParams?: any): Observable<{}> { return this._deleteWithHttpInfo(id, extraHttpRequestParams) .

map((response: Response) => { if (response.status === 204) { return undefined; } else { return response.json() || {}; } }); } /** * * @summary Create service in order * @param appControllersServiceCreate */ public create(appControllersServiceCreate?: AppControllersServiceCreate, extraHttpRequestParams?: any): Observable<{}> { return this.createWithHttpInfo(appControllersServiceCreate, extraHttpRequestParams) .

map((response: Response) => { if (response.status === 204) { return undefined; } else { return response.json() || {}; } }); } /** * Create service in order * * @param appControllersServiceCreate */ public createWithHttpInfo appControllersServiceCreate?: AppControllersServiceCreate, extraHttpRequestParams?: any): Observable<Response> { const path = this.basePath + '/dedic/services'; let queryParameters = new URLSearchParams(); // https://github.com/angular/angular/issues/6845 let headers = new Headers(this.defaultHeaders.toJSON()); // to determine the Accept header let produces: string[] = [ 'application/json' ]; // authentication (oauth) required // oauth required if (this.configuration.accessToken) { let accessToken = typeof this.configuration.accessToken === 'function' ? this.configuration.accessToken() : this.configuration.accessToken; headers.set('Authorization', 'Bearer ' + accessToken); } headers.set('Content-Type', 'application/json'); let requestOptions: RequestOptionsArgs = new RequestOptions({ method: RequestMethod.Post, headers: headers, // https://github.com/angular/angular/issues/10612 body: appControllersServiceCreate == null ? '' : JSON.stringify(appControllersServiceCreate), search: queryParameters, withCredentials:this.configuration.withCredentials }); // https://github.com/swagger-api/swagger-codegen/issues/4037 if (extraHttpRequestParams) { requestOptions = (<any>Object).

assign(requestOptions, extraHttpRequestParams); } return this.http.request(path, requestOptions); } }

Таким образом, чтобы сделать, например, пост-запрос на создание сервиса с использованием соответствующего сервиса, необходимо:

  • Добавить в компонент Service.service
  • Вызовите метод service.create с параметром в соответствии с интерфейсом appControllersServiceCreate.
  • Подпишитесь, чтобы получить результат
Хотелось бы сразу объяснить, почему параметр имеет имя в стиле Java. Причина в том, что это имя формируется из спецификации, а точнее из поля имени:

post: tags: [Dedicated, Service] x-swagger-router-controller: app.controllers.service operationId: create security: - oauth: [] summary: Create service in order parameters: - name: app_controllers_service_create in: body

Мы решили использовать такое громоздкое имя, чтобы имена не пересекались и были уникальными.

Если вы укажете, например, data в качестве имени, то codegen добавит к данным счетчик и в результате получится 10 интерфейсов с именами Data_0, Data_1 и так далее.

Найти нужный интерфейс при импорте становится проблематично).

Также стоит знать, что codegen создает модули, которые необходимо импортировать, а их имена формируются на основе тега метода.

Таким образом, описанный выше метод будет присутствовать в модулях Dedicated и Service. Это удобно, так как позволяет не импортировать все API и не блуждать по методам, а использовать только то, что необходимо компоненту.

Как известно, в Angular 4.4 заменили HttpModule на HttpClientModule, что добавило удобства (о разнице можно почитать, например здесь .

Но, к сожалению, текущая стабильная версия codegen работает с HttpModule. Поэтому подобные конструкции остаются: HttpClientModule по умолчанию возвращает json:

.

map((response: Response) => { if (response.status === 204) { return undefined; } else { return response.json() || {}; }

Добавление заголовка для авторизации ложится на плечи HttpПерехватчик :

if (this.configuration.accessToken) { let accessToken = typeof this.configuration.accessToken === 'function' ? this.configuration.accessToken() : this.configuration.accessToken; headers.set('Authorization', 'Bearer ' + accessToken); }

Ждём обновления, а пока работаем с тем, что есть.

В следующей части я начну рассказ непосредственно об Angular и затрону API со стороны фронтенда.

Теги: #api #проектирование системы #архитектура системы #angular #панель управления #angularjs #центры обработки данных «Миран»

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.