从 JSON.parse 到树形视图:实现一个在线 JSON 格式化工具

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

JSON 格式化的本质

其实核心就两行代码:

bash 复制代码
const parsed = JSON.parse(input)
const formatted = JSON.stringify(parsed, null, 2)

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

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

错误定位:从 position 到行列号

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

bash 复制代码
Unexpected token } in JSON at position 45

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

转换算法很简单:

bash 复制代码
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 }
}

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

这样报错信息就变成了:

bash 复制代码
JSON 解析错误: Unexpected token } (行 3, 列 12)

用户一眼就能定位问题。

树形视图的递归实现

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

核心是一个递归组件:

bash 复制代码
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 或自己实现:

bash 复制代码
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 延迟处理:

bash 复制代码
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 :

bash 复制代码
// 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 遇到循环引用会报错:

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

检测循环引用:

bash 复制代码
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 中的特殊字符需要正确转义:

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

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

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

3. 大整数精度

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

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

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

bash 复制代码
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

相关推荐
Aphasia3119 小时前
手写KeepAlive组件
前端·react.js·面试
whatever who cares13 小时前
大型 React 项目的文件结构
前端·react.js·前端框架
假如让我当三天老蒯14 小时前
useCallback 详细解释(从原理到用法)(自学用)
前端·react.js
Vu46115 小时前
nextjs的图片和文字优化
react.js
gogoing17 小时前
React Hooks 完整指南
react.js
假如让我当三天老蒯19 小时前
State和Props区别和左右(自学用)
前端·react.js
夜雪闻竹20 小时前
React Query + REST API 最佳实践
前端·react.js·前端框架
戈德斯文21 小时前
我做了一面互联网摸鱼墙:从无限 Canvas 到本地生产环境
react.js·canvas·next.js
vim怎么退出2 天前
Dive into React——Hooks 原理
react.js·源码阅读