我有多个 Angular 应用程序,它们都引用相同的 GraphQL API(使用 Apollo)并决定将 API 服务层迁移到共享库。作为该服务层的一部分,我正在导出一个名为“ApiValidator”的验证函数,用于触发表单中的错误状态。
它的工作方式是每当 API 遇到错误时,响应都会将其注册到 ErrorService 单例中的可观察对象。ApiValidator 函数返回一个承诺检查 ErrorService 是否有任何具有匹配“字段”属性的错误,如果是则返回错误。
当一切都在应用程序级别时,这非常有效,但是在迁移到库之后,验证器每次都会创建一个新的单例实例。这是因为我使用 Injector.create 和“useClass”来获取对验证器中服务的引用。但是,如果没有循环依赖,我无法让“useExisting”工作,并且在尝试通过包装服务中的构造函数使用 DI 时也遇到了同样的问题。
我不确定如何有效地设置它以提供关于 stackblitz 的工作示例或考虑到它跨私有库和应用程序的东西 - 如果有帮助,很乐意将代码发布在那里。现在,我已经在下面发布了所有相关部分。
非常感谢您提前提供任何见解或帮助!
图书馆代码
错误服务
@Injectable({
providedIn: 'root'
})
export class ErrorService {
/**
* An observable list of response errors returned from the graphql API
*/
private errors: BehaviorSubject<GraphQLResponseError[]> = new BehaviorSubject<GraphQLResponseError[]>([]);
/**
* Get any errors on the page
* @return An observable of the current errors on the page
*/
public getErrors(): Observable<GraphQLResponseError[]> {
return this.errors;
}
/**
* Get only field message errors for forms
* @return An observable of current field errors on the page
*/
public getFieldErrors(): Observable<GraphQLResponseError[]> {
return this.errors.pipe(map((error: GraphQLResponseError[]) => {
return error.filter((err: GraphQLResponseError) => err.context === 'field');
}));
}
/**
* Get only page message errors
* @return An observable of current page errors on the page
*/
public getPageErrors(): Observable<GraphQLResponseError[]> {
return this.errors.pipe(map((error: GraphQLResponseError[]) => {
return error.filter((err: GraphQLResponseError) => err.context === 'page');
}));
}
/**
* Records a response error in the list of errors
* @param error The error to add to the page
*/
public recordError(error: GraphQLResponseError): void {
this.errors.pipe(take(1)).subscribe(errors => {
if (!errors.includes(error)) {
errors.push(error);
this.errors.next(errors);
}
});
}
}
ApiValidator
import { Injector } from '@angular/core';
import { AbstractControl, ValidationErrors } from '@angular/forms';
import { GraphQLResponseError } from '../interfaces/graphql-response-error.interface';
import { take, map } from 'rxjs/operators';
import { ErrorService } from '../services/error.service';
/**
* Validator which triggers validations based on field errors recorded in the page service
* @param control The form control which should be validated against this validator
* @return A promise with the errors associated with the form or null if there are none
*/
export function ApiValidator(control: AbstractControl): Promise<ValidationErrors | null> {
const injector = Injector.create({
providers: [{ provide: ErrorService, useClass: ErrorService, deps: [] }]
});
const errorService = injector.get(ErrorService);
// get the name of the control to compare to any errors in the service
const controlName = (control.parent) ? Object.keys(control.parent.controls).find(name => control === control.parent.controls[name]) : null;
// return any errors that exist for the current control, or null if none do
return errorService.getFieldErrors().pipe(take(1), map((errors: GraphQLResponseError[]) => {
if (errors && errors.length > 0) {
const fieldErrors = errors.filter((error: GraphQLResponseError) => error.field === controlName);
return (fieldErrors.length > 0) ? { api: fieldErrors.map(error => error.message).join()} : null;
}
return null;
})).toPromise();
}
接口模块
import { HttpClientModule } from '@angular/common/http';
import { CommonModule, DatePipe } from '@angular/common';
import { NgModule, Optional, SkipSelf, ModuleWithProviders } from '@angular/core';
import { Apollo, ApolloBoost, ApolloBoostModule, ApolloModule } from 'apollo-angular-boost';
import { GraphQLService } from './services/graphql.service';
import { ApiValidator } from './validators/api.validator';
@NgModule({
declarations: [
],
imports: [
HttpClientModule,
CommonModule,
ApolloBoostModule,
ApolloModule
],
providers: [
DatePipe,
{
provide: GraphQLService,
useClass: GraphQLService,
deps: [Apollo, ApolloBoost, DatePipe]
}
]
})
export class ApiModule {
constructor(@Optional() @SkipSelf() parentModule?: ApiModule) {
if (parentModule) {
throw new Error(
'ApiModule is already loaded. Import it in the AppModule only');
}
}
}
GraphQLService(相关部分)
import { Injectable, Injector, Inject } from '@angular/core';
import { ApolloBoost, Apollo, gql, WatchQueryFetchPolicy } from 'apollo-angular-boost';
import { Observable, BehaviorSubject } from 'rxjs';
import { map, take } from 'rxjs/operators';
import { GraphQLResponse } from '../interfaces/graphql-response.interface';
import { GraphQLRefetchQuery } from '../interfaces/graphql-refetch-query.interface';
import { DatePipe } from '@angular/common';
import { GraphQLResponseError } from '../interfaces/graphql-response-error.interface';
import { AbstractControl, ValidationErrors } from '@angular/forms';
import { ErrorService } from './error.service';
@Injectable({
providedIn: 'root'
})
export class GraphQLService {
/**
* @var API_DATE_FORMAT The format to use for representing dates
*/
protected readonly API_DATE_FORMAT = 'yyyy-MM-dd HH:mm:ss';
/**
* @ignore
*/
constructor(
protected apollo: Apollo,
protected apolloBoost: ApolloBoost,
protected datePipe: DatePipe,
protected errorService: ErrorService
) {
}
/**
* Initializes the connection with the graphql API
* @param url The graphql API endpoint
* @param token The authorization token to use for requests
*/
public connect(url: string, token: string = null): void {
// set environment variables to app context
this.apolloBoost.create({
uri: url,
request: async (operation) => {
if (token !== null) {
operation.setContext({
headers: {
authorization: token
}
});
}
},
onError: ({ graphQLErrors, networkError }) => {
// if the gql request returned errors, register the errors with the page
if (graphQLErrors) {
graphQLErrors.forEach(error => {
// if the error has an extensions field, consider it a field-level error
if (error.hasOwnProperty('extensions') && error.extensions.hasOwnProperty('field')) {
this.errorService.recordError({
context: 'field',
message: error.message,
field: error.extensions.field
});
}
// else, consider a page level error
else {
this.errorService.recordError({
context: 'page',
message: error.message
});
}
});
}
// if there were network errors, register those with the page
// note, it doesn't seem to work correctly with true network errors
// and we catch these in the api.interceptor for now
if (networkError) {
this.errorService.recordError({
context: 'page',
message: networkError.message
});
}
}
});
}
}
申请代码
应用模块
import { HttpClientModule, HttpClient, HTTP_INTERCEPTORS } from '@angular/common/http';
import { CommonModule } from '@angular/common';
import { NgModule, APP_INITIALIZER, Injector } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule } from '@angular/router';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
import { CookieService } from 'ngx-cookie-service';
import { environment } from '../environments/environment';
import { GraphQLService, ApiModule } from '@angular/library';
import { initializeAuthentication, AppContextService, AuthInterceptor } from '@application/core';
import { SharedModule } from '@application/shared';
import { LayoutModule } from './layout/layout.module';
import { routes } from './app.routing';
import { AppComponent } from './app.component';
import { DashboardComponent } from './pages/dashboard/dashboard.component';
@NgModule({
declarations: [
AppComponent,
DashboardComponent
],
imports: [
RouterModule.forRoot(routes),
BrowserModule,
BrowserAnimationsModule,
HttpClientModule,
CommonModule,
SharedModule,
LayoutModule,
MatTooltipModule,
ApiModule
],
providers: [
{
provide: APP_INITIALIZER,
useFactory: (appContextService: AppContextService) => function() { appContextService.setEnvironmentVariables(environment); },
deps: [AppContextService],
multi: true
},
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
},
{
provide: APP_INITIALIZER,
useFactory: initializeAuthentication,
multi: true,
deps: [HttpClient, AppContextService, CookieService]
},
{
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
useValue: {
appearance: 'outline'
}
}
],
bootstrap: [AppComponent]
})
export class AppModule {
constructor(private gqlService: GraphQLService, private cookieService: CookieService) {
// if using authorization set token
const token = (environment.useAuthorization) ? this.cookieService.get('auth') : null;
// initialize connection with api
this.gqlService.connect(environment.apiUrl, token);
}
}
UserForm(相关部分)
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { FormGroup, FormBuilder, FormArray } from '@angular/forms';
import { take, finalize, catchError } from 'rxjs/operators';
import { ToastrService } from 'ngx-toastr';
import { User, GraphQLResponse, UserService, ApiValidator } from '@application/library';
import { PageMode, PageService } from '@application/core';
@Component({
selector: 'sm-user-form',
templateUrl: './user-form.component.html',
styleUrls: ['./user-form.component.scss']
})
export class UserFormComponent implements OnInit {
/**
* @ignore
*/
constructor(
private fb: FormBuilder,
private activatedRoute: ActivatedRoute,
private router: Router,
public pageService: PageService,
private userService: UserService,
private toastService: ToastrService
) {
// define form structure
this.form = this.fb.group({
email: ['', null, ApiValidator],
password: ['', null, ApiValidator],
prefix: ['', null, ApiValidator],
firstName: ['', null, ApiValidator],
middleName: ['', null, ApiValidator],
lastName: ['', null, ApiValidator]
});
}
}
UserFormTemplate(相关部分)
<div fxLayout="row">
<mat-form-field fxFlex="10">
<mat-label>Prefix</mat-label>
<input matInput formControlName="prefix">
<mat-error *ngIf="form.get('prefix').hasError('api')">{{form.get('prefix').getError('api')}}</mat-error>
</mat-form-field>
<mat-form-field fxFlex="30">
<mat-label>First Name</mat-label>
<input matInput formControlName="firstName">
<mat-error *ngIf="form.get('firstName').hasError('api')">{{form.get('firstName').getError('api')}}</mat-error>
</mat-form-field>
<mat-form-field fxFlex="20">
<mat-label>Middle Name</mat-label>
<input matInput formControlName="middleName">
<mat-error *ngIf="form.get('middleName').hasError('api')">{{form.get('middleName').getError('api')}}</mat-error>
</mat-form-field>
<mat-form-field fxFlex="30">
<mat-label>Last Name</mat-label>
<input matInput formControlName="lastName">
<mat-error *ngIf="form.get('lastName').hasError('api')">{{form.get('lastName').getError('api')}}</mat-error>
</mat-form-field>
<mat-form-field fxFlex="10">
<mat-label>Suffix</mat-label>
<input matInput formControlName="suffix">
<mat-error *ngIf="form.get('suffix').hasError('api')">{{form.get('suffix').getError('api')}}</mat-error>
</mat-form-field>
</div>