前端解决方案-雪花算法ID传输到前端之后精度丢失的问题

4,713 阅读4分钟

问题出现

后端ID的生成升级为雪花算法(snowflake),什么是雪花算法,其实就是一个生成全局唯一分布式ID的算法,改完之后在http请求回来的时候发现响应的数据跟后端数据库的数据不一致,一时间不知道问题出在哪里。

分析问题

经过分析,由于JavaScript语言本身问题,number类型的数值的最大安全数(Number.MAX_SAFE_INTEGER)是2^53次方, 也即是16位,而雪花算法生成的ID是19位,所以通过http传输到前端之后就会出现精度丢失的问题, 如1540265707848196888会变成1540265707030306800。

解决问题

该问题可以由后端解决,也可以由前端解决,网上大部分答案都是在后端解决的,在后端序列化后返回给前端,本文只分析前端的解决方法。

核心就是将超出js安全数字的值转换为bigint类型或者字符类型

第一种方法

利用JavaScript Bigint数据类型的特性,引入基于这个数据类型的插件json-bigint,然后在axios中的create方法中有一个transformResponse属性,在这个属性中中将json-bigint封装后的数据返回即可。如果数据里面有大于js安全数字的超大数字,这个插件就会把该数字转换为bigint类型,使用的时候就需要将该字段toString()才能得到该数字的字符串类型,具体代码如下。

// 安装
npm i json-bigint -S 
// 引入
import JSONBig from 'json-bigint'
// 在axios中使用 
const http = axios.create({ 
    timeout: 1000 * 30, 
    withCredentials: true, 
    headers: { 
        'Content-Type': 'application/json; charset=utf-8'
    }, 
    transformResponse: [ 
        function(data) { 
            try{ 
                return JSONBig.parse(data)
            }catch(e){ 
                return data
            }
         } 
    ]
})
 // 返回的数据 
 categoryIdBigNumber c: (2) [1540265707030306666] e18 s1
 // 使用
 res.data.data.categoryId = res.data.data.categoryId.toSting()

缺陷/待完善

全局处理后返回,思路是循环判断data中的数据,判断每个数据的数据类型,如果是bigint类型的话,直接toString后返回,但是这样处理的时间复杂度是是O(n^2),而且数据格式涉及分页,列表,对象,以及基本数据类型,处理起来非常麻烦,而且性能极低,接下来看第二种方法。

2022年07月04日更正:写这篇文章前没有好好的看文档,这是作为程序员👨🏻‍💻这样严谨的职业不改犯的错误,有误导大家的地方请大家多多见谅。

事情是这样的,上面的情况是在json-bigint不加参数的情况下出现的,经查阅json-bigint的npm文档发现了有这样一个参数({ storeAsString: true }),大概意思就是将bigint作为字符串而不是默认BigNumber存储在对象中,但是他后面又跟了一句话,请注意,这是一种危险的行为,因为它破坏了在不更改数据类型的情况下来回转换的默认功能(因为这会将所有bigint转换为be-and-stay字符串),目前还没发现加这个参数会出现什么问题【手动捂脸】。

原文:

Specifies if BigInts should be stored in the object as a string, rather than the default BigNumber. Note that this is a dangerous behavior as it breaks the default functionality of being able to convert back-and-forth without data type changes (as this will convert all BigInts to be-and-stay strings).

然后代码就变成了下面这个样子。

// 安装
npm i json-bigint -S 
// 引入
const JSONBig = require('json-bigint')({ storeAsString: true })
// 在axios中使用 
const http = axios.create({ 
    timeout: 1000 * 30, 
    withCredentials: true, 
    headers: { 
        'Content-Type': 'application/json; charset=utf-8'
    }, 
    transformResponse: [ 
        function(data) { 
            try{ 
                return JSONBig.parse(data)
            }catch(e){ 
                return data
            }
         } 
    ]
})
 // 返回的数据 
 categoryId"1540265707030306666"

第二种方法

利用transformResponse函数返回的数据是原始数据(即stringify后的数据)的特性,可以利用正则将大于16位的数字匹配到,然后利用replace方法将改字段的值替换成字符串,这样即简单性能又高,代码如下。

const http = axios.create({ 
    timeout: 1000 * 30, 
    withCredentials: true, 
    headers: { 
        'Content-Type': 'application/json; charset=utf-8', 
        projectId: getProjectId() 
    }, 
    transformResponse: [ 
        function(data) { 
            // 加冒号是防止替换到非字段的值
            data = data.replace(/:([0-9]{16,})/g, (a, b) => {
                return `:"${b}"`
             }) 
             // 判断能被JSON.parse()的数据
             const flag = /^{.+}$/.test(data)
             return flag ? JSON.parse(data) : {}
         } 
    ]
})
// 返回的数据
[{"categoryId":"1540265707030306818","categoryName":"兑换券","categoryDescribe":null,"createTime":"2022-06-24 17:28:56.0"}]

写在最后

第一次在掘金发文,写起来还是问题不断,看到其它内容输出大佬写的文章格式那么优雅,真的自愧不如,但是我会不断优化争取写出更好的文章。关于本文,如果写得不好或者不对的地方还请各位大佬指正,感激不尽。