处理后台返回JSON数据number类型精度丢失,手写JSON字符串转JSON对象

起因

后台返回的JSON数据中,部分ID使用了一长串数字作为ID使用,但由于JS的number会伴随精度丢失的问题,很有可能你的9999999999999999读出来就变成了10000000000000000了,后续数据交互若需要此值,则每次得到的始终都不会是期望的值

解决办法

  • 后台处理的方式,将可能的值转成字符串返回,这种方式比较简单,只需要后端支持一下即可(想要正常的计算,转为bigInt即可)
  • 前端处理的方式
    1. 使用正则,将请求获取到的原始数据先行处理一遍,给大于精度临界值的值加上双引号,然后再交给JSON.parse处理
    2. 自行解析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毫秒,不过还在我接受范围之中,所以就忽略啦。代码中还有很多地方应该是可以调整优化下来提升性能的,后续有时间再做吧。

你可能感兴趣的:(处理后台返回JSON数据number类型精度丢失,手写JSON字符串转JSON对象)