30

我目前正在切换到 Angular 4.3 的新 HttpClient。一个优点是我可以在 GET 方法上指定类型信息,并且返回的 JSON 被解析为给定的类型,例如

this.http.get<Person> (url).subscribe(...)

但不幸的是,JSON 中的所有日期都被解析为结果对象中的数字(可能是因为 Java Date 对象在后端被序列化为数字)。

使用旧的 Http 我在调用 JSON.parse() 时使用了 reviver 函数,如下所示:

this.http.get(url)
  .map(response => JSON.parse(response.text(), this.reviver))

在 reviver 函数中,我从数字创建了日期对象:

reviver (key, value): any {
  if (value !== null && (key === 'created' || key === 'modified'))
    return new Date(value);

  return value;
}

新的 HttpClient 是否有类似的机制?或者在解析 JSON 时进行转换的最佳做法是什么?

4

5 回答 5

28

这对我有用:

import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse, HttpResponse } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class ApiInterceptor implements HttpInterceptor {
  private dateRegex = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)$/;

  private utcDateRegex = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/;

  constructor() { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(request)
      .do((event: HttpEvent<any>) => {
        if (event instanceof HttpResponse) {
          this.convertDates(event.body);
        }
      });
  }

  private convertDates(object: Object) {
    if (!object || !(object instanceof Object)) {
      return;
    }

    if (object instanceof Array) {
      for (const item of object) {
        this.convertDates(item);
      }
    }

    for (const key of Object.keys(object)) {
      const value = object[key];

      if (value instanceof Array) {
        for (const item of value) {
          this.convertDates(item);
        }
      }

      if (value instanceof Object) {
        this.convertDates(value);
      }

      if (typeof value === 'string' && this.dateRegex.test(value)) {
        object[key] = new Date(value);
      }
    }
  }
}

与bygrace的答案相比,这样做的优点是您不需要自己解析为 json,您只需在 angular 完成解析后转换日期即可。

这也适用于数组和嵌套对象。我修改它应该支持数组。

于 2018-03-14T14:11:59.980 回答
24

不幸的是,似乎没有办法将 reviver 传递给 Angular HttpClient 中使用的 JSON.parse 方法。这是他们调用 JSON.parse 的源代码: https ://github.com/angular/angular/blob/20e1cc049fa632e88dfeb00476455a41b7f42338/packages/common/http/src/xhr.ts#L189

如果响应类型设置为“json”,Angular 只会解析响应(您可以在第 183 行看到这一点)。如果您不指定响应类型,则 Angular 默认为“json”(https://github.com/angular/angular/blob/20e1cc049fa632e88dfeb00476455a41b7f42338/packages/common/http/src/request.ts#L112)。

因此,您可以使用“文本”的响应类型并自己解析 json。或者你可能只是懒惰并序列化然后反序列化响应(JSON.parse(JSON.stringify(res.body), reviver))。

您可以创建一个拦截器,而不是修改每个调用,如下所示:

json-interceptor.ts

import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpResponse, HttpErrorResponse } from '@angular/common/http';
import { Observable } from 'rxjs/Rx';
import 'rxjs/add/operator/map';

// https://github.com/angular/angular/blob/master/packages/common/http/src/xhr.ts#L18
const XSSI_PREFIX = /^\)\]\}',?\n/;

/**
 * Provide custom json parsing capabilities for api requests.
 * @export
 * @class JsonInterceptor
 */
@Injectable()
export class JsonInterceptor implements HttpInterceptor {

  /**
   * Custom http request interceptor
   * @public
   * @param {HttpRequest<any>} req
   * @param {HttpHandler} next
   * @returns {Observable<HttpEvent<any>>}
   * @memberof JsonInterceptor
   */
  public intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    if (req.responseType !== 'json') {
      return next.handle(req);
    }
    // convert to responseType of text to skip angular parsing
    req = req.clone({
      responseType: 'text'
    });

    return next.handle(req).map(event => {
      // Pass through everything except for the final response.
      if (!(event instanceof HttpResponse)) {
        return event;
      }
      return this.processJsonResponse(event);
    });
  }

  /**
   * Parse the json body using custom revivers.
   * @private
   * @param {HttpResponse<string>} res
   * @returns{HttpResponse<any>}
   * @memberof JsonInterceptor
   */
  private processJsonResponse(res: HttpResponse<string>): HttpResponse<any> {
      let body = res.body;
      if (typeof body === 'string') {
        const originalBody = body;
        body = body.replace(XSSI_PREFIX, '');
        try {
          body = body !== '' ? JSON.parse(body, (key: any, value: any) => this.reviveUtcDate(key, value)) : null;
        } catch (error) {
          // match https://github.com/angular/angular/blob/master/packages/common/http/src/xhr.ts#L221
          throw new HttpErrorResponse({
            error: { error, text: originalBody },
            headers: res.headers,
            status: res.status,
            statusText: res.statusText,
            url: res.url || undefined,
          });
        }
      }
      return res.clone({ body });
  }

  /**
   * Detect a date string and convert it to a date object.
   * @private
   * @param {*} key json property key.
   * @param {*} value json property value.
   * @returns {*} original value or the parsed date.
   * @memberof JsonInterceptor
   */
  private reviveUtcDate(key: any, value: any): any {
      if (typeof value !== 'string') {
          return value;
      }
      if (value === '0001-01-01T00:00:00') {
          return null;
      }
      const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
      if (!match) {
          return value;
      }
      return new Date(value);
  }
}

然后你必须在你的模块中提供它:

*.module.ts

import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { JsonInterceptor } from '...';

@NgModule({
    providers: [
        {
            provide: HTTP_INTERCEPTORS,
            useClass: JsonInterceptor,
            multi: true
        }
    ]
})
...

在示例中,我尝试尽可能多地模仿 angular 是如何进行解析的。他们似乎没有导出 HttpJsonParseError 所以我无法将错误转换为该类型。它可能并不完美,但我希望它能传达这个想法。

这是一个正在运行的示例(在控制台中查看解析的日期):https ://stackblitz.com/edit/json-interceptor

我在这里创建了一个功能请求: https ://github.com/angular/angular/issues/21079

于 2017-12-18T16:18:33.197 回答
9

类似于Jonas Stensved 的回答,但使用管道:

import { map } from "rxjs/operators";

this.http.get(url)
  .pipe(
    map(response => {
      response.mydate = new Date(response.mydate);
      return response;
    })

请注意运算符的不同导入语法map

管道是在 RxJS 5.5 中引入的。它们有助于导入处理、代码可读性并减小包大小。请参阅了解运算符导入

于 2018-05-03T07:55:47.017 回答
7

你仍然可以,但你需要map()像这样从 rxjs 导入 -operator:

import 'rxjs/add/operator/map';

然后,正如 Diego 指出的那样,您可以使用map以下代码:

return this.http.get<BlogPost>(url)
.map(x => {
x.published = new Date(String(x.published));
    return x;
})
[...]
于 2018-01-18T14:41:57.120 回答
3

您可以使用:

this.http.get(url, { responseType: 'text' })
    .map(r => JSON.parse(r, this.reviver))
    .subscribe(...)

rxjs 6+ 的更新

this.http.get(url, { responseType: 'text' })
    .pipe(map(r => JSON.parse(r, this.reviver)))
    .subscribe(...)
于 2017-10-24T13:51:51.807 回答