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