起因
后台返回的JSON数据中,部分ID使用了一长串数字作为ID使用,但由于JS的number会伴随精度丢失的问题,很有可能你的9999999999999999读出来就变成了10000000000000000了,后续数据交互若需要此值,则每次得到的始终都不会是期望的值
解决办法
- 后台处理的方式,将可能的值转成字符串返回,这种方式比较简单,只需要后端支持一下即可(想要正常的计算,转为bigInt即可)
- 前端处理的方式
- 使用正则,将请求获取到的原始数据先行处理一遍,给大于精度临界值的值加上双引号,然后再交给JSON.parse处理
- 自行解析JSON,这种处理方式可能会伴随部分性能的损失,不推荐(然鹅,我还是用了这种)。
手写JSON解析器
我的思路是,以逐字符读取的方式,将每个字符一一解析,自行判断类型和获取值,最终得到完整的JSON对象,再添加一个附加功能,能够hook到数据处理的关键步骤中,以达到自行对数据预处理,按着如上思路,有了以下代码:
enum JsonType {
String,
Boolean,
Number,
Null,
JsonObject,
JsonArray,
}
interface Result {
key: string;
value: any;
resource: string;
}
export class DGson {
jsonStr = '';
hooks: any;
/** char-index */
charIndex = 0;
constructor(jsonStr: string = '', hooks?: (result: Result) => any) {
this.jsonStr = jsonStr;
this.hooks = hooks;
this.jsonStrPreprocessing();
return this.getValue().value;
}
jsonStrPreprocessing() {
this.jsonStr = this.jsonStr
.replace(/\\n/g, '\n')
.replace(/\\r/g, '\r')
.replace(/\\t/g, '\t')
.replace(/\\b/g, '\b')
.replace(/\\f/g, '\f')
.replace(/\\\\/g, '\\');
}
/** factory */
readCharFactory(): string {
let firstChar;
if (this.jsonStr.length > this.charIndex) {
firstChar = this.jsonStr[this.charIndex];
this.charIndex += 1;
}
return firstChar as string; // 强制声明为string,原因是正则匹配类型检查不允许undefined
}
/** 工厂回流 */
backFlowReadChar(n: number) {
this.charIndex -= n;
}
readCharHeadNotEmpty() {
let result = this.readCharFactory();
while (/\s/.test(result)) {
// 忽略空值,继续往下读
result = this.readCharFactory();
}
return result;
}
getValue() {
// const firstChar = this.readCharFactory();
let result: {
value: any;
resource: string;
key: string;
} = {
value: '',
resource: '',
key: '',
};
let nextFirstStr = this.readCharHeadNotEmpty();
const type = this.getType(nextFirstStr);
if (type === JsonType.JsonObject) {
result = {
key: '',
value: {},
resource: '',
};
let isReadNodeDone = false;
while (!isReadNodeDone) {
nextFirstStr = this.readCharHeadNotEmpty();
if (nextFirstStr === '"') {
// 发现双引号代表找到key值的开头了
const key = this.getKey();
let keyAfterChar = this.readCharHeadNotEmpty();
if (keyAfterChar !== ':') {
// key后面需要有 : 符号,否则视为非法
this.throwNoSuchChar(':');
}
const value = this.getValue();
let valueAfterChar = this.readCharHeadNotEmpty();
if (/[,}]/.test(valueAfterChar)) {
if (valueAfterChar === '}') {
// 如果发现结尾了,需要停止循环,代表已经读到尽头
isReadNodeDone = true;
// this.backFlowReadChar(1);
} else {
// nextFirstStr = this.readCharFactory();
}
} else {
this.throwNoEndSign(`,' or '}`);
}
result.value[key] = this.hooks ? this.hooks(value) : value.value;
} else if (nextFirstStr === '}') {
isReadNodeDone = true;
} else {
this.throwNoEndSign('}');
}
}
} else if (type === JsonType.JsonArray) {
result = {
key: '',
value: [],
resource: '',
};
nextFirstStr = this.readCharHeadNotEmpty();
if (nextFirstStr !== ']') {
this.backFlowReadChar(1);
let isReadNodeDone = false;
while (!isReadNodeDone) {
const value = this.getValue();
let valueAfterChar = this.readCharFactory();
while (/\s/.test(valueAfterChar)) {
valueAfterChar = this.readCharFactory();
}
if (/[,\]]/.test(valueAfterChar)) {
if (valueAfterChar === ']') {
isReadNodeDone = true;
}
} else {
this.throwNoEndSign(`,' or ']`);
}
if(this.hooks){
result.value.push(this.hooks(value));
}
else{
result.value.push(value.value);
}
}
}
} else if (type === JsonType.String) {
const v = this.getStringValue();
result = {
key: '',
value: v,
resource: v,
};
} else if (type === JsonType.Number) {
const v = this.getNumberValue();
result = {
key: '',
value: parseFloat(v),
resource: v,
};
} else if (type === JsonType.Boolean) {
const v = this.getBooleanValue();
result = {
key: '',
value: /true/i.test(v),
resource: v,
};
} else if (type === JsonType.Null) {
const v = this.getNullValue();
result = {
key: '',
value: null,
resource: v,
};
} else {
this.throwError(`This value cannot be resolved`);
}
return result;
}
getKey() {
let result = this.getStringValue();
if (result === '') {
// 键不允许为空
this.throwError(`Key is not allowed to be empty`);
}
return result;
}
getObjectValue() {}
getStringValue() {
let result = '';
let nextFirstStr = this.readCharFactory();
while (nextFirstStr !== '"') {
if (nextFirstStr === undefined) {
this.throwNoEndSign(`"`);
}
if (nextFirstStr === '\\') {
// 发现反斜杠,如果反斜杠后面是双引号,则表明下一个双引号是需要录入的,并且不算结束符号,反之则将反斜杠算作常规字符
nextFirstStr = this.readCharFactory();
if (nextFirstStr === '"') {
result += nextFirstStr;
} else if (nextFirstStr !== undefined) {
result += '\\' + nextFirstStr;
} else {
this.throwNoEndSign('"');
}
} else {
result += nextFirstStr;
}
nextFirstStr = this.readCharFactory();
}
return result;
}
getNumberValue() {
const lastStr = this.jsonStr[this.charIndex - 1]; // 获取上一个值,因为这是不可缺少的一部分
let result = '';
let nextFirstStr = lastStr;
while (/[0-9\.e\-\+]/i.test(nextFirstStr)) {
const lastStr = this.jsonStr[this.charIndex - 2];
if (/[0-9]/.test(nextFirstStr)) {
result += nextFirstStr;
nextFirstStr = this.readCharFactory();
} else if (nextFirstStr === '.') {
// 如果出现小数点,需要检查前面是否有过小数点,并且需要检查上一个字符是否是数字
if (!/\./.test(result) && /[0-9]/.test(lastStr)) {
result += nextFirstStr;
nextFirstStr = this.readCharFactory();
if (/[0-9]/.test(nextFirstStr)) {
result += nextFirstStr;
} else {
this.throwError(`Floating point values are incomplete`);
}
}
else {
this.throwError(`Point is error`);
}
} else if (/-/.test(nextFirstStr)) {
if (result.length > 0 && !/e/i.test(lastStr)) {
// 如果前面是e,则表示可能是科学计数法
if (/e/.test(lastStr)) {
result += nextFirstStr;
nextFirstStr = this.readCharFactory();
if (/[0-9]/.test(nextFirstStr)) {
// 科学计数法符号e后面必须跟随数字,否则就是不完整的错误格式
result += nextFirstStr;
} else {
this.throwError(`The expression of scientific counting method is incomplete`);
}
} else {
this.throwError(`The symbol "-" can only appear after the beginning or 'e'`);
}
} else {
result += nextFirstStr;
}
} else if (/e/i.test(nextFirstStr)) {
if (/e/i.test(result)) {
this.throwError(`
It's impossible to have two characters e`);
} else {
result += nextFirstStr;
}
} else if (/\+/.test(nextFirstStr)) {
if (result.length > 0 && /e/i.test(lastStr)) {
// 如果前面是e,则表示可能是科学计数法
result += nextFirstStr;
nextFirstStr = this.readCharFactory();
if (/[0-9]/.test(nextFirstStr)) {
result += nextFirstStr;
} else {
this.throwError(`The expression of scientific counting method is incomplete`);
}
} else {
this.throwError(`Can't start with an '+' sign`);
}
} else {
// nextFirstStr = this.readCharFactory();
}
}
this.backFlowReadChar(1); // Number类型不定长度,获取到最后会将下一个字符吞并,所以需要回流
return result;
}
getNullValue() {
const lastStr = this.jsonStr[this.charIndex - 1]; // 获取上一个值,因为这是不可缺少的一部分
let result = lastStr;
for (let i = 0; i < 3; i++) {
result += this.readCharFactory();
}
if (!/null/i.test(result)) {
this.throwError(`Value '${result}' is not 'Null' type`);
}
return result;
}
getBooleanValue() {
const lastStr = this.jsonStr[this.charIndex - 1]; // 获取上一个值,因为这是不可缺少的一部分
let result = lastStr;
for (let i = 0; i < 3; i++) {
result += this.readCharFactory();
}
if (/fals/i.test(result)) {
result += this.readCharFactory();
}
if (!/true|false/i.test(result)) {
this.throwError(`Value '${result}' is not 'Boolean' type`);
}
return result;
}
getType(aChar: string = '') {
let result;
if (aChar === '{') {
result = JsonType.JsonObject;
} else if (aChar === '[') {
result = JsonType.JsonArray;
} else if (aChar === '"') {
result = JsonType.String;
} else if (aChar === '-' || /[0-9]/.test(aChar)) {
result = JsonType.Number;
} else if (/[tf]/i.test(aChar)) {
result = JsonType.Boolean;
} else if (/n/i.test(aChar)) {
result = JsonType.Null;
} else {
this.throwError(`No matching type was found`);
}
return result;
}
throwError(e: string) {
throw `DGson Exception: ${e}, at position ${this.charIndex}`;
}
/**
* 没有找到对应结尾字符的异常
* @param aChar 该字符应该是char类型
*/
throwNoEndSign(aChar: string) {
this.throwError(`No end sign '${aChar}' was found`);
}
/**
* 没有找到某个字符异常
*/
throwNoSuchChar(aChar: string) {
this.throwError(`No such char is '${aChar}'`);
}
}
手写解析器的性能
自行测试的时候发现,比起JSON.parse,性能总是会慢30毫秒,不过还在我接受范围之中,所以就忽略啦。代码中还有很多地方应该是可以调整优化下来提升性能的,后续有时间再做吧。