Interacting efficiently with a RESTful service with Angular2 and RXJS (Part 3)

本文探讨了如何在Angular2应用中高效地与RESTful服务交互。通过展示框架提供的HTTP支持,介绍了请求拦截、错误处理、安全性及重试等通用功能的实现方式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

In the previous part of the article, we dealt with the way to manage data from a RESTful service and to link requests with form elements. We now tackle global issues like request interception, error handling, security, and retry.

Intercepting requests

The request interception feature isn’t provided out of the box by Angular2. That said, the framework allows you to override classes gotten from dependency injection. The Http class is responsible from leveraging the underlying XMLHttpObject object to execute HTTP requests.

This class is the one to extend to intercept request calls. The following snippet describes how to implement such class to detect events: before the request, after request, and on error. For this, we leverage the catch and finally operators of observables.

@Injectable()
export class CustomHttp extends Http {
  constructor(backend: ConnectionBackend, defaultOptions: RequestOptions) {
    super(backend, defaultOptions);
  }

  request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
    console.log('Before the request...');
    return super.request(url, options)
        .catch((err) => {
          console.log('On received an error...');
          return Observable.throw(err);
        })
        .finally(() => {
          console.log(After the request...');
        });
  }

  get(url: string, options?: RequestOptionsArgs): Observable<Response> {
    console.log('Before the request...');
    return super.get(url, options)
        .catch((err) => {
          console.log('On received an error...');
          return Observable.throw(err);
        })
        .finally(() => {
          console.log(After the request...');
        });
  }
}

Now we implemented the CustomHttp class, we need to configure its provider when bootstrapping our application. This can be done using the provide function with the useFactoryattribute to keep the hand on the way to instantiate it and provide it the right dependencies.

bootstrap(AppComponent, [
  HTTP_PROVIDERS,
  provide(Http, {
    useFactory: (backend: XHRBackend, defaultOptions: RequestOptions) => {
      return new CustomHttp(backend, defaultOptions);
    },
    deps: [ XHRBackend, RequestOptions ]
  })
]);

Be careful to configure the provider of the CustomHttp class after specifying HTTP_PROVIDERS.

This class will be used in the following as foundations to implement generic features like error handling, security and retries under the hood and without any updates in parts of the application that uses the http object. Let’s start by error handling.

Handling errors

Sure we could handle errors for each HTTP call but this could be inconvenient. A better approach consists of leveraging our CustomHttp class. Using the catch operator we can intercept errors. We need then to notify the other parts of the application. For example, the component that is responsible for displaying them.

For this, we need to create a dedicated service ErrorNotifierService that will include an observable and its associated observer. Based on this service, we will be able to notify when errors occur and to be notified.

export class ErrorNotifierService {
  private errorObservable:Observable<any>;
  private errorObserver:Observer<any>;

  constructor() {
    this.errorObservable = Observable.create((observer:Observer) => {
      this.errorObserver = observer;
    }).share();
  }

  notifyError(error:any) {
    this.errorObserver.next(error);
  }

  onError(callback:(err:any) => void) {
    this.errorObservable.subscribe(callback);
  }
}

Be careful to register this service when bootstrapping your application to make it shared by the whole application.

bootstrap(AppComponent, [
  (...)
  ErrorNotifierService
]);

This can be injected in the CustomHttp class. When an error is caught, the notifyError method can be called. All components that registered a callback using the onError method, we will be notified and display messages accordingly.

@Injectable()
export class CustomHttp extends Http {
  constructor(backend: ConnectionBackend,
            defaultOptions: RequestOptions,
            private errorService:ErrorNotifierService) {
    super(backend, defaultOptions);
  }

  request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
    console.log('Before the request...');
    return super.request(url, options)
        .catch((err) => {
          this.errorService.notifyError(err);
        })
        .finally(() => {
          console.log(After the request...');
        });
  }

  (...)
}

Don’t forget to update the way we configured our CustomHttp class for dependency injection:

bootstrap(AppComponent, [
  HTTP_PROVIDERS,
  provide(Http, {
    useFactory: (backend: XHRBackend, defaultOptions: RequestOptions,
                         errorService:ErrorNotifierService) => {
      return new CustomHttp(backend, defaultOptions, errorService);
    },
    deps: [ XHRBackend, RequestOptions, ErrorNotifierService ]
  }),
  ErrorNotifierService
]);

This approach is a bit too global. In some cases, we don’t need to intercept the error but let the error be handled by the code that triggers the request. It’s particularly the case for asynchronous validators of forms and partially when submitting forms. In fact, it’s rather linked to the HTTP status code of the response. The 400 and 422 ones must be handled separately to add some user-friendly messages within forms. For example along with a specific field.

In this case, we need to throw the intercepted error, as described below:

@Injectable()
export class CustomHttp extends Http {
  (...)

  request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
    console.log('Before the request...');
    return super.request(url, options)
        .catch((err) => {
          if (err.status === 400 || err.status === 422) {
            return Observable.throw(err);
          } else {
            this.errorService.notifyError(err);
          }
        })
        .finally(() => {
          console.log(After the request...');
        });
  }

  (...)
}

Handling security

As you can see, we didn’t handle security when executing HTTP requests. That said we don’t want to impact all this code by adding it. Using the class that extends the Http one, we will be able to such processing under the hood without any impact on the existing code.

There are several parts where security can apply especially if the routing feature of Angular is used. For example, we could extend the RouterOutlet directive to check if the user is authenticated before activating secured routes. We could also extend the RouterLink one to check the current roles of the user to display or hide links to secure routes. All these aspects would contribute to improving the user experience.

In this section, we will only focus on securing HTTP calls transparently, i.e. with no impact on the existing code that uses the Http class. We will of course leverage the request interception mechanism previously described.

We can even go further by displaying a dialog to invite the user to fill his credentials. Under the hood, it can be basic authentication or token-based security with tokens that need to be revalidated automatically using a refresh token.

For the basic authentication, it only corresponds to setting the Authorization header based on the credentials the user provides when authenticating. In the case, using the merge method of the request options is enough:

merge(options?:RequestOptionsArgs):RequestOptions {
  (...)
  var credentials = this.securityService.getCredentials();
  if (credentials) {
    let headers = options.headers | {};
    headers['Authorization] = 'Basic ' +
          btoa(credentials.username + ':' + credentials.password);
    options.headers = headers;
  }
  return super.merge(options);
}

In the case of tokens, this must be done within our custom Http class since we need to eventually chain requests using observables and their operators. If we detected that the current token expired, we need to execute the request to refresh this token. When we receive the response, we need to return the observable for the initial request using the flatMap operator or throw an error.

request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
  if (this.securityService.hasTokenExpired()) {
    return this.securityService
             .refreshAuthenticationObservable()
             .flatMap((authenticationResult:AuthenticationResult) => {
               if (authenticationResult.authenticated) {
                 this.securityService.setAuthorizationHeader(request.headers);
                 return super.request(url, request);
               } else {
                 return Observable.throw(new Error('Can't refresh the token'));
               }
    });
  } else {
    return super.request(url, options);
  }
}

The last thing missing to our application is the ability retry requests in failure before returning the failure to the application.

Retry support

Before considering a request as failed, we could consider implementing a retry mechanism. This feature is supported out of the box by observables thanks to the retry operator. Let’s add some retries when calling an HTTP request, for example, within the getBookKinds method.

getBookKinds(): Observable<BookKind[]> {
  return this.http.get('/bookkinds')
                  .retry(4)
                  .map(res => res.map());
}

This approach is much too basic since retries are immediately executed without waiting for a delay. A better one consists of waiting for a bit before retrying and abort after a given amount of time. Observables allow to mix retryWhendelay and timeout operators to achieve this, as described in the following snippet:

getBookKinds(): Observable<BookKind[]> {
  return this.http.get('/bookkinds')
                  .retryWhen(error => error.delay(500))
                  .timeout(2000, new Error('delay exceeded'))
                  .map(res => res.map());
}

Because such a feature is generic, we can implement it within our CustomHttp class as described below:

@Injectable()
export class CustomHttp extends Http {
  (...)

  request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
    return super.request(url, options)
        .retryWhen(error => error.delay(500))
        .timeout(2000, new Error('delay exceeded'));
  }

  (...)
}

You simplify the code above for clarity by removing processing for security and handling previously described.

Conclusion

In this article, we discussed how to interact with a RESTful service from an Angular 2 application. We deal with the HTTP support provided by the framework and show how Reactive Programming can fit at this level and what it can provide.

We also describe how to implement concrete use cases to control the way requests are executed, to get data from several requests, error handling, security, and retries. All these features contribute to making the application most robust.

We made an effort on the design to leverage the best features and mechanisms of Angular 2. The idea is to add global features with the least impact on the existing code of the application.

The source code is available in the following Github repository: https://github.com/restlet/restlet-samples-angular2-rxjs.

原文链接: http://restlet.com/blog/2016/04/18/interacting-efficiently-with-a-restful-service-with-angular2-and-rxjs-part-3/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值