From 6cbf6e14b943a844628ec90400bfffa5ccc7d915 Mon Sep 17 00:00:00 2001 From: kaikaibenkai Date: Mon, 8 Jan 2024 11:29:09 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20bigint=20=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E9=94=99=E8=AF=AF=EF=BC=9B=E6=B7=BB=E5=8A=A0=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/axios.spec.ts | 25 ++++++++++++++++ package.json | 1 + pnpm-lock.yaml | 8 ++++++ src/axios.ts | 64 ++++++++++++++++++++++++++++------------- 4 files changed, 78 insertions(+), 20 deletions(-) create mode 100644 __tests__/axios.spec.ts diff --git a/__tests__/axios.spec.ts b/__tests__/axios.spec.ts new file mode 100644 index 0000000..b12285c --- /dev/null +++ b/__tests__/axios.spec.ts @@ -0,0 +1,25 @@ +import { createAxios } from '../src/axios'; + +describe('请求非 JSON 数据', () => { + it('XML 数据', async () => { + const axios = createAxios(); + const res = await axios.get('https://httpbin.org/robots.txt'); + expect(res.data).toBe('User-agent: *\nDisallow: /deny\n'); + }); +}); + +describe('请求 JSON 数据', () => { + it('普通数据', async () => { + const axios = createAxios(); + const res = await axios.get('https://httpbin.org/json'); + const expected = JSON.parse('{"slideshow":{"author":"Yours Truly","date":"date of publication","slides":[{"title":"Wake up to WonderWidgets!","type":"all"},{"items":["Why WonderWidgets are great","Who buys WonderWidgets"],"title":"Overview","type":"all"}],"title":"Sample Slide Show"}}'); + expect(res.data).toMatchObject(expected); + }); + + it('带 bigint', async () => { + const axios = createAxios(); + const obj = { id: 9223372036854775808n }; + const res = await axios.post('https://httpbin.org/anything', obj); + expect(res.data.json).toMatchObject(obj); + }); +}); diff --git a/package.json b/package.json index 5e92e30..6695856 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ }, "dependencies": { "axios": "^1.6.2", + "is-plain-object": "^5.0.0", "js-base64": "^3.7.5", "json-bigint": "^1.0.0", "mitt": "^3.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f7951dc..aabc4a7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,9 @@ dependencies: axios: specifier: ^1.6.2 version: 1.6.2 + is-plain-object: + specifier: ^5.0.0 + version: 5.0.0 js-base64: specifier: ^3.7.5 version: 3.7.5 @@ -3239,6 +3242,11 @@ packages: engines: {node: '>=8'} dev: true + /is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + dev: false + /is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} diff --git a/src/axios.ts b/src/axios.ts index 2b8c0c1..27d7014 100644 --- a/src/axios.ts +++ b/src/axios.ts @@ -1,4 +1,5 @@ -import axios, { AxiosResponse, CreateAxiosDefaults } from 'axios'; +import axios, { AxiosResponse, CreateAxiosDefaults, InternalAxiosRequestConfig } from 'axios'; +import { isPlainObject } from 'is-plain-object'; import jsonBigint from 'json-bigint'; import { FanbookApiError } from './error'; @@ -7,21 +8,40 @@ export const bigintJsonParser = jsonBigint({ useNativeBigInt: true, }); +/** 测试 Content-Type 是否为 json 的正则表达式。 */ +const isJsonContentType = /^application\/(.*\+)?json$/; /** - * 转换请求响应。 - * @param data 响应体 - * @returns 转换后的响应体 + * 解析请求 bigint。 + * @param req 请求数据 + * @returns 转换后的请求数据 */ -function parseResponseBigint(data: unknown) { - console.log(data); - if (typeof data === 'string') { - try { // 能转 JSON 就转 - return bigintJsonParser.parse(data); - } catch { // 不能转原样返回 - return data; - } +function requestBigintInterceptor(req: InternalAxiosRequestConfig) { + const type = req.headers['content-type']; + if ( + (typeof type === 'string' && isJsonContentType.test(type)) + || isPlainObject(req.data) + ) { // 是 JSON + req.data = bigintJsonParser.stringify(req.data); + req.headers['content-type'] ??= 'application/json'; } - return data; + return req; +} +/** + * 解析响应 bigint。 + * @param res 响应数据 + * @returns 转换后的响应数据 + */ +function responseBigintInterceptor(res: AxiosResponse) { + // 非 json 不处理 + const type = res.headers['content-type']; + if (typeof type !== 'string') return res; + if (!isJsonContentType.test(type)) return res; + + // 带 bigint 解析 json,不行就算了 + try { + res.data = bigintJsonParser.parse(res.data); + } catch {} + return res; } /** @@ -30,13 +50,17 @@ function parseResponseBigint(data: unknown) { * @returns Axios 实例 */ export function createAxios(options?: CreateAxiosDefaults) { - let pipes = options?.transformResponse; - // 将 pipes 转数组,然后浅拷贝,保证后续修改 options 不会影响到 pipes - if (Array.isArray(pipes)) pipes = [...pipes]; - else pipes = pipes ? [pipes] : []; // pipes 可能为 undefined - - pipes.unshift(parseResponseBigint); - return axios.create(options); + const inst = axios.create({ + ...options, + // 由于 axios 自动解析 json 导致 bigint 精度丢失,要阻止它的 json 解析 + // 如果用户已经给定 pipes,它会阻止 json 解析 + // 如果用户没有给定 pipes,要用 `(x) => x` 阻止 json 解析 + transformRequest: options?.transformRequest ?? ((x) => x), + transformResponse: options?.transformResponse ?? ((x) => x), + }); + inst.interceptors.request.use(requestBigintInterceptor); + inst.interceptors.response.use(responseBigintInterceptor); + return inst; } /**