最近在处理 API 返回的数据时,经常遇到压缩成一行的 JSON ,调试起来很痛苦。找了几个在线工具,要么广告多,要么功能单一。干脆自己实现一个,顺便把实现思路记录下来。

JSON 格式化的本质

其实核心就两行代码:

const parsed = JSON.parse(input)
const formatted = JSON.stringify(parsed, null, 2)

JSON.stringify 的第三个参数就是缩进空格数。传 2 就是 2 空格缩进,传 4 就是 4 空格,传 0 就是压缩。

但实际做工具时,远没这么简单。

错误定位:从 position 到行列号

JSON.parse 报错时,错误信息类似:

Unexpected token } in JSON at position 45

这个 position 45 是字符位置,对用户来说毫无意义。用户需要的是"第几行第几列"。

转换算法很简单:

function getLineAndColumn(input: string, position: number) {
  const lines = input.substring(0, position).split('\n')
  const line = lines.length
  const column = lines[lines.length - 1].length + 1
  return { line, column }
}

截取错误位置之前的 字符串 ,按换行符分割,行数就是数组长度,列数就是最后一行的字符数。

这样报错信息就变成了:

JSON 解析错误: Unexpected token } (行 3, 列 12)

用户一眼就能定位问题。

树形视图的递归实现

格式化后的 JSON 虽然可读,但层级深的时候还是不够直观。树形视图能更好地展示结构。

核心是一个递归组件:

function TreeNode({ data, name, level }: Props) {
  const [expanded, setExpanded] = useState(true)
  const isObject = data !== null && typeof data === 'object'
  const isArray = Array.isArray(data)
  
  if (!isObject) {
    // 基础类型:直接显示值
    return (
      <div style={{ marginLeft: level * 16 }}>
        <span className="key">{name}:</span>
        <span className={getTypeColor(data)}>
          {typeof data === 'string' ? `"${data}"` : String(data)}
        </span>
      </div>
    )
  }
  
  // 对象/数组:递归渲染子节点
  const keys = Object.keys(data)
  return (
    <div>
      <button onClick={() => setExpanded(!expanded)}>
        {expanded ? '▼' : '▶'} {name}
        {isArray ? `[${keys.length}]` : `{${keys.length}}`}
      </button>
      {expanded && keys.map(key => (
        <TreeNode
          key={key}
          data={isArray ? data[+key] : data[key]}
          name={isArray ? `[${key}]` : key}
          level={level + 1}
        />
      ))}
    </div>
  )
}

几个细节:

  1. 类型着色:字符串绿色、数字青色、布尔紫色,一眼区分类型
  2. 数组标记:用 [3] 显示数组长度,{5} 显示对象属性数
  3. 缩进控制:通过 level * 16px 实现层级缩进

性能优化:大文件处理

当 JSON 文件达到几 MB 时,直接渲染会卡顿。几个优化手段:

1. 虚拟滚动

只渲染可视区域的节点,配合 react-window 或自己实现:

import { FixedSizeList } from 'react-window'

function VirtualTree({ data }: { data: object }) {
  const nodes = flattenTree(data)  // 扁平化树结构
  
  return (
    <FixedSizeList
      height={600}
      itemCount={nodes.length}
      itemSize={24}
    >
      {({ index, style }) => (
        <div style={style}>
          <TreeNode data={nodes[index]} />
        </div>
      )}
    </FixedSizeList>
  )
}

2. 延迟解析

用户输入时不要实时解析,用 debounce 延迟处理:

const debouncedParse = useMemo(
  () => debounce((value: string) => {
    try {
      const parsed = JSON.parse(value)
      setOutput(formatJson(parsed))
    } catch (e) {
      setError(e.message)
    }
  }, 300),
  []
)

3. Web Worker

把 JSON 解析放到 Web Worker 中,避免阻塞 UI :

// worker.ts
self.onmessage = (e) => {
  try {
    const parsed = JSON.parse(e.data)
    self.postMessage({ success: true, data: parsed })
  } catch (e) {
    self.postMessage({ success: false, error: e.message })
  }
}

// main.tsx
const worker = new Worker('worker.ts')
worker.postMessage(largeJson)
worker.onmessage = (e) => {
  if (e.data.success) {
    setOutput(formatJson(e.data.data))
  }
}

一些边界情况

实现过程中踩过的坑:

1. 循环引用

JSON.stringify 遇到循环引用会报错:

const obj = { a: 1 }
obj.self = obj
JSON.stringify(obj)  // TypeError: Converting circular structure to JSON

检测循环引用:

function hasCircular(obj: any, seen = new WeakSet()): boolean {
  if (obj && typeof obj === 'object') {
    if (seen.has(obj)) return true
    seen.add(obj)
    return Object.values(obj).some(v => hasCircular(v, seen))
  }
  return false
}

2. 特殊字符

JSON 中的特殊字符需要正确转义:

const json = '{"text": "Line1\nLine2"}'  // \n 是换行
JSON.parse(json)  // 正确解析

const json2 = '{"text": "Line1
Line2"}'  // 直接换行会报错

编辑器组件需要处理这种情况,或者提示用户。

3. 大整数精度

JavaScript 的 Number.MAX_SAFE_INTEGER2^53 - 1,超过这个值的整数会丢失精度:

const json = '{"id": 9007199254740993}'
const obj = JSON.parse(json)
console.log(obj.id)  // 9007199254740992,精度丢失

解决方案是用 JSON.parse 的 reviver 参数:

function safeParse(json: string) {
  return JSON.parse(json, (key, value) => {
    if (typeof value === 'number' && !Number.isSafeInteger(value)) {
      return String(value)  // 转为字符串保留精度
    }
    return value
  })
}

最终效果

基于以上思路,做了一个在线工具:JSON 格式化

主要功能:

  • 格式化 / 压缩 / 校验
  • 错误定位到行列号
  • 树形视图折叠展开
  • 支持最大 10MB 文件

代码实现不复杂,但把细节做好需要花些心思。希望这篇对你有帮助。


相关工具:JSON 差异对比 | JSON 转 CSV

标签: none

添加新评论