Angular HttpClient responseType和observe的坑人行为

现在是凌晨2点半。

我从晚上8点就遇到这个问题了,一直想不通,直到刚才查到https://github.com/angular/angular/issues/18586才发现被坑了一回。

问题是这样的,使用Angular的HttpClient发出get请求。文档规定的get方法有2个参数,一个是URL字符串,另外一个是options。在options中可以指定headers、observe、params、reportProgress、responseType、withCredentials。

很自然地就会想到如下方式:把options单独提出来定义

    const uri = `${this.config.uri}/${this.domain}`;
    const httpOptions = {
        headers: this.headers,
        responseType: 'json',
        params: (new HttpParams()).set('members_like', userId),
    };
    return this.http
      .get<Project[]>(uri, httpOptions);

可以指定后台返回的数据格式(responseType),比如json格式,text格式,还有blob格式及arraybuffer格式。

如上这种方式看起来是很正常的操作,不过烦人的是tslint报了一个error:

类型“{ headers: HttpHeaders; responseType: string; params: HttpParams; }”的参数不能赋给类型“{ headers?: HttpHeaders | { [header: string]: string | string[]; }; observe?: "body"; params?: HttpParams | { [param: string]: string | string[]; }; reportProgress?: boolean; responseType?: "json"; withCredentials?: boolean; }”的参数。
属性“responseType”的类型不兼容。
不能将类型“string”分配给类型“"json"”。 [2345]

很奇怪对不对?我也很奇怪,明明是正常操作,为什么会报error呢?get方法是支持把responseType设置成json的啊。

百思不得其解,点击get进入angular的代码,显示:

    get(url: string, options: {
        headers?: HttpHeaders | {
            [header: string]: string | string[];
        };
        observe?: 'body';
        params?: HttpParams | {
            [param: string]: string | string[];
        };
        reportProgress?: boolean;
        responseType: 'arraybuffer';
        withCredentials?: boolean;
    }): Observable<ArrayBuffer>;

这个更奇怪了,我明明定义的是json,为什么angular的get方法重载(get方法有15种重载)到arraybuffer呢?观察了好久猜测导航到的重载正好是15种重载中的第一个,是不是ts不能识别这个时候的重载规则所以默认导航到第一个重载了?

按照这个思路想下去,为什么ts不能识别这个时候的重载规则呢?是不是这个时候ts不知道httpOptions 的数据类型啊?

抱着试一试的态度(其实也不抱希望,毕竟连js都有类型推导,更何况ts呢),我给httpOptions 加了数据类型:

    const uri = `${this.config.uri}/${this.domain}`;
    const httpOptions: {
      headers?: HttpHeaders | {
        [header: string]: string | string[];
      };
      observe?: 'body';
      params?: HttpParams | {
          [param: string]: string | string[];
      };
      reportProgress?: boolean;
      responseType: 'json';
      withCredentials?: boolean;
    } = {
        headers: this.headers,
        responseType: 'json',
        params: (new HttpParams()).set('members_like', userId),
    };
    return this.http
      .get<Project[]>(uri, httpOptions);

httpOptions 的数据类型就是直接copy Angular的码源定义的,竟然不报Error了。。。。

结合之前报错的信息“{ headers: HttpHeaders; responseType: string; params: HttpParams; }”,就可以发现:

如果没有给出数据类型,ts会根据规则推导出responseType的数据类型是string,但是get方法的15种重载中,responseType的数据类型是"json" | "text" | "blob" | "arraybuffer"。

string类型显然不是"json" | "text" | "blob" | "arraybuffer"其中的一个,所以报了一个error,那是不是不做类型推导就可以了?为了验证这个猜想,我做了如下测试:

    const httpOptions: Object = {
        headers: this.headers,
        responseType: 'json',
        params: (new HttpParams()).set('members_like', userId),
    };
    return this.http
      .get<Project[]>(uri, httpOptions);

这样是可以的,考虑一下,数据类型为Object时,就不会再进行类型推导,也就不会报error了。

如果单独把responseType的赋值不提出了呢?

    const uri = `${this.config.uri}/${this.domain}`;
    const httpOptions = {
        headers: this.headers,
        params: (new HttpParams()).set('members_like', userId),
    };
    return this.http
      .get<Project[]>(uri, {...httpOptions, responseType: 'json'});

这样同样也是可以的。所以结论就是: 要么给ts正确的推导,要么不要让ts进行推导。

 

现在再记录一下上面提到的文章:

观点1:

const res = this.http.get(url, {responseType: 'text'});
const options = {responseType: 'text'};
const res = this.http.get(url, options);
是不同的,这是类型推导引起的,对于1:ts不会进行类型推导,对于2:ts把options类型推导为{responseType: string},这就造成了错误。

观点2:

 可以用下面的方法让ts有正确的推导:

    const uri = `${this.config.uri}/${this.domain}`;
    const httpOptions = {
        headers: this.headers,
        responseType: 'json' as 'json',
        params: (new HttpParams()).set('members_like', userId),
    };
    return this.http
      .get<Project[]>(uri, httpOptions);

  这样推导出来的就是{responseType: 'json'},就可以正常显示了。

观点3:

 当然,github上的各位小哥心中都有一个mmp,这个明明是Angular的bug,为什么从4.3版本更新到如今的7.1版本还是没有修改过来?

 为了抗议,有个小哥提出如下方法:

// define this namespace somewhere
export namespace ResponseType {
    export const JSON = 'json' as 'json';
    export const ArrayBuffer = 'arraybuffer' as 'arraybuffer';
    export const Blob = 'blob' as 'blob';
    export const Text = 'text' as 'text';
}
// import the namespace above and use it like this
const reqOpts = {
    params: params,
    headers: headers,
    responseType: ResponseType.JSON,
};

// no type error, the right signature is selected
const a = await this.http.get(url, reqOpts);
const b = await this.http.get<MyClass>(url, reqOpts);

我尝试了一下,是可以用的。如果只是写成这样,那也不足为奇,小哥本着反正是开源软件,就修改一下源代码吧,Angular的开发人员不修改bug,他就自己修改了。

You would need to first put the following in the declaration @angular/common/http/src/client.d.ts:
declare module '@angular/common/http' {
    export namespace HttpResponseType {
        export const JSON: 'json';
        export const ArrayBuffer: 'arraybuffer';
        export const Blob: 'blob';
        export const Text: 'text';
    }

    export declare type HttpObserve = 'body' | 'events' | 'response';
    export namespace HttpObserve {
        export const Body: 'body';
        export const Events: 'events';
        export const Response: 'response';
    }
}
Then implement it in angular/packages/common/http/src/:
export namespace HttpResponseType {
    export const JSON: 'json' = 'json';
    export const ArrayBuffer: 'arraybuffer' = 'arraybuffer';
    export const Blob: 'blob' = 'blob';
    export const Text: 'text' = 'text';
}

export type HttpObserve = 'body' | 'events' | 'response';
export namespace HttpObserve {
    export const Body: 'body' = 'body';
    export const Events: 'events' = 'events';
    export const Response: 'response' = 'response';
}

Finally the two namespaces should be properly exported all the way back to @angular/common/http.

我也尝试了一下,也是可以的,但是还是不建议这样修改啊,毕竟没有经过严格的单元测试,不知道会不会影响全局的代码啊。

 

总结一下:既然官方没有修改,开发者就要一直打补丁,建议使用如下的方式:

1:

    // 加数据类型
    const uri = `${this.config.uri}/${this.domain}`;
    const httpOptions: {
      headers?: HttpHeaders | {
        [header: string]: string | string[];
      };
      observe?: 'body';
      params?: HttpParams | {
          [param: string]: string | string[];
      };
      reportProgress?: boolean;
      responseType: 'json';
      withCredentials?: boolean;
    } = {
        headers: this.headers,
        responseType: 'json',
        params: (new HttpParams()).set('members_like', userId),
    };
    return this.http
      .get<Project[]>(uri, httpOptions);

2:

    // 把特殊的responseType和observe不要提出来
    const uri = `${this.config.uri}/${this.domain}`;
    const httpOptions = {
        headers: this.headers,
        params: (new HttpParams()).set('members_like', userId),
    };
    return this.http
      .get<Project[]>(uri, {...httpOptions, responseType: 'json'});

3:

// 强制正确类型推导    
    const uri = `${this.config.uri}/${this.domain}`;
    const httpOptions = {
        headers: this.headers,
        responseType: 'json' as 'json',
        params: (new HttpParams()).set('members_like', userId),
    };
    return this.http
      .get<Project[]>(uri, httpOptions);

4:企业级开发中为了大家使用的方便,建议把类型单独提取出来

 1 // httpOptions的observe,参见https://www.cnblogs.com/wangtingnoblog/p/10322483.html
 2 export namespace ObserveType {
 3   export const Body = 'body' as 'body';
 4   export const Response = 'response' as 'response';
 5   export const Events = 'events' as 'events';
 6 }
 7 // httpOptions的responseType,参见https://www.cnblogs.com/wangtingnoblog/p/10322483.html
 8 export namespace ResponseType {
 9   export const Json = 'json' as 'json';
10   export const Text = 'text' as 'text';
11   export const Blob = 'blob' as 'blob';
12   export const Arraybuffer = 'arraybuffer' as 'arraybuffer';
13 }

使用:

1     const httpOptions = {
2         headers: this.headers,
3         // 如何设置observe和responseType参见https://www.cnblogs.com/wangtingnoblog/p/10322483.html
4         observe: ObserveType.Response,
5         responseType: ResponseType.Json,
6         // 查询参数,会被解析成?members_like=1,可以给HttpParams传递参数HttpParamsOptions(支持数组,查询字符串),
7         // HttpParams是不可修改的,只能通过set返回新的HttpParams
8         params: (new HttpParams()).set('members_like', userId),
9     };

注意第4行 第5行

Over

修改: 2019-02-16 17:34:05

posted on 2019-01-26 04:23  西门本不吹雪  阅读(5188)  评论(1编辑  收藏  举报