diff --git a/src/types.ts b/src/types.ts index a06b773..3045cdc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -25,3 +25,14 @@ export type UniNetworkRequestWithoutCallback = Omit< > & Omit & Omit + +/** + * 序列化选项 + */ +export interface SerializeOptions { + /** + * 如果设置为 true,对象中的数组值将被连接成一个逗号分隔的字符串。 + * @default false + */ + asStrings?: boolean +} diff --git a/src/utils.ts b/src/utils.ts index ea35178..73bed9c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -12,6 +12,7 @@ import { AxiosHeaders } from 'axios' import type { MethodType, ResolvedOptions, + SerializeOptions, UniNetworkRequestWithoutCallback, UserOptions, } from './types' @@ -43,7 +44,7 @@ export function resolveUniAppRequestOptions(config: AxiosRequestConfig, _options const { headers, baseURL, ...requestConfig } = config - const requestHeaders = AxiosHeaders.from(headers as any).normalize(false) + const requestHeaders = AxiosHeaders.from(serializeObject(headers)).normalize(false) if (config.auth) { const username = config.auth.username || '' @@ -114,3 +115,98 @@ export function progressEventReducer(listener: (progressEvent: AxiosProgressEven listener(data) } } + +/** + * ### 对象序列化 + * - 将一个对象序列化为一个纯净的、类似JSON的新对象。 + * - 此过程会过滤掉值为 null、undefined 或 false 的属性。 + * + * @link https://github.com/axios/axios/blob/ef36347fb559383b04c755b07f1a8d11897fab7f/lib/core/AxiosHeaders.js#L238-L246 + * @param {Record | null | undefined} sourceObj 要进行序列化的源对象。 + * @param {SerializeOptions} [options] 序列化选项。 + * @returns {Record} 返回一个新的、纯净的 JavaScript 对象。 + */ +export function serializeObject( + sourceObj: Record | null | undefined, + options?: SerializeOptions, +): Record { + const resultObj: Record = Object.create(null) + + if (!sourceObj) + return resultObj + + const { asStrings = false } = options || {} + + forEach(sourceObj, (value, key) => { + if (value != null && value !== false) { + // 如果 asStrings 为 true 且值是数组,则拼接成字符串,否则直接使用原值 + resultObj[key] = asStrings && Array.isArray(value) ? value.join(', ') : value + } + }) + + return resultObj +} + +/** + * Iterates over an Array, invoking a function for each item. + * + * @param {T[]} obj The array to iterate over. + * @param {(value: T, index: number, array: T[]) => void} fn The callback to invoke for each item. + * @param {{allOwnKeys?: boolean}} [options] Optional options. + */ +export function forEach(obj: T[], fn: (value: T, index: number, array: T[]) => void, options?: { allOwnKeys?: boolean }): void + +/** + * Iterates over an Object, invoking a function for each item. + * + * @param {T} obj The object to iterate over. + * @param {(value: T[keyof T], key: keyof T, object: T) => void} fn The callback to invoke for each property. + * @param {{allOwnKeys?: boolean}} [options] Optional options. + */ +export function forEach(obj: T, fn: (value: T[keyof T], key: keyof T, object: T) => void, options?: { allOwnKeys?: boolean }): void + +/** + * Iterate over an Array or an Object invoking a function for each item. + * + * If `obj` is an Array callback will be called passing + * the value, index, and complete array for each item. + * + * If 'obj' is an Object callback will be called passing + * the value, key, and complete object for each property. + * + * @link https://github.com/axios/axios/blob/v1.x/lib/utils.js#L240 + * + * @param {object | Array} obj The object to iterate + * @param {Function} fn The callback to invoke for each item + * @param {object} [options] + * @param {boolean} [options.allOwnKeys] + */ +export function forEach(obj: any, fn: Function, { allOwnKeys = false }: { allOwnKeys?: boolean } = {}): void { + // Don't bother if no value provided + if (obj === null || typeof obj === 'undefined') + return + + // Force an array if not already something iterable + if (typeof obj !== 'object') + obj = [obj] + + if (Array.isArray(obj)) { + // Iterate over array values + for (let i = 0, l = obj.length; i < l; i++) { + // fn(value, index, array) + fn(obj[i], i, obj) + } + } + else { + // Iterate over object keys + const keys = allOwnKeys ? Object.getOwnPropertyNames(obj) : Object.keys(obj) + const len = keys.length + let key + + for (let i = 0; i < len; i++) { + key = keys[i] + // fn(value, key, object) + fn(obj[key], key, obj) + } + } +} diff --git a/test/utils.test.ts b/test/utils.test.ts index 3c4e6ae..d628c39 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from 'vitest' -import { getMethodType, resolveUniAppRequestOptions } from '../src/utils' +import { describe, expect, it, vi } from 'vitest' +import { forEach, getMethodType, resolveUniAppRequestOptions, serializeObject } from '../src/utils' describe('getMethodType', () => { it('request', () => { @@ -64,3 +64,132 @@ describe('resolveUniAppRequestOptions', () => { `) }) }) + +describe('serializeObject', () => { + it('should filter out null, undefined, and false values', () => { + const obj = { + a: 1, + b: 'hello', + c: true, + d: null, + e: undefined, + f: false, + g: 0, + h: '', + } + expect(serializeObject(obj)).toEqual({ + a: 1, + b: 'hello', + c: true, + g: 0, + h: '', + }) + }) + + it('should handle null or undefined input by returning an empty object', () => { + expect(serializeObject(null)).toEqual({}) + expect(serializeObject(undefined)).toEqual({}) + }) + + it('should handle an empty object input', () => { + expect(serializeObject({})).toEqual({}) + }) + + it('should convert array values to strings if asStrings is true', () => { + const obj = { + arr: ['a', 'b', 3], + str: 'string', + } + const result = serializeObject(obj, { asStrings: true }) + expect(result).toEqual({ + arr: 'a, b, 3', + str: 'string', + }) + }) + + it('should keep array values as arrays if asStrings is false or not provided', () => { + const obj = { + arr: ['a', 'b', 3], + } + // asStrings 未提供 + expect(serializeObject(obj)).toEqual({ + arr: ['a', 'b', 3], + }) + // asStrings 为 false + expect(serializeObject(obj, { asStrings: false })).toEqual({ + arr: ['a', 'b', 3], + }) + }) + + it('should return an object that does not inherit from Object.prototype', () => { + const result = serializeObject({ a: 1 }) + // 验证对象的原型是 null => Object.create(null) 纯净的对象 + expect(Object.getPrototypeOf(result)).toBeNull() + }) +}) + +describe('forEach', () => { + it('should iterate over an array, providing value, index, and array to the callback', () => { + const arr = ['a', 1, true] + const callback = vi.fn() + + forEach(arr, callback) + + expect(callback).toHaveBeenCalledTimes(3) + expect(callback).toHaveBeenCalledWith('a', 0, arr) + expect(callback).toHaveBeenCalledWith(1, 1, arr) + expect(callback).toHaveBeenCalledWith(true, 2, arr) + }) + + it('should iterate over an object\'s own enumerable properties', () => { + const obj = { a: 1, b: 2 } + // 添加一个不可枚举的属性 + Object.defineProperty(obj, 'c', { value: 3, enumerable: false }) + // 添加一个原型链上的属性 + Object.setPrototypeOf(obj, { d: 4 }) + + const callback = vi.fn() + forEach(obj, callback) + + expect(callback).toHaveBeenCalledTimes(2) // 只应调用 2 次 + expect(callback).toHaveBeenCalledWith(1, 'a', obj) + expect(callback).toHaveBeenCalledWith(2, 'b', obj) + expect(callback).not.toHaveBeenCalledWith(3, 'c', obj) // 不应包含不可枚举的属性 + expect(callback).not.toHaveBeenCalledWith(4, 'd', obj) // 不应包含原型链上的属性 + }) + + it('should iterate over all own properties when allOwnKeys is true', () => { + const obj = { a: 1 } + Object.defineProperty(obj, 'b', { value: 2, enumerable: false }) + const callback = vi.fn() + + forEach(obj, callback, { allOwnKeys: true }) + + expect(callback).toHaveBeenCalledTimes(2) + expect(callback).toHaveBeenCalledWith(1, 'a', obj) + expect(callback).toHaveBeenCalledWith(2, 'b', obj) + }) + + it('should handle non-object values by wrapping them in an array', () => { + const callback = vi.fn() + + forEach(123, callback) + expect(callback).toHaveBeenCalledOnce() + expect(callback).toHaveBeenCalledWith(123, 0, [123]) + + callback.mockClear() + + forEach('test', callback) + expect(callback).toHaveBeenCalledOnce() + expect(callback).toHaveBeenCalledWith('test', 0, ['test']) + }) + + it('should not call the callback for null or undefined input', () => { + const callback = vi.fn() + + forEach(null, callback) + forEach(undefined, callback) + + expect(callback).not.toHaveBeenCalled() + }) +})