纯本地JSON格式化校验压缩工具开发实践
2026/9/8 15:18:46 网站建设 项目流程

前阵子跟一个老接口联调,返回体被日志打成了很长的一串 JSON。我从日志中间拷出来,随手打开一个在线格式化网站准备排错。格式化倒是挺快,可页面右上角的请求一直跳,我心里就咯噔了一下:虽然只是测试环境的数据,里面没有真实账号密码,但那段 JSON 带了内网标记、账号别名和设备标识。它们在别人网页上停留的每一秒,我都不能确定会不会被采集。

从那次之后,我决定把整套“JSON格式化、JSON校验、JSON压缩”搬到一个纯本地跑的单页工具里。它不做后台、不请求任何接口、不加载第三方统计脚本,输出结果和原始文本都在浏览器内完成。这篇文章就把这个工具的设计过程和实现思路完整拆开讲一遍,包括格式化背后的原理、校验时如何精确定位错误、语法高亮怎么做,以及我在自测时踩过的几个边界情况。如果你平时也经常处理配置型 JSON,或者正在考虑给自己写一个类似的内部小工具,这部分内容可以直接参考。

1. 这个工具不是“又一套 JSON 页面”,而是被在线网站坑过之后的产物

1.1 在线工具省下的五分钟,可能要用十倍隐私去还

做后端对接或者调前端接口的人,基本都遇到过这种场景:服务端返回的数据是压缩成一行甚至多行拼接的,肉眼完全没法看;本地编辑器虽然支持格式化,但有些环境不能随便装插件,有些数据是从远程日志里拷出来的,不可能先同步到本地工程里。

于是在线 JSON 格式化网站就成了默认选择。说实话,它们确实快,粘贴进去,点一下,缩进就出来了。问题在于“数据到了别人手里”这件事基本不可控。你贴上去的可能是带 token 的调试报文、包含内部路径的日志、还没脱敏的测试用户信息。很多在线工具页面上还挂着统计、广告联盟、埋点脚本,你没点上传按钮,不代表输入框里的内容没有被监听。更极端的还有把格式化结果缓存到服务端以便“历史记录”功能的,这些行为在用户协议里可能写了,但你基本不会看。

我并不是说所有在线工具都有恶意,只是从工程风险控制的角度看,非公开数据能不能出设备,应该由我们自己决定,而不是默认相信一个陌生站点。处理公开的示例 JSON 用在线网站没关系,但处理内部联调数据,我的原则是本地能干完的活儿绝不上传。

1.2 三合一不是把三个按钮放到同一页

标题里的“格式化/校验/压缩”三个功能看起来很简单,实际上如果只是把三个功能按钮硬塞到同一张网页里,用起来依然很难受。我所理解的“三合一”应该是:同一段输入,三种操作可以随时切换,并且切换过程中不会丢失对原始内容的理解。

举个例子,我最初想得很简单:格式化时拿一个 textarea 展示结果,压缩时再拿一个 textarea 展示结果,校验时弹一个 alert。后来实际用了几次就发现,最常见的操作路径其实是“压缩后的 JSON 复制到某个系统里保存,那边报错,我拷回来本地校验,再格式化定位问题”。这个路径里三个阶段是反复横跳的,数据不能每次都在输入框之间手动搬。

所以最终工具的核心交互被我收敛成了:一个输入编辑区,一个输出编辑区,顶部一组操作按钮。输入区负责接收原始 JSON,输出区展示格式化或压缩后的结果。每次输入变化时会自动做语法检查,但不会赶在用户还在打字的时候就格式化整个文档,只会在状态栏给“暂未通过校验”的提示。只有用户明确点击格式化或压缩时,才把输出区内容替换掉。

另外,我明确砍掉了一些和 JSON 本身无关的需求。比如不做 XML 转换,不做 YAML 转换,不做 JSONPath 在线调试,不做随机 JSON 生成器。这些功能放在同一个页面里只会让界面变臃肿。一个 JSON 处理工具的核心价值是准确、稳定、低干扰,而不是把所有沾边功能都堆进来。

1.3 为什么坚持不做后端

最初也有人劝我,校验用一段 Java 代码写在服务端不更严谨吗?我拒绝的理由很直接:后端一引入,这个工具就从“本地工具”变成了“在线服务”,又要处理鉴权、并发、日志脱敏和文件保存策略。为了一个格式化输入框去维护一套后端,性价比太低。

JSON 解析和序列化本来就是现代 JavaScript 运行时的原生能力。前端页面里执行JSON.parseJSON.stringify,用的是和 Node.js、浏览器控制台一致的 V8/JavaScriptCore 引擎实现,处理标准 JSON 没有任何准确性劣势。真正需要后端或独立 parser 的场景是超大型文件、非标准 JSON 方言、需要流式解析的应用,这类场景也不适合在浏览器里做。所以我给这个工具定的边界很明确:标准 JSON,单文件控制,数据不出浏览器,把 80% 的日常需求做好就够了。

技术选型上我甚至没有引入前端框架,最后产物就是一个自包含的index.html,CSS 和 JS 全部内联。这样做的好处是双击就能用,也可以直接扔到内网服务器上,不用 npm install,不用构建。如果你想复刻出一个差不多的工具,这个选型应该是最容易起步的。

2. 格式化与压缩的真正分水岭:字符串里的空格不能乱动

2.1 格式化不是“加几个空格”那么简单

很多第一次写格式化工具的人,第一反应是写一个文本处理函数:遍历每个字符,遇到左括号就换行加缩进,遇到右括号就减少缩进。这种写法看起来能处理八成的标准 JSON,但本质上是在用“文本结构猜测”代替“语法解析”,只要字符串内部出现括号、引号、冒号,立刻就会翻车。

比如这段合法 JSON:

{"tips":"他说:\"你点的菜到了\",然后说:好","list":[1,2]}

如果只是机械地对冒号后加空格、对花括号换行,"他说:\"你点的菜到了\""里的中文冒号和转义引号都会干扰逻辑。真正稳妥的做法是先调用JSON.parse,把原始 JSON 文本变成 JavaScript 对象,再用JSON.stringify重新序列化输出。也就是说,格式化工具的核心并不是“排版”,而是“解析后再序列化”。

基础实现可以短到这样:

function formatJson(input, indent = 2) { const parsed = JSON.parse(input); return JSON.stringify(parsed, null, indent); } function minifyJson(input) { const parsed = JSON.parse(input); return JSON.stringify(parsed); }

formatJson里,如果输入不是合法 JSON,JSON.parse会直接抛异常,所以格式化按钮的公共逻辑必须是先捕获异常,再决定是否渲染输出,绝不能拿着解析失败的结果继续往下走。压缩函数同理,也必须先 parse 一次。这两个函数看起来简单,但它们是整个工具的基石。

2.2 自定义缩进不是让用户随便填个数字

标题里写了“缩进自定义”,这个功能如果做成一个数字输入框确实很简单,但实际使用中有几个细节值得注意。

JSON.stringify的第三个参数space接受数字或字符串。数字表示缩进多少个空格,但规范里有个限制:大于 10 的数字会被截断为 10。如果你填 16,实际缩进也是 10 个空格,很多用户会以为出了 bug。所以界面上做数字选项时,如果要允许用户输入,必须在输入框下面提示范围,或者在formatJson里做一次截断处理。

如果你希望支持 Tab 缩进,也可以直接给space传入字符串'\t'

function formatJson(input, formatType) { const parsed = JSON.parse(input); const spaceMap = { "2": 2, "4": 4, "tab": "\t" }; return JSON.stringify(parsed, null, spaceMap[formatType] ?? 2); }

我在工具里没有提供“按照层级染色缩进线”这种视觉功能,因为那对 JSON 阅读的辅助有限,反而增加了页面复杂度。真正有价值的是格式化后的输出能稳定复现,不会同一个 JSON 在不同浏览器里跑出两种缩进。用JSON.stringify天然满足这一点。

2.3 压缩不是去掉所有空格

JSON 压缩这个功能看起来更简单,无非就是把格式化后的内容再变回一行。但有个高频错误我见过太多次:有人用text.replace(/\s+/g, '')去压缩 JSON,结果字符串内部包含的空格也被删了。

例如:

const text = '{"message":"hello world","code":0}'; console.log(text.replace(/\s+/g, '')); // 输出:{"message":"helloworld","code":0}

"hello world"里的三个连续空格被当成 JSON 文本里的排版空格删掉了,最终得到的 JSON 虽然语法上仍然合法,但数据本身已经被改坏了。如果需求是“压缩后发送到接口做签名校验”,这种错误会直接导致签名对不上,而且排查起来相当隐蔽。

所以压缩同样要走JSON.parse再到JSON.stringify的链路。先保证语法正确,再通过序列化去掉所有非字符串内部的空白。实际上JSON.stringify(parsed)默认就不会输出任何多余空格,字符串内部的内容则由 JSON 语法规则完整保留。压缩后如果想放到 URL 参数里使用,还要记得做一次encodeURIComponent,否则{}:"这些字符会破坏 URL 结构。

同样的道理也适用于格式化:不要想着“格式化就是让内容变漂亮”,格式化等于“把 JSON 先还原成结构,再按标准缩进输出”。无论你多喜欢用正则和文本替换,只要不经过解析,就永远处理不好字符串内部有括号和引号的边界情况。

3. 校验的体验差距:给出“第几行第几列”,比一句 parse error 有用得多

3.1 浏览器给出的错误信息差异太大

JSON.parse做 JSON 校验,是天然的做法。但它抛出的错误信息在不同浏览器里格式并不一致,状态栏如果只显示SyntaxError: Unexpected token } in JSON at position 64,普通用户根本不知道要去改哪里。

我在实际使用中收集到的大致有这几种格式:

浏览器/环境错误示例
ChromeUnexpected token } in JSON at position 64
FirefoxJSON.parse: unexpected character at line 1 column 14 of the JSON data
SafariJSON Parse error: Unexpected identifier 'abc'
Node.jsUnexpected token } in JSON at position 64

既然错误位置信息分散在消息文本里,校验功能就不能只显示 message,还要想办法从 message 中提取位置信息,再做行列换算。

我的定位逻辑分成三种情况:先尝试匹配 Firefox 风格里的line X column Y;再尝试匹配 Chrome/Node.js 风格里的position N;如果两边都匹配不到,就退回到“只显示错误原因和错误处前后片段”。真实场景里 Chrome 风格和 Firefox 风格占了绝大多数,所以这个策略的命中率足够高。

3.2 从 offset 换算行列号和上下文

当拿到position后,把它理解为这个字符串里的字符偏移,然后遍历前缀文本换算出是第几行第几列,这样 UI 才能直接展示“第 12 行,第 8 列附近有问题”。

一个简单的换算函数可以这样写:

function offsetToLineColumn(text, offset) { let line = 1; let column = 1; const safeOffset = Math.max(0, Math.min(offset, text.length)); for (let i = 0; i < safeOffset; i++) { if (text.charCodeAt(i) === 10) { line++; column = 1; } else { column++; } } return { line, column }; }

校验函数里,先拿到错误信息,再把错误位置附近的一小段原文截取出来:

function validateJson(text) { if (!text.trim()) { return { valid: false, message: "内容为空" }; } try { JSON.parse(text); return { valid: true }; } catch (error) { const raw = error.message; // Firefox: "line X column Y" const lineColumn = raw.match(/line\s+(\d+)\s+column\s+(\d+)/i); if (lineColumn) { return { valid: false, line: Number(lineColumn[1]), column: Number(lineColumn[2]), message: error.message }; } // Chrome/Node: "position N" const position = raw.match(/position\s+(\d+)/i); if (position) { const offset = Number(position[1]); const pos = offsetToLineColumn(text, offset); const start = Math.max(0, offset - 20); const end = Math.min(text.length, offset + 20); return { valid: false, line: pos.line, column: pos.column, snippet: text.slice(start, end), message: error.message }; } return { valid: false, message: error.message }; } }

有了行列号和上下文片段,工具就可以在状态栏显示“第 12 行,第 8 列附近有异常,错误原文片段是 ...”,同时把输入区滚动到对应的行。这个体验和单纯弹一个报错框差距非常大。谁都不想在一份几千行的 JSON 里人工数行号。

3.3 标准 JSON 的“不背锅”场景:BOM、尾逗号、单引号

调试多了以后,我发现很多“校验失败”并不是用户手写错了,而是粘贴过来的内容里夹带了不可见字符,或使用了 JSON 标准之外的写法。

最典型的是 BOM 头。Windows 下某些工具导出的 JSON 文件会在开头加一个 UTF-8 BOM(\uFEFF),直接JSON.parse会报Unexpected token。我的工具会在读取和粘贴后自动把开头的\uFEFF去掉,而不是让用户自己去发现那是个看不见的字符。

还有三种常见写法,工具应该直接给出明确的错误提示,而不是让浏览器异常里的“unexpected token”吓到人:

  • 尾逗号:{"a":1,}不是合法 JSON,很多人从 JavaScript 对象字面量复制过来时会带尾逗号。
  • 单引号字符串:{'a': 1}不是合法 JSON,JSON 明确要求字符串用双引号。
  • 注释:{"a": 1 /* 这是注释 */}不是合法 JSON,JSON 设计之初就没有注释。

我早期在工具里遇到这些错误时,只会原样显示SyntaxError。后来我在解析前加了一层“错误原因猜测”,如果是 BOM 就提示“已自动移除 BOM”,如果是尾逗号就提示“JSON 不允许尾逗号”,如果是单引号就提示“JSON 字符串必须使用双引号”。虽然这类提示本质上是字符串匹配,不能覆盖所有情况,但一个准确友好的提示,能帮用户少走不少弯路。

4. 高亮、错误定位和编辑联动,其实可以只用 textarea 实现

4.1 一个够用的 token 扫描器

语法高亮对 JSON 来说并不复杂,但我一开始也踩了一个常见的坑:不能直接拿全局正则对整段文本做多次匹配。因为 JSON 的字符串内部可以包含冒号、逗号、花括号等符号,如果先匹配数字、再匹配符号,数字很可能匹配到字符串内部的内容上。

正确做法是先扫描字符串,把字符串内所有内容作为一个整体 token 处理,然后再去识别字符串外面的符号和数字。我用了一个非常简单的 token 扫描器思路:

function tokenizeJson(text) { const tokens = []; let i = 0; const len = text.length; while (i < len) { const ch = text[i]; if (/\s/.test(ch)) { i++; continue; } if (ch === '"') { const start = i; i++; while (i < len) { if (text[i] === "\\") { i += 2; continue; } if (text[i] === '"') { i++; break; } i++; } tokens.push({ type: "string", start, end: i }); continue; } if ("{}[],:".includes(ch)) { tokens.push({ type: "punct", start: i, end: i + 1 }); i++; continue; } const start = i; while (i < len && !/[\s{}[\]",:]/.test(text[i])) { i++; } const word = text.slice(start, i); if (word === "true" || word === "false" || word === "null") { tokens.push({ type: "literal", start, end: i }); } else { tokens.push({ type: "value", start, end: i }); } } return tokens; }

这个扫描器的核心是:遇到双引号时,先处理转义字符,再处理结束引号,从而保证字符串内部即使出现{:也不会被错误识别成结构符号。字符串识别完以后,再统一处理花括号、中括号、逗号、冒号和字面量。对 JSON 这种语法来说,这个思路简单稳定,跑起来也非常快。

4.2 高亮层和输入层怎么对齐

有了 token 列表,下一步就是要把 token 渲染成带颜色的 HTML,并且让高亮层和底下的编辑区对齐。我采用的是一个很常见的方案:一个包含代码高亮的<pre>层,上面覆盖一层文字透明的<textarea>,两层的字体、字号、行高、内边距完全一致,滚动事件同步。

大致结构是这样:

<div class="editor-wrap"> <pre class="highlight-layer" aria-hidden="true"></pre> <textarea class="input-layer" spellcheck="false"></textarea> </div>

CSS 里必须保证:

.editor-wrap { position: relative; font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; font-size: 14px; line-height: 1.6; } .highlight-layer, .input-layer { white-space: pre-wrap; word-wrap: break-word; padding: 12px; margin: 0; border: 0; width: 100%; min-height: 400px; font: inherit; line-height: inherit; } .highlight-layer { position: absolute; top: 0; left: 0; pointer-events: none; color: #abb2bf; z-index: 1; } .input-layer { position: relative; z-index: 2; color: transparent; background: transparent; caret-color: #e5c07b; -webkit-text-fill-color: transparent; }

当输入框滚动时,需要把高亮层同步滚动:

inputLayer.addEventListener("scroll", () => { highlightLayer.scrollTop = inputLayer.scrollTop; highlightLayer.scrollLeft = inputLayer.scrollLeft; });

由于<pre>里渲染的是重新拼接的 HTML,必须对原始文本做 HTML 转义,防止内容里的<>&被浏览器当成标签解析。每次更新高亮时,我会先把文本按 token 切成片段,再给每段包上对应的 span。

function buildHighlightHtml(text) { const tokens = tokenizeJson(text); let html = ""; let lastIndex = 0; const colorMap = { string: "#98c379", literal: "#56b6c2", punct: "#abb2bf", value: "#d19a66" }; for (const token of tokens) { html += escapeHtml(text.slice(lastIndex, token.start)); const content = escapeHtml(text.slice(token.start, token.end)); html += `<span style="color:${colorMap[token.type]}">${content}</span>`; lastIndex = token.end; } html += escapeHtml(text.slice(lastIndex)); return html; }

这样处理之后,普通用户看到的就是一个带颜色的 JSON 编辑区。因为输入框文本是透明的,真正显示出来的文字是高亮层里的 HTML,所以颜色可以自由控制,光标还保持在原来的 textarea 中,不会出现 contenteditable 那种光标乱跳的问题。

4.3 实时格式化、防抖、光标保持

给输入区绑一个input事件后,可以做实时校验。但这里不能每敲一个字符就跑一次完整格式化加高亮,否则内容一多页面会卡。一般我会做一个 150 到 250 毫秒的防抖,用户停止输入后才执行校验和高亮更新。

let timer; inputLayer.addEventListener("input", () => { clearTimeout(timer); statusNode.textContent = "正在检查..."; timer = setTimeout(() => { const text = inputLayer.value; const result = validateJson(stripBom(text)); statusNode.textContent = result.valid ? "JSON 格式正确" : `${result.line || "?"} 行 ${result.column || "?"} 列:${result.message}`; highlightLayer.innerHTML = buildHighlightHtml(stripBom(text)); }, 200); });

有一个细节:格式化按钮点击后,应该把格式化结果放回输入区而不是只放到输出区。我一开始把结果放到独立的输出区,结果用户还是要手动复制回输入区继续编辑,多了一步。后来我把交互改成“格式化后结果直接替换输入框内容,输出区只负责压缩结果和复制按钮”,用起来顺手很多。压缩也是一样,输出区会保存上一次操作的副本,用户可以从输出区复制,也可以一键保存为.json文件。

5. “本地安全处理”到底怎么落地:文件读写和隐私边界

5.1 保证页面不发任何网络请求

“本地安全处理”这句话不能只是写在标题里,得让使用者可验证。我这个工具的 HTML 里没有设置任何fetchXMLHttpRequestWebSocket,也不加载远程字体、远程图片、第三方库。如果你拿到这样一份 HTML,最直接的验证方法是:打开浏览器开发者工具的 Network 面板,刷新页面,操作一遍,如果整个过程中请求数为 0,那么可以确定没有发生网络传输。

但这里我要说明一点:就算页面上没有任何主动请求,也不代表浏览器一定不会因为插件、扩展、浏览器同步等机制发送数据。所以我在工具界面上加了一行提示:“建议在无敏感信息的本机环境使用,处理完毕关闭页面。”同时,页面的 CSP 头也可以被设置成一个相对严格的值,比如:

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'none'

作为单 HTML 文件,CSP 并不好通过 HTTP 头指定,但如果你把这个文件部署到内网 Web 服务上,就可以在服务器配置里加这个响应头。断网状态下使用当然是最保险的验证方式。

5.2 文件拖拽、另存为和剪贴板

本地处理工具必然涉及文件的打开和保存。用浏览器 File API 打开文件时,文件内容并不会自动上传,只有当你显式用fetchFormData提交时才会离开设备。因此文件读取功能对隐私是友好的。

支持拖拽打开的方式也很直接:监听dragoverdrop事件,然后读取文件:

dropZone.addEventListener("dragover", (event) => { event.preventDefault(); }); dropZone.addEventListener("drop", async (event) => { event.preventDefault(); const file = event.dataTransfer.files[0]; if (!file) return; const text = await file.text(); inputLayer.value = stripBom(text); updateHighlightAndValidate(); });

保存文件时,我会生成一个Blob,然后用URL.createObjectURL生成一个本地对象地址,触发下载:

function saveJsonFile(text, filename = "formatted.json") { const blob = new Blob([text], { type: "application/json;charset=utf-8" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = filename; link.click(); URL.revokeObjectURL(url); }

剪贴板复制按钮同样是在本地完成的。需要注意,navigator.clipboard.writeText在某些环境下要求页面处于 HTTPS 或 localhost,单 HTML 通过file://打开时不一定可用。所以我留了一个降级方案:如果剪贴板 API 不可用,就自动选中输出框内容,提示用户按 Ctrl+C 复制。

5.3 “本地”也不等于“一定安全”

我在工具落地后重新理解了“本地安全处理”的边界。数据不出本机,规避的是网络传输风险。但浏览器本身并不提供完整的沙箱隔离,比如恶意浏览器扩展可以读取当前页面 DOM,甚至监听输入框内容。因此“本地处理”能做到的是降低风险,而不是把它变成零。

另外还要小心浏览器自动填充和本地存储。我不建议把用户粘贴的 JSON 自动存进localStorage,虽然 localStorage 不会主动上传,但任何本地脚本一旦被注入都能读到。我做的处理是:工具关闭后不保留输入内容,刷新页面即清空。如果你需要草稿功能,可以自己做导出,而不是把敏感内容默认留在本机缓存里。

剪贴板里的数据也要提醒用户及时清空。复制敏感 JSON 到剪贴板后,如果机器上装了带剪贴板读取权限的应用,仍然有泄露可能。所以我在保存成功的提示里写了一句话:“操作完成,可以清空剪贴板。”这不是一句废话,是实际使用中我自己经常会忘记的步骤。

6. 我在自测中处理掉的几个刁钻 JSON 边界问题

6.1 超大 JSON 和页面假死的斗争

第一次拿一个 50 万字符的日志 JSON 测试时,页面直接卡了好几秒。主要时间花在 token 扫描和 HTML 重绘上。虽然 JSON.parse 本身很快,但每次输入变化都重建全量高亮 DOM,代价非常大。

针对大文本,我做了三件事:第一,把输入事件里的防抖时间从 200 毫秒提高到 300 毫秒,只有用户明显停顿后才开始处理;第二,超过 500KB 的内容默认关闭实时高亮,只更新校验状态;第三,格式化按钮点击时如果遇到超大结果,走一次异步渲染,用requestIdleCallbacksetTimeout让页面先绘制出“处理中”的状态,避免用户以为按钮坏了。

function processLargeJson(text) { statusNode.textContent = "正在处理大文件..."; setTimeout(() => { const formatted = formatJson(stripBom(text)); inputLayer.value = formatted; updateHighlightAndValidate(); }, 0); }

这种手动让步式的优化看起来不高端,但对单页面工具来说非常实际。如果你想做得更彻底,可以用 Web Worker 来跑 parse 和 tokenizer,让主线程完全不卡。我因为工具本身追求简单,暂时没有引入 Worker,而是用门槛限制和异步渲染解决了体验问题。

6.2 合法顶层值不只是对象

很多人默认 JSON 顶层就应该是{...},于是校验函数写成JSON.parse(text).type === 'object'才认为合法。但 JSON 标准允许顶层是数组、字符串、数字、布尔值或null。比如"hello"123true[1,2,3]都是合法的 JSON。

如果工具只认对象,就会把[1,2,3]这种常见的接口响应体误判为非法。我在校验结果里加入了顶层类型显示:如果解析成功,会顺便提示“顶层类型:Array / Object / Number / String / Boolean / Null”。这样用户看到后不会误以为工具把数组当成非法输入。

格式化这类顶层类型时也要注意。比如顶层是一个字符串"hello",格式化后依然是"hello",不会自动变成带引号换行的对象。不要在渲染层假设所有合法 JSON 都是对象结构。

6.3 格式化后对象键顺序、重复键与大整数

自测过程中,我特别关注了“格式化会不会改变用户原始内容”的问题。标准 JSON 经过JSON.parseJSON.stringify,在绝大多数情况下键顺序会保留 JavaScript 的插入顺序。但实际存在两个细节:

一是数字样式的键会先被 JavaScript 排序,比如{"2":"b","1":"a"}格式化后,"1"很可能出现在"2"前面。这是 JavaScript 对象键序的固有行为,不是 JSON 解析导致的新问题。如果你要严格保持原始顺序,原生JSON.parse做不到,需要接入手写 parser 或第三方解析库。

二是重复键。JSON.parse('{"a":1,"a":2}')不会报错,但重复键只有最后一个生效,格式化就会把前面的丢点。如果在真实业务里出现过重复键且不能丢,就需要一个能收集重复键的解析方案。

三是大整数精度。9007199254740993这个数字大于 JS 安全整数范围,JSON.parse后会变成不精确的浮点数,再序列化出来就是9007199254740992。对大整数敏感的报文,不能依赖原生 JSON API 做格式化。对于绝大多数人阅读和排错的需求,这个问题发生概率不高,但我在工具说明里放了一句“只适合标准日常 JSON”,不算免责声明,而是避免将来有人拿数字加密场景里的 JSON 过来踩坑。


最后分享一个我实际使用中觉得最值得的改动:不要只做“格式化”和“压缩”两个按钮,在中间加一个“复制结果”按钮,并且这个按钮永远复制最近一次操作的输出。过去我在别的工具里,格式化完还要手动全选、复制、切到编辑器粘贴,一天重复几十次真的很累。现在只要输出结果有变化,复制按钮就会自动把新结果放到剪贴板,配合一个快捷键位,基本上可以做到单手完成整套流程。小工具的核心价值往往不在功能数量,而在于高频操作能不能缩短到一两个动作以内。这个思路,我希望你也能用上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询