我给自己写了一个本地 JSON 工具:格式化、对比差异、UI 全自定义实录
目录
- 缘起:为什么我不再用任何在线 JSON 工具
- 需求拆解:从"能格式化"到"能秒懂差异"
- 技术选型:一个 HTML 文件就够了
- 核心实现一:格式化与语法高亮
- 核心实现二:两份 JSON 的差异对比
- UI 定制:JSON 工具也是"自己的工作台"
- 三个让我卡了一阵的坑
- 结尾:开发者工具的"双栏对照"哲学
1. 缘起:为什么我不再用任何在线 JSON 工具
我每天要和 JSON 打交道——接口返回、配置文件、日志片段。常做的事就两件:格式化和对比差异。但市面工具总让我皱眉:
- 在线工具广告多、加载慢,断网就废;
- 强推"保存到云端",我贴的可能是带 token 的接口返回;
- 大文件卡顿,几 MB 的 JSON 直接把页面冻住;
- 差异对比要么不支持、要么高亮不清晰,看不出"到底哪一层变了"。
我忽然意识到:JSON 处理是最高频的本地操作,凭什么要联网、要登录、要等加载。既然现成工具都在制造摩擦,那就自己写一个。
2. 需求拆解:从"能格式化"到"能秒懂差异"
把"做一个 JSON 工具"拆成可验收清单:
| 诉求(人话) | 拆解后(可验收) |
|---|---|
| 能格式化 | 粘贴即格式化,缩进可调(2/4/Tab) |
| 能压缩 | 一键压成单行,去空格换行 |
| 能高亮 | key/string/number/bool/null 不同色 |
| 能对比 | 左右两份 JSON,逐层高亮增删改 |
| 能折叠 | 大对象可按层级折叠,秒开几 MB |
| 不上传 | 全程本地,不发起任何网络请求 |
这张表决定了工具的骨架。功能不多,但每一项都做到位——这是我对所有"自己用的工具"的一贯要求。
3. 技术选型:一个 HTML 文件就够了
我犹豫过要不要上某个 JSON 库,但仔细想需求——浏览器原生 JSON.parse + JSON.stringify 已经足够,差异对比自己写递归更可控:
| 能力 | 方案 |
|---|---|
| 解析 | 原生 JSON.parse,带错误定位 |
| 格式化 | JSON.stringify(obj, null, indent) |
| 高亮 | 自写 tokenizer + 着色 |
| 折叠 | 渲染成可折叠的树节点 |
| 差异 | 递归对比两棵树,标记 add/del/mod |
整个工具就是一个 HTML 文件,双击就能开。没有安装包、没有更新提示、没有后台进程——JSON 全程在内存里,关掉即消失。
心得:选型时先问"我到底需要什么"。本地小工具的最高境界是双击即用、关掉即走。
4. 核心实现一:格式化与语法高亮
格式化本身一行:
const formatted = JSON.stringify(JSON.parse(input), null, indent);
但语法高亮要自己来。最稳的做法是用正则 tokenizer 切分 token,按类型着色:
function highlight(json) {
return json.replace(
/("(?:\\.|[^"\\])*"\s*:?)|(\b-?\d+\.?\d*\b)|(\btrue\b|\bfalse\b)|(\bnull\b)/g,
(m, str, num, bool, nul) => {
if (str) return `<span class="json-key">${str}</span>`;
if (num) return `<span class="json-num">${num}</span>`;
if (bool) return `<span class="json-bool">${bool}</span>`;
if (nul) return `<span class="json-null">${nul}</span>`;
}
);
}
几个让体验"顺手"的细节:
- 错误定位到行列。
JSON.parse抛错时带message,但原生不给位置。我用一个轻量json-parse-even-better-errors思路——失败时二分定位非法字符,提示"第 N 行第 M 列"; - 缩进可切换。2 空格 / 4 空格 / Tab 三档,顶部一排切换;
- 大文件虚拟渲染。几千行的 JSON 不全塞进 DOM,只渲染可视区,滚动时按需补。
心得:JSON 工具的核心指标是"贴进来不卡、看下去不晕"。性能和可读性缺一不可。
5. 核心实现二:两份 JSON 的差异对比
差异对比是这类工具的灵魂。递归比较两棵树,标记四种状态:
function diff(a, b, path = '') {
if (a === b) return []; // 相同
if (a == null || b == null) return [{path, type: 'mod', from: a, to: b}];
if (typeof a !== typeof b) return [{path, type: 'mod', from: a, to: b}];
if (Array.isArray(a) && Array.isArray(b)) {
return diffArrays(a, b, path); // 数组按索引比
}
if (typeof a === 'object') {
return diffObjects(a, b, path); // 对象按键比
}
return [{path, type: 'mod', from: a, to: b}]; // 原始值变了
}
diffObjects 里对键做三种判断:
function diffObjects(a, b, path) {
const result = [];
for (const k of new Set([...Object.keys(a), ...Object.keys(b)])) {
if (!(k in a)) result.push({path: `${path}.${k}`, type: 'add', to: b[k]});
else if (!(k in b)) result.push({path: `${path}.${k}`, type: 'del', from: a[k]});
else result.push(...diff(a[k], b[k], `${path}.${k}`));
}
return result;
}
渲染时,用路径定位到树节点,add 染绿、del 染红、mod 染黄并显示新旧值。几个让差异"秒懂"的细节:
- 差异列表单独一栏。不只染色,还列出"
.user.name: ‘a’ → ‘b’"这种可读条目; - 点击差异条目跳转到树中对应节点。大对象里不用自己翻找;
- 数组按索引比,不按内容比。这是有意的——接口返回的数组顺序通常有意义,按索引比才能发现"第 3 个元素变了"。
心得:差异工具的核心指标是"不用展开整棵树就能看懂哪层变了"。差异列表 + 路径跳转,比纯染色直观十倍。
6. UI 定制:JSON 工具也是"自己的工作台"
本地工具最大的特权,是 UI 完全我说了算。我给自己定了几个商业软件不会这么做的设计选择:
- 双栏对照:左原始 / 右格式化,或左旧 / 右新(对比模式),顶部一排切换;
- 等宽字体 + 行号。长 JSON 没行号根本没法定位错误;
- 暗色为主,跟随系统。长时间看 JSON,暗色更护眼;
- 错误不弹窗。解析失败时输入框红框 + 下方一行红字提示行列,不打断输入流。
这些设计在商业产品里几乎不可能出现——它们太"个人化"了。但本地工具的意义,恰恰就是让软件贴合你的调试习惯,而不是让你迁就软件的功能预设。
7. 三个让我卡了一阵的坑
坑一:JSON.parse 错误不带位置。 原生只给 Unexpected token x in JSON at position N,但 N 是字符偏移不是行列。我自己写了个 offsetToLineCol(text, offset) 把偏移转成行列——任何面向人的错误提示都要给行列,不给偏移。
坑二:超大 JSON 渲染卡死。 几 MB 的 JSON 全塞进 DOM,浏览器直接冻。改成只渲染前 1000 行 + 虚拟滚动后丝滑。教训:任何"可能很大"的文本渲染,虚拟滚动是必选项,不是优化项。
坑三:循环引用。 JSON.stringify 遇到循环引用会抛错。虽然纯 JSON 不会有循环,但用户偶尔会粘 console.log 打印的对象(带循环)。我用 WeakSet 检测循环并标注 [Circular],不让工具崩。
8. 结尾:开发者工具的"双栏对照"哲学
做完这个 JSON 工具,我对"开发者工具"有了一种更深的体会:
- 它不抢你注意力。没有广告、没有登录、没有"猜你想格式化什么";
- 它尊重你的数据。JSON 全程本地,带 token 的接口返回放心贴;
- 它尊重你的时间。贴进来即格式化,对比即高亮,不需要点"运行"。
在这个所有软件都在抢用户时长的时代,一个"贴即格式化、关掉即走"的本地工具,反而成了一种奢侈。
如果你也常处理 JSON、也想要一个安静的格式化 + 对比工具,我强烈建议自己写一个。技术门槛比想象中低,一个 HTML 文件就够;真正难的是克制住加功能的冲动——不做 JSON Schema 校验、不做"AI 解释字段"、不做"分享我的 JSON",就做一个老老实实的本地处理器。
工具小而静,价值往往不在多,而在"它永远在那里,贴一下就懂"。
- 点赞
- 收藏
- 关注作者
评论(0)