基于angular httpclient转码遇到的'+'不转译的问题我们虽然已经解决了,但是我们还不知道原因。我们自己去调用encodeURLComponent方法的时候 + 是被编码成%2B的,带着疑惑我们去看angular的源码
// client.ts
get(url: string, options: {
headers?: HttpHeaders|{[header: string]: string | string[]},
context?: HttpContext,
observe?: 'body'|'events'|'response',
params?: HttpParams|
{[param: string]: string | number | boolean | ReadonlyArray<string|number|boolean>},
reportProgress?: boolean,
responseType?: 'arraybuffer'|'blob'|'json'|'text',
withCredentials?: boolean,
} = {}): Observable<any> {
return this.request<any>('GET', url, options as any);
}
request(first: string|HttpRequest<any>, url?: string, options: {
body?: any,
headers?: HttpHeaders|{[header: string]: string | string[]},
context?: HttpContext,
observe?: 'body'|'events'|'response',
params?: HttpParams|
{[param: string]: string | number | boolean | ReadonlyArray<string|number|boolean>},
reportProgress?: boolean,
responseType?: 'arraybuffer'|'blob'|'json'|'text',
withCredentials?: boolean,
} = {}): Observable<any> {
let req: HttpRequest<any>;
// First, check whether the primary argument is an instance of `HttpRequest`.
if (first instanceof HttpRequest) {
// It is. The other arguments must be undefined (per the signatures) and can be
// ignored.
req = first;
} else {
// It's a string, so it represents a URL. Construct a request based on it,
// and incorporate the remaining arguments (assuming `GET` unless a method is
// provided.
// Figure out the headers.
let headers: HttpHeaders|undefined = undefined;
if (options.headers instanceof HttpHeaders) {
headers = options.headers;
} else {
headers = new HttpHeaders(options.headers);
}
// Sort out parameters.
let params: HttpParams|undefined = undefined;
if (!!options.params) {
if (options.params instanceof HttpParams) {
params = options.params;
} else {
params = new HttpParams({fromObject: options.params} as HttpParamsOptions);
}
}
// Construct the request.
req = new HttpRequest(first, url!, (options.body !== undefined ? options.body : null), {
headers,
context: options.context,
params,
reportProgress: options.reportProgress,
// By default, JSON is assumed to be returned for all calls.
responseType: options.responseType || 'json',
withCredentials: options.withCredentials,
});
}
截取了片段的angular请求封装代码,我们看到params会被构造成一个HttpParams的实例,我们找到HttpParams类
//params.ts
export class HttpParams {
private map: Map<string, string[]>|null;
private encoder: HttpParameterCodec;
private updates: Update[]|null = null;
private cloneFrom: HttpParams|null = null;
constructor(options: HttpParamsOptions = {} as HttpParamsOptions) {
this.encoder = options.encoder || new HttpUrlEncodingCodec();
if (!!options.fromString) {
if (!!options.fromObject) {
throw new Error(`Cannot specify both fromString and fromObject.`);
}
this.map = paramParser(options.fromString, this.encoder);
} else if (!!options.fromObject) {
this.map = new Map<string, string[]>();
Object.keys(options.fromObject).forEach(key => {
const value = (options.fromObject as any)[key];
// convert the values to strings
const values = Array.isArray(value) ? value.map(valueToString) : [valueToString(value)];
this.map!.set(key, values);
});
} else {
this.map = null;
}
}
}
参数生成后接着实例化请求,我们看到请求是个HttpRequest实例,我们继续往下走,找到HttpRequest类
export class HttpRequest<T> {
constructor(
method: string, readonly url: string, third?: T|{
headers?: HttpHeaders,
context?: HttpContext,
reportProgress?: boolean,
params?: HttpParams,
responseType?: 'arraybuffer'|'blob'|'json'|'text',
withCredentials?: boolean,
}|null,
fourth?: {
headers?: HttpHeaders,
context?: HttpContext,
reportProgress?: boolean,
params?: HttpParams,
responseType?: 'arraybuffer'|'blob'|'json'|'text',
withCredentials?: boolean,
}) {
this.method = method.toUpperCase();
// Next, need to figure out which argument holds the HttpRequestInit
// options, if any.
let options: HttpRequestInit|undefined;
// Check whether a body argument is expected. The only valid way to omit
// the body argument is to use a known no-body method like GET.
if (mightHaveBody(this.method) || !!fourth) {
// Body is the third argument, options are the fourth.
this.body = (third !== undefined) ? third as T : null;
options = fourth;
} else {
// No body required, options are the third argument. The body stays null.
options = third as HttpRequestInit;
}
// If options have been passed, interpret them.
if (options) {
// Normalize reportProgress and withCredentials.
this.reportProgress = !!options.reportProgress;
this.withCredentials = !!options.withCredentials;
// Override default response type of 'json' if one is provided.
if (!!options.responseType) {
this.responseType = options.responseType;
}
// Override headers if they're provided.
if (!!options.headers) {
this.headers = options.headers;
}
if (!!options.context) {
this.context = options.context;
}
if (!!options.params) {
this.params = options.params;
}
}
// If no headers have been passed in, construct a new HttpHeaders instance.
if (!this.headers) {
this.headers = new HttpHeaders();
}
// If no context have been passed in, construct a new HttpContext instance.
if (!this.context) {
this.context = new HttpContext();
}
// If no parameters have been passed in, construct a new HttpUrlEncodedParams instance.
if (!this.params) {
this.params = new HttpParams();
this.urlWithParams = url;
} else {
// Encode the parameters to a string in preparation for inclusion in the URL.
const params = this.params.toString();
if (params.length === 0) {
// No parameters, the visible URL is just the URL given at creation time.
this.urlWithParams = url;
} else {
// Does the URL already have query parameters? Look for '?'.
const qIdx = url.indexOf('?');
// There are 3 cases to handle:
// 1) No existing parameters -> append '?' followed by params.
// 2) '?' exists and is followed by existing query string ->
// append '&' followed by params.
// 3) '?' exists at the end of the url -> append params directly.
// This basically amounts to determining the character, if any, with
// which to join the URL and parameters.
const sep: string = qIdx === -1 ? '?' : (qIdx < url.length - 1 ? '&' : '');
this.urlWithParams = url + sep + params;
}
}
}
}
老样子摘取片段
我们看到有
const params = this.params.toString();
if (params.length === 0) {
this.urlWithParams = url;
} else {
this.urlWithParams = url + sep + params;
}
这个片段 ,上面我们已经知道了params是个HttpParams类,我们找到他的toString方法
toString(): string {
this.init();
return this.keys()
.map(key => {
const eKey = this.encoder.encodeKey(key);
// `a: ['1']` produces `'a=1'`
// `b: []` produces `''`
// `c: ['1', '2']` produces `'c=1&c=2'`
return this.map!.get(key)!.map(value => eKey + '=' + this.encoder.encodeValue(value))
.join('&');
})
// filter out empty values because `b: []` produces `''`
// which results in `a=1&&c=1&c=2` instead of `a=1&c=1&c=2` if we don't
.filter(param => param !== '')
.join('&');
}
export class HttpUrlEncodingCodec implements HttpParameterCodec {
/**
* Encodes a key name for a URL parameter or query-string.
* @param key The key name.
* @returns The encoded key name.
*/
encodeKey(key: string): string {
return standardEncoding(key);
}
/**
* Encodes the value of a URL parameter or query-string.
* @param value The value.
* @returns The encoded value.
*/
encodeValue(value: string): string {
return standardEncoding(value);
}
/**
* Decodes an encoded URL parameter or query-string key.
* @param key The encoded key name.
* @returns The decoded key name.
*/
decodeKey(key: string): string {
return decodeURIComponent(key);
}
/**
* Decodes an encoded URL parameter or query-string value.
* @param value The encoded value.
* @returns The decoded value.
*/
decodeValue(value: string) {
return decodeURIComponent(value);
}
}
const STANDARD_ENCODING_REGEX = /%(\d[a-f0-9])/gi;
const STANDARD_ENCODING_REPLACEMENTS: {[x: string]: string} = {
'40': '@',
'3A': ':',
'24': '$',
'2C': ',',
'3B': ';',
'3D': '=',
'3F': '?',
'2F': '/',
};
function standardEncoding(v: string): string {
return encodeURIComponent(v).replace(
STANDARD_ENCODING_REGEX, (s, t) => STANDARD_ENCODING_REPLACEMENTS[t] ?? s);
}
可以看到 toString方法是对字符串做了处理的,调用encoder的encodeValue方法对值进行转码,这个转码我们可以看到是有限制的,一些特殊字符不参与转码。
可是这边也没有说到'+'是不会被转码的,为什么项目中的'+'没有被转码。
直接说结论:版本问题,开头提到过这个是最新版本的angular,但是在两年之前的版本,+是被特殊处理的,如下图
所以我们在项目不升级版本的情况下,要处理这个问题就要仿造他的改动进行改动,基于前面文章的基础,我们把自定义类改造成这样
class CustomHttpUrlEncodingCodec implements HttpParameterCodec {
encodeKey(key: string): string {
return standardEncoding(key);
}
encodeValue(value: string): string {
return standardEncoding(value);
}
decodeKey(key: string): string {
return decodeURIComponent(key);
}
decodeValue(value: string): string {
return decodeURIComponent(value);
}
}
const STANDARD_ENCODING_REGEX = /%(\d[a-f0-9])/gi;
const STANDARD_ENCODING_REPLACEMENTS: {[x: string]: string} = {
'40': '@',
'3A': ':',
'24': '$',
'2C': ',',
'3B': ';',
'3D': '=',
'3F': '?',
'2F': '/',
};
function standardEncoding(v: string): string {
return encodeURIComponent(v).replace(
STANDARD_ENCODING_REGEX, (s, t) => STANDARD_ENCODING_REPLACEMENTS[t] ?? s);
}