在线工具怎么做:JSON 格式化与文本对比的实现细节
在线工具怎么做:JSON 格式化与文本对比的实现细节
博客里放在线工具有一个很实际的好处:它们既能被使用,也能反过来成为文章素材。
第一批工具选择了:
- JSON 格式化
- 文本对比
两者都能完全在浏览器运行,不需要把用户输入传到服务器。除了响应更快,也减少了敏感数据离开浏览器的风险。
一、JSON 格式化的最小核心
核心只有几行:
function formatJSON(input) {
const value = JSON.parse(input);
return JSON.stringify(value, null, 2);
}
但一个可用页面还需要:
- 错误提示
- 行号
- 树形预览
- 复制
- 导出
- 输入防抖
- 大内容滚动
二、输入防抖
每次按键都立即解析 JSON,输入较大内容时会频繁计算。
let debounceTimer;
textarea.addEventListener('input', function () {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(formatJSON, 500);
});
500ms 内继续输入就重置计时器。用户停下来后再格式化。
防抖不是为了显得前端很专业,而是避免用户输入一个左花括号时,页面立刻郑重宣布 JSON 不合法。
三、行号同步
行号是一个独立 pre:
<pre id="line-numbers">1</pre>
<textarea id="json-input"></textarea>
根据换行数更新:
function updateLineNumbers() {
const count = Math.max(textarea.value.split('\n').length, 1);
lineNumbers.textContent = Array.from(
{ length: count },
(_, index) => index + 1,
).join('\n');
}
滚动同步:
textarea.addEventListener('scroll', function () {
lineNumbers.scrollTop = textarea.scrollTop;
});
字体、字号、行高和上下 padding 必须完全一致,否则行号会越往下偏得越明显。
四、JSON 树形预览
树形视图使用一个固定版本的第三方 formatter,放在:
web/static/js/vendor/json-formatter.js
页面脚本只负责调用:
const formatter = new JSONFormatter(parsed, 2, {
hoverPreviewEnabled: false,
animateOpen: true,
animateClose: true,
});
preview.appendChild(formatter.render());
formatter.openAtDepth(3);
第三方源码和自己的页面逻辑分开,未来替换库时不会在一个文件里拆炸弹。
五、文本对比先做行级匹配
文本按行拆分:
function splitLines(text) {
return text
.replace(/\r\n/g, '\n')
.replace(/\r/g, '\n')
.split('\n');
}
统一换行符非常重要。Windows 的 \r\n 和 Unix 的 \n 如果不归一化,视觉上相同的文本可能被判定为不同。
行级 diff 输出操作:
equal
insert
delete
replace
统计增删行数:
if (tag === 'insert') additions += j2 - j1;
if (tag === 'delete') deletions += i2 - i1;
if (tag === 'replace') {
deletions += i2 - i1;
additions += j2 - j1;
}
六、replace 还需要行内高亮
如果整行都变成红色和绿色,读者仍然要自己找哪里变了。
因此对 replace 行再做一次 token diff。
分词规则:
function tokenize(line) {
return line.match(
/\s+|[\u4e00-\u9fff]|[A-Za-z0-9_]+|[^\sA-Za-z0-9_\u4e00-\u9fff]+/g
) || [];
}
它把内容分成:
- 空白
- 单个中文字符
- 连续英文和数字
- 标点
然后对 token 序列做匹配,把变化部分包进:
<span class="diff-inline-del">旧内容</span>
<span class="diff-inline-add">新内容</span>
这不是完整语义 diff,但对代码、JSON 和中英文文本都比整行染色更有用。
七、用户输入必须转义
对比内容最终通过 innerHTML 渲染高亮标签,因此用户原文必须先转义:
function escapeHTML(value) {
return String(value)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
只有工具自己生成的 <span> 可以进入 HTML。
否则拿下面内容做对比:
<img src=x onerror=alert(1)>
页面就会从文本对比工具升级成 XSS 演示工具,而且完全免费。
八、让空白差异可见
有些差异肉眼看不见:
- 行尾空格
- 连续空格
- Tab
可以显示为:
空格 -> ·
Tab -> →
只在有歧义时显示,普通单个空格保持原样。否则一段正常文本会看起来像铺满芝麻。
九、结果表为什么需要固定布局
左右对比表包含六列:
左行号 | 左符号 | 左内容 | 右行号 | 右符号 | 右内容
关键 CSS:
.diff-table {
table-layout: fixed;
width: 100%;
}
.diff-table .gutter {
width: 48px;
}
.diff-table .sign {
width: 24px;
}
.diff-table .code {
white-space: pre-wrap;
overflow-wrap: anywhere;
}
行号和符号列固定,内容列平分剩余空间。长 JSON 不会把右侧直接挤出屏幕。
移动端改为上下输入布局,结果区域仍可横向或纵向滚动。
十、纯前端工具的边界
这些工具不把内容上传服务器,因此:
- 不记录输入
- 不保存历史
- 不需要登录
- 刷新后内容消失
这是刻意的产品边界,不是功能没写完。
如果未来增加历史记录,就必须同时考虑:
- 数据是否敏感
- 是否加密
- 保存多久
- 谁能读取
- 用户如何删除
一个“顺便保存一下”的按钮,背后经常跟着一整份隐私设计。
十一、验证清单
JSON 工具:
- 空输入
- 非法 JSON
- 深层对象
- 大数组
- 中文和 Unicode
- 复制与导出
文本对比:
- 完全相同
- 单行插入
- 单行删除
- 多行替换
- 中文变化
- HTML 输入
- 行尾空格
- Windows 换行
- 超长单行
在线工具真正的价值不在于代码量,而在于把边界处理完整。核心算法可能只有十行,剩下的一百行通常都在处理现实世界。
评论
暂无评论,来说点什么吧。
请先登录后再发表评论
去登录