Cairn
← 返回博客

在线工具怎么做: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, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

只有工具自己生成的 <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 换行
  • 超长单行

在线工具真正的价值不在于代码量,而在于把边界处理完整。核心算法可能只有十行,剩下的一百行通常都在处理现实世界。

评论

暂无评论,来说点什么吧。

请先登录后再发表评论

去登录