当前端收到一个比梦想还大的数字:BigInt处理指南

当接口返回Bigint:前端精度丢失排查与解决之旅

某天下午,阳光正好,群消息突然闪烁。点开一看,后端同学甩来两张截图:

A 图是某个列表接口的返回,数据结构大致如下:

json 复制代码
{
    ...,
    data: [
        {
            id: 1234567890123456789, // 后端通过算法生成的19位自增id
            ...
        },
        ...
    ]
}

B 图则是前端拿着其中某个 id 去请求详情时接口报错的截图,请求参数长这样:

txt 复制代码
接口地址/xxx?id=1234567890123456800  // 注意:这里的 id 和 原id 后几位不一致

嗯?什么情况?!

一般来说,前端不会对 id 这类字段做特殊处理,查询时都是直接透传给后端,怎么会发生变化?

于是我开始排查:

  1. 打开开发者工具的 Network 面板,通过 Preview 查看接口返回,发现 id 显示为 1234567890123456800 ------ 咦,没毛病啊,B 图的入参就是这个值!可为什么和后端截图里的返回值不一致?
  2. 接着我点开 Response 查看原始返回数据,发现 id 其实是 1234567890123456789
  3. 直觉告诉我:有问题!平常的业务场景很少涉及这么大的数字。查了一下发现,JavaScript 的安全整数范围其实是有限制的:
js 复制代码
> Number.MAX_SAFE_INTEGER
< 9007199254740991  // 16位

一旦数字超过 MAX_SAFE_INTEGERJSON.parse 在序列化时就会出现精度丢失。而开发者工具的 Preview 面板正是通过 JSON.parse 来处理 JSON 数据的。

  1. 结论很明显:后端返回的 id 已经超出了 JavaScript 的安全整数范围。

(此处省略前后端友好交流环节...🤝)

接下来介绍:前端如何处理接口中的 Bigint

(PS:最稳妥的方式当然是后端在序列化时就把这类数字转为字符串。但总有各种原因需要前端接手,比如历史项目、临时方案等...你懂的。)

我们的方案并不复杂,但也踩了坑,一并分享给大家:

方案背景

  1. 项目基于 axios 封装了统一的 http 模块,为了控制影响范围,最好在其基础上做适配。
  2. 对于超出安全范围的 long 类型数字,通常有两种处理方式:
    • 转为 BigInt
    • 转为字符串

综合考虑后,我们选择了使用 json-bigint 这个库,它能灵活支持以上两种转换方式。

鉴于我们项目中目前只有 id 字段会出现大数,最终选择了「转为字符串」方案。

如果你考虑使用 BigInt,请务必了解其在各浏览器中的兼容性,并注意 BigInt 与普通 Number 在运算和类型判断上的差异。

有关 BigInt 的详细说明可参考:MDN - BigInt

具体实现

我们在 axios 的 transformResponse 阶段介入处理:

为何不在response拦截器做处理? axois拦截器阶段,json数据已经被序列化,此时的大数字已经精度丢失。

js 复制代码
import JSONbig from 'json-bigint'

axios.create({
    ...,
    transformResponse: [
        function(data, headers) {
            if (typeof data !== 'string') return data
            if (!data.trim()) return data
            
            try {
                const JSONbigParser = JSONbig({
                    storeAsString: true,   // 将大数转为字符串
                    useNativeBigInt: false,
                    alwaysParseAsBig: false,
                    strict: false
                })
                const parsed = JSONbigParser.parse(data)
                // 注意:这里有坑!下文详解
                return parsed
            } catch(err) {
                try {
                    return JSON.parse(data)
                } catch (callbackError) {
                    return data
                }
            }
        }
    ]
})

调试之后,确认接口返回中的大数字段已被正确转为字符串。

是不是觉得......有点太顺利了?

But...

随后我们发现,在 axios 的响应拦截器 interceptors.response 中,处理 resp.data 时会报错:

js 复制代码
axiosInstance.interceptors.response.use(
    (resp) => {
        resp.data.toString()  // 报错:toString is not a function
        // ...
    },
    (error) => { ... }
)

什么?resp.data 明明有值,却没有 toString 方法?

坑点分析

反复对比转换前后的 resp.data,我们发现:经过 json-bigint 解析后的对象,竟然没有原型(prototype)!

翻看其源码发现:

js 复制代码
// 摘自:https://github.com/sidorares/json-bigint/blob/master/lib/parse.js
object = Object.create(null)  // 使用这种方式创建对象,会丢失原型链

原来,作者为了预防「原型污染」,在 1.0.0 版本中将对象创建方式从 {} 改为了 Object.create(null),导致转换后的对象不再继承 Object.prototype

解决方案

有两种方式可以应对:

  1. 版本降级 :将 json-bigint 降级至 0.4.0 版本(该版本仍使用 {} 创建对象)。

    👉 相关 GitHub Issue

  2. 保持当前版本,手动恢复原型:我们编写了一个工具函数,递归地为对象恢复原型对象:

(以下为恢复原型的工具函数,可根据需要引用)

js 复制代码
/**
 * 原型恢复工具函数
 * 用于修复json-bigint解析后对象缺失原型的问题
 */

/**
 * 递归恢复对象原型链
 * json-bigint使用Object.create(null)创建纯对象,导致缺失Object.prototype方法
 * @param obj 需要恢复原型的对象
 * @returns 恢复原型后的对象
 */
export function restorePrototype(obj: any): any {
  // 基本类型和null直接返回
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  // 如果已经有原型,说明不需要恢复
  if (Object.getPrototypeOf(obj) !== null) {
    // 但仍需要递归处理子对象
    if (Array.isArray(obj)) {
      return obj.map(item => restorePrototype(item));
    } else {
      const restored: any = {};
      for (const key in obj) {
        if (obj.hasOwnProperty && obj.hasOwnProperty(key)) {
          restored[key] = restorePrototype(obj[key]);
        } else if (Object.prototype.hasOwnProperty.call(obj, key)) {
          // 对于没有hasOwnProperty方法的对象,使用Object.prototype.hasOwnProperty
          restored[key] = restorePrototype(obj[key]);
        }
      }
      return restored;
    }
  }

  // 处理数组
  if (Array.isArray(obj)) {
    // 创建新数组并恢复每个元素的原型
    return obj.map(item => restorePrototype(item));
  }

  // 处理普通对象:创建新对象并恢复原型
  const restored: any = {};
  
  // 复制所有属性(包括不可枚举的)
  const keys = Object.getOwnPropertyNames(obj);
  for (const key of keys) {
    const descriptor = Object.getOwnPropertyDescriptor(obj, key);
    if (descriptor) {
      // 递归恢复属性值的原型
      const restoredValue = restorePrototype(descriptor.value);
      Object.defineProperty(restored, key, {
        ...descriptor,
        value: restoredValue
      });
    }
  }

  return restored;
}

/**
 * 检查对象是否缺失原型
 * @param obj 要检查的对象
 * @returns 是否缺失原型
 */
export function isMissingPrototype(obj: any): boolean {
  return obj !== null && 
         typeof obj === 'object' && 
         Object.getPrototypeOf(obj) === null;
}

/**
 * 安全地调用对象方法,如果方法不存在则使用默认实现
 * @param obj 目标对象
 * @param methodName 方法名
 * @param defaultImpl 默认实现
 * @param args 方法参数
 * @returns 方法调用结果
 */
export function safeMethodCall<T>(
  obj: any, 
  methodName: string, 
  defaultImpl: (...args: any[]) => T,
  ...args: any[]
): T {
  if (obj && typeof obj[methodName] === 'function') {
    return obj[methodName](...args);
  }
  return defaultImpl.call(obj, ...args);
}

/**
 * 深度恢复对象原型(递归处理所有嵌套对象)
 * @param obj 要处理的对象
 * @returns 处理后的对象
 */
export function deepRestorePrototype(obj: any): any {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  // 处理数组
  if (Array.isArray(obj)) {
    return obj.map(item => deepRestorePrototype(item));
  }

  // 处理普通对象
  let result = obj;
  
  // 如果缺失原型,恢复它
  if (isMissingPrototype(obj)) {
    result = restorePrototype(obj);
  }

  // 递归处理所有属性
  for (const key in result) {
    if (Object.prototype.hasOwnProperty.call(result, key)) {
      result[key] = deepRestorePrototype(result[key]);
    }
  }

  return result;
}

小结一下

处理 BigInt 精度问题并不难,关键是要注意 json-bigint 在 v1.0.0 后的行为变化,以及如何在尽量少侵入业务的情况下平稳落地解决方案。

如果你也遇到了类似问题,希望这篇分享能帮到你。如果有更好的处理方式,欢迎交流讨论!

相关推荐
小喷友3 小时前
阶段四:实战(项目开发能力)
前端·rust
小高0073 小时前
性能优化零成本:只加3行代码,FCP从1.8s砍到1.2s
前端·javascript·面试
子兮曰3 小时前
🌏浏览器硬件API大全:30个颠覆性技术让你重新认识Web开发
前端·javascript·浏览器
即兴小索奇3 小时前
Google AI Mode 颠覆传统搜索方式,它是有很大可能的
前端·后端·架构
大虾写代码3 小时前
nvm和nrm的详细安装配置,从卸载nodejs到安装NVM管理nodejs版本,以及安装nrm管理npm版本
前端·npm·node.js·nvm·nrm
星哥说事3 小时前
下一代开源 RAG 引擎,让你的 AI 检索与推理能力直接起飞
前端
....4923 小时前
Vue3 与 AntV X6 节点传参、自动布局及边颜色控制教程
前端·javascript·vue.js
machinecat4 小时前
Webpack模块联邦 - vue项目嵌套react项目部分功能实践
前端·webpack
今禾4 小时前
深入浅出:ES6 Modules 与 CommonJS 的爱恨情仇
前端·javascript·面试