我用单个 HTML 文件构建了一个 JSON diff 工具(无需构建步骤)

发布: (2026年1月7日 GMT+8 11:49)
4 min read
原文: Dev.to

Source: Dev.to

概览

我经常需要并排比较 JSON 负载(API 响应 vs 预期、前后对比、生产 vs 暂存)。每次我都会把它粘贴到随便的在线工具里,盯着看,然后几分钟后又重复这个过程。

为了解决这个问题,我构建了 fknjsn.com —— 一个本地优先的 JSON 对比工具,整个工具只在一个 HTML 文件中,无需构建步骤

  • 通过 CDN 引入 Vue 3(全局构建)
  • 通过 CDN 引入 vue-json-pretty 用于树形渲染
  • 使用 localStorage 实现持久化

就这些 —— 没有 webpack、Vite、node_modules,也没有 package.json。只需要一个 index.html,甚至可以直接邮件发送给别人。

设置

整个应用只需两个 <script> 标签和对 Vue 全局导出的解构:

const { createApp, ref, computed, watch, onMounted, nextTick } = Vue;

// ... the rest is just Vue

全局构建虽然不时髦,但能用。没有打包工具,没有 import map —— 只用像 2014 年那样的 <script> 标签,却仍然拥有响应式框架。

任意位置粘贴的用户体验

应用全局监听粘贴事件,但在 <input><textarea> 获得焦点时会忽略。这让你可以在页面任意位置粘贴 JSON,内容会自动落到选中的行上。

window.addEventListener('paste', (e) => {
  const tag = document.activeElement?.tagName?.toLowerCase()
  if (tag === 'input' || tag === 'textarea') return

  try {
    const json = JSON.parse(e.clipboardData.getData('text'))
    addJsonToSelectedRow(json)
  } catch {
    // not valid JSON, ignore
  }
})

递归搜索

每个 JSON 块都有自己的搜索输入框。过滤器会遍历整棵树,并在有任意子节点匹配时保留父节点。概念上的实现如下:

function filterJson(obj, search) {
  if (!search) return obj
  const lower = search.toLowerCase()

  if (Array.isArray(obj)) {
    const filtered = obj
      .map(item => filterJson(item, search))
      .filter(item => item !== undefined)
    return filtered.length ? filtered : undefined
  }

  if (obj && typeof obj === 'object') {
    const result = {}
    for (const [key, value] of Object.entries(obj)) {
      if (key.toLowerCase().includes(lower)) {
        result[key] = value
      } else {
        const filtered = filterJson(value, search)
        if (filtered !== undefined) result[key] = filtered
      }
    }
    return Object.keys(result).length ? result : undefined
  }

  // primitives
  const str = String(obj).toLowerCase()
  return str.includes(lower) ? obj : undefined
}

防抖持久化

状态会保存到 localStorage,但采用 500 ms 的防抖,以避免在快速变更时频繁写入存储:

watch(rows, () => {
  clearTimeout(saveTimeout)
  saveTimeout = setTimeout(() => {
    localStorage.setItem('json-rows', JSON.stringify(state))
  }, 500)
}, { deep: true })

扩展思路

如果工具需要处理更大的 JSON 负载或增加更多功能,我会考虑:

  • 为大体积过滤添加 Web Worker
  • 为树形视图使用 虚拟滚动
  • 实现真正的 diff 视图,而不仅仅是并排比较

目前它正好满足我的需求,整个项目也能轻松装进我的脑袋里。

链接

  • 在线演示: https://fknjsn.com
  • 源码: 页面 view‑source(整个应用就在那儿)

名字的发音正如你想的那样。

Back to Blog

相关文章

阅读更多 »

React 组件中的 TypeScript 泛型

介绍:泛型并不是在 React 组件中每天都会使用的东西,但在某些情况下,它们可以让你编写既灵活又类型安全的组件。