Введение.
Еще немного об API.
Так, последний раз Мы остановились на описании процесса сборки API, с тех пор кое-что изменилось.
А именно, Grunt был заменен на Gulp. Основная причина такого изменения – скорость работы.
После перехода разница стала заметна невооруженным глазом (тем более, что gulp отображает время, потраченное на каждую задачу).
Такой результат достигается за счет того, что все задачи по умолчанию выполняются параллельно.
Это решение нас вполне устроило.
Некоторые части работы, которую проделывал Грант, были независимы друг от друга, а значит, их можно было выполнять одновременно.
Например, добавление отступов к файлам определений и путей.
К сожалению, были и недостатки.
Задачи, требующие завершения предыдущих, остаются.
Но у gulp обширная база пакетов на все случаи жизни, поэтому решение было найдено довольно быстро — пакет runSequence
То есть вместо стандартного объявления задачи для gulp в качестве аргумента передается обратный вызов, в котором задачи выполняются в указанном порядке.gulp.task('client', (callback) => { runSequence( 'client:indent', 'client:concat', 'client:replace', 'client:validate', 'client:toJson', 'client:clean', callback ); });
В нашем случае порядок выполнения был важен только для 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 до фронтенда.
Как я уже говорил, спецификация 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 — дополнительная настройка (чтобы исправить проблему с именованием переменных, о которой я говорил в первой части)
Таким простым способом мы получаем все необходимые модули, интерфейсы, классы и сервисы для работы с 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.
- Подпишитесь, чтобы получить результат
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 #центры обработки данных «Миран»
-
Роботы Против Людей: Собираем Стул От Икеа
19 Oct, 24 -
Аркадный Волейбол. 40 Кб Удовольствия
19 Oct, 24 -
Дух Времени
19 Oct, 24 -
Кто Организаторы Совместных Pr-Мероприятий?
19 Oct, 24