手写高性能在线 JSON 工具|Web Worker 工程化打包 + 语法自动修复 + 多语言代码生成实战

手写高性能在线 JSON 工具|Web Worker 工程化打包 + 语法自动修复 + 多语言代码生成实战

前言:解决开发者的JSON高频痛点

日常开发中,JSON 是接口调试、配置编辑、日志排查的核心数据格式,但原生开发场景始终存在诸多痛点:单行压缩JSON难以阅读、语法报错无法精准定位、嵌套层级过深结构混乱、需要手动转换各类编程语言实体类。

市面上多数在线工具功能零散、存在广告、部分存在数据上传泄露风险,大文件解析还会出现页面卡顿问题。

这个 JSON 页面不是临时拼出来的小工具,而是我在真实工具站里持续打磨的一部分。最开始做它,是因为日常开发里处理 JSON 的频率太高了:数据一大页面就卡,错误提示不够直观,树形结构一深就不容易看清,代码生成和格式转换也总得在多个工具之间来回切换。

本文会围绕这些真实问题,拆解这个 纯前端 JSON 工具页面 的核心实现。表面上看,它讲的是 JSON;实际上更值得复用的是一套 Web Worker 落地思路:当前端页面里存在解析、格式化、转换、压缩这类重任务时,怎样把计算从主线程拆出去,同时不把工程结构搞乱。

阅读提示:先解释几个高频名词

  • manifest:可以理解成"构建产物清单",这里用来记录 Worker 最终带 hash 的文件名
  • content hash:文件内容指纹。内容一变,文件名就变,适合处理浏览器缓存
  • snapshot:一次解析后的结果快照,包含格式化结果、报错信息、统计信息等
  • safe/lossysafe 表示基本不改原始语义;lossy 表示修复成功了,但可能改动了原始内容,需要用户确认
  • quicktype-core:一个把 JSON 样本转换成多语言类型定义的代码生成库
  • JSON5:比标准 JSON 更宽松的语法格式,支持注释、单引号、尾逗号等写法

核心挑战与解决方案速览

核心挑战 对应方案
大体积 JSON 解析时页面卡死 用 Web Worker 把解析、格式化、压缩放到子线程执行
Worker 构建容易和主包耦合 用 esbuild 独立构建 Worker,并通过 manifest 动态加载
原生报错信息不够友好 提取 position / line / column,统一转成可读错误提示
深层嵌套 JSON 很难浏览 提供树形视图 + 思维导图两种可视化方式
多语言实体类生成重复劳动多 内置生成器 + quicktype 组合生成代码
配置类数据需要频繁互转 在 Worker 中按需加载 YAML / XML / TOML 转换能力
调试数据分享容易丢上下文 用 LZ-String 压缩后写入 URL hash,实现可还原分享

工具效果预览

【JSON 在线工具界面】页面包含顶部工具栏实现 JSON 格式化、压缩、排序、示例、清空功能,底部工具栏提供 Unicode 转义与文件体积状态展示,左侧支持 JSON 输入、上传编辑,右侧提供树形、代码、多格式预览切换,附带语法纠错提示与多语言实体类、数据库 Schema 代码生成模块。

一、工具核心能力清单(先看它能解决什么)

这款 JSON 工具聚焦开发者高频场景,核心能力如下:

  • 智能格式化与压缩:一键美化层级、压缩为单行精简数据,适配展示、传输、存储不同场景
  • 精准语法校验自动修复:精准定位报错行列,自动修复中文引号、单引号、尾逗号、JS注释等常见语法错误
  • 双模式可视化:树形视图支持展开、编辑、删除与路径复制,思维导图直观展示深层嵌套层级结构
  • 多语言代码一键生成:支持TypeScript、Java、Python、Go、Rust等11种开发语言实体类/接口生成
  • 跨格式预览:支持 JSON 转 YAML、XML、TOML 预览,适配配置文件查看与迁移场景
  • 本地离线处理:基于Web Worker实现纯前端解析,所有数据本地运算,无上传泄露风险
  • 高效辅助能力:历史记录、快捷键、文件导出、分享链接,大幅提升调试效率

二、整体架构与执行流程(先建立全局视角)

为解决传统JSON工具主线程阻塞、报错模糊、功能割裂的问题,本工具采用Web Worker异步解析模块化拆分架构,统一解析、校验、修复、可视化、转换全链路流程,执行逻辑清晰、扩展性极强。
flowchart TB A"粘贴/上传JSON文件" --> B"Web Worker异步解析(不阻塞UI)" B --> C{"JSON语法合法?"} C -->|合法| D"格式化/压缩基础处理" C -->|非法| E"精准报错定位+智能自动修复" E --> F{"修复成功?"} F -->|成功| D F -->|失败| G"展示详细报错原因及修复建议" D --> H"多视图切换展示" H --> I"代码高亮视图" H --> J"可编辑树形视图" H --> K"层级思维导图视图" D --> O"一键跨格式/代码转换" O --> P"TS/Java/Go等多语言代码" O --> Q"YAML/XML/TOML配置格式"

核心架构优势:所有耗时解析运算脱离主线程,UI交互全程流畅,大体积JSON文件也不会出现页面卡死问题。

三、核心功能技术实现(按问题逐个拆开)

3.1 基于Web Worker异步解析(解决:大 JSON 页面卡顿)

原生JSON.parseJSON.stringify为同步阻塞方法,处理超大JSON文件时会冻结页面。本工具将所有解析、格式化、压缩任务放入Web Worker子线程,彻底解放主线程。

3.1.1 Worker 文件打包配置(让 Worker 能独立上线)

本项目采用 esbuild 独立构建 + manifest 加载 方案。这里的 manifest 可以理解成"Worker 文件清单":Worker 不走 Next.js 默认打包链路,而是独立编译输出到 public/workers/ 目录,再由主线程按清单读取真正的带 hash 文件名。

完整源码详见 GitHub 仓库,正文仅保留核心逻辑片段:

javascript 复制代码
// scripts/build-workers.mjs
const config = {
  entryPoints: ['src/workers/jsonFormat.worker.ts'],
  outdir: path.join(root, 'public', 'workers'),
  bundle: true,
  format: 'iife',
  target: ['es2020'],
  platform: 'browser',
  banner: { js: '/// <reference lib="webworker" />' },
  entryNames: '[name]-[hash]',
}

const result = await build({ ...config, metafile: true })

const manifest = {}
for (const [outputPath, output] of Object.entries(result.metafile.outputs)) {
  if (!output.entryPoint) continue
  manifest['jsonFormat.worker'] = toPublicWorkerPath(outputPath)
}
await writeManifest(manifest)

构建产物结构

复制代码
public/workers/
├── manifest.json              # Worker 文件清单(含 hash)
├── jsonFormat.worker-<hash>.js   # 编译后的 Worker

manifest.json 内容

json 复制代码
{
  "jsonFormat.worker": "/workers/jsonFormat.worker-<hash>.js",
}

优势

  • Worker 独立构建,不影响主 bundle 体积
  • Content hash 命名,线上可永久缓存
  • 构建脚本可单独 watch,开发热更新

构建流程

bash 复制代码
# 开发环境
node scripts/build-workers.mjs --watch

# 生产构建(集成到 package.json scripts)
node scripts/build-workers.mjs

3.1.2 主线程 Worker 封装(主线程只做协调)

当前工程把主线程逻辑拆到了 JsonFormatPage.tsxuseJsonFormatter.tsuseJsonWorker.ts 三层。这里你不用记住所有细节,只要抓住一件事:useJsonWorker.ts 只负责建线程、发消息、收消息,页面层不直接碰底层线程细节。
点击查看代码

复制代码
// src/features/hooks/useJsonWorker.ts
type ConvertWorkerRequest = {
  id: number
  action: 'convert' | 'snapshot' | 'getParsed' | 'compress' | 'sort'
  jsonData?: string
  version?: number | null
  lang?: CodeLanguageId | 'yaml' | 'xml' | 'toml'
  indentSize?: number
}

type ConvertWorkerResponse =
  | { id: number; ok: true; result?: string; snapshot?: JsonSnapshot; parsed?: unknown; stats?: JsonStats; version?: number | null }
  | { id: number; ok: false; error: string }

import workerManifest from '../../../public/workers/manifest.json'

const workerRef = useRef<Worker | null>(null)
const requestIdRef = useRef(0)
const pendingRef = useRef(
  new Map<number, {
    resolve: (value: WorkerSuccessPayload) => void
    reject: (error: Error) => void
  }>()
)

const getWorker = useCallback(() => {
  if (workerRef.current || typeof window === 'undefined') {
    return workerRef.current
  }

  const worker = new Worker(workerManifest['jsonFormat.worker'])

  worker.onmessage = (event: MessageEvent<ConvertWorkerResponse>) => {
    const data = event.data
    const pending = pendingRef.current.get(data.id)
    if (!pending) return

    pendingRef.current.delete(data.id)

    if (data.ok) {
      pending.resolve({
        result: data.result,
        snapshot: data.snapshot,
        parsed: data.parsed,
        stats: data.stats,
        version: data.version ?? null,
      })
    } else {
      pending.reject(new Error(data.error || 'Code conversion failed'))
    }
  }

  worker.onerror = () => {
    for (const pending of pendingRef.current.values()) {
      pending.reject(new Error('JSON worker crashed'))
    }
    pendingRef.current.clear()
    worker.terminate()
    workerRef.current = null
  }

  workerRef.current = worker
  return worker
}, [])

const post = useCallback((payload: Omit<ConvertWorkerRequest, 'id'>) => {
  const worker = getWorker()
  if (!worker) {
    return Promise.reject(new Error('JSON worker is unavailable'))
  }

  const requestId = requestIdRef.current + 1
  requestIdRef.current = requestId

  return new Promise<WorkerSuccessPayload>((resolve, reject) => {
    pendingRef.current.set(requestId, { resolve, reject })
    worker.postMessage({ ...payload, id: requestId } satisfies ConvertWorkerRequest)
  })
}, [getWorker])

const convert = useCallback(
  (lang: CodeLanguageId | 'yaml' | 'xml' | 'toml', version: number | null, jsonData?: string) => {
    const worker = getWorker()
    if (!worker) {
      return Promise.reject(new Error('JSON worker is unavailable'))
    }

    return post({
      action: 'convert',
      lang,
      version,
      jsonData,
    })
  },
  [getWorker, post]
)

页面层通过 useJsonFormatter.ts 把这套 worker 能力包成"可直接消费的状态"。编辑器实时输入写入 editorValue,防抖后的 input 再触发解析和转换,既保证输入流畅,也避免每次按键都把主逻辑重新跑一遍:

typescript 复制代码
// src/features/hooks/useJsonFormatter.ts
const [input, setInput] = useState('')
const [editorValue, setEditorValue] = useState('')
const debouncedInput = useDebouncedValue(editorValue, 300)

useEffect(() => {
  setInput(debouncedInput)
}, [debouncedInput])

useEffect(() => {
  void worker.post({
    action: 'snapshot',
    jsonData: input,
    indentSize,
  }).then((response) => {
    setSnapshot({
      ...response.snapshot,
      parsed: null,
    })
    setParsedDocument({
      version: response.version ?? null,
      data: null,
    })
  })
}, [indentSize, input, worker])

这层拆分后的好处很直接:

  • JsonFormatPage.tsx 只管界面和交互,不再承担 worker 生命周期管理
  • useJsonWorker.ts 只管异步通信,职责稳定,后续新增动作也更容易扩展
  • useJsonFormatter.ts 统一管理解析、预览、缓存和自动修复,页面状态不会再散落在一个超大组件里

如果你在当前工程里继续扩展功能,建议也沿用这套分层:页面组件负责展示,业务 hook 负责状态,worker hook 负责异步通信

3.1.3 Worker 子线程核心逻辑(把重活集中到一处)

当前线上实现并没有把所有逻辑都塞进一个大 switch,而是围绕 snapshot / compress / sort / convert / getParsed 这些动作组织。下面摘取最关键的部分:

typescript 复制代码
/// <reference lib="webworker" />

function createJsonSnapshotInWorker(input: string, indentSize: number) {
  try {
    const parsed = JSON.parse(input)
    const formatted = JSON.stringify(parsed, null, indentSize)
    return {
      rawValid: true,
      formatted,
      fixedText: null,
      fixKind: null,
      error: null,
    }
  } catch (error) {
    const { fixed, fixKind } = tryFixJSONInWorker(input, indentSize)
    return {
      rawValid: false,
      formatted: fixed ?? '',
      fixedText: fixed,
      fixKind,
      error: { raw: (error as Error).message },
    }
  }
}

async function convertInWorker(jsonData: string, lang: string): Promise<string> {
  switch (lang) {
    case 'typescript':
      return generateTypeScriptTypes(jsonData)
    case 'mysql':
      return generateAdvancedMySQLSchema(jsonData)
    case 'yaml': {
      const { default: jsYAML } = await import('js-yaml')
      return jsYAML.dump(JSON.parse(jsonData))
    }
    case 'xml': {
      const { create } = await import('xmlbuilder2')
      const data = JSON.parse(jsonData)
      const wrapped = Array.isArray(data) ? { root: { item: data } } : { root: data }
      return create().ele(wrapped).end({ prettyPrint: true })
    }
    case 'toml': {
      const { default: stringifyToml } = await import('@iarna/toml/stringify.js')
      return stringifyToml(JSON.parse(jsonData))
    }
    default:
      return await generateWithQuicktype(jsonData, lang)
  }
}

上面这套拆分有两个实际好处:

  • 主线程只负责状态与交互,耗时计算尽量留给 Worker
  • 转换库按需加载,避免把 quicktype-corejs-yamlxmlbuilder2 一次性打进首屏

3.1.4 性能基准测试数据(重点看结论,不必死盯数字)

测试环境

  • 浏览器:Headless Chrome 149(macOS)
  • 设备:Apple 芯片 Mac M3,16GB 内存(毫秒级数据更适合横向参考,不适合作为绝对标准)
  • 测试页面:https://geekformat.com/zh-CN/json/format/
  • 测试方法:
    • JSON.parse 主线程、Worker、格式化、压缩:每个场景取 3 次平均值
    • 思维导图:使用 100节点500节点1000节点2000节点 样本逐个加载后测量首次切换与一次节点重排
    • LZ-String:使用页面同款 compressToEncodedURIComponent

1. JSON 解析性能对比(主线程 VS Worker)

JSON 大小 主线程/Worker耗时(ms)
0.5 MB 0.6/65.8
1 MB 1.4/64.3
3 MB 4.9/181.1
5 MB 4.5/278.9
10 MB 8.6/387.0

结论 :在当前测试机上,单纯 JSON.parse 的主线程耗时并不夸张;真正有意义的是把完整解析/格式化链路放到 Worker 后,页面交互仍能保持流畅,右侧视图切换与工具栏操作不会被长任务锁死。

2. 格式化 + 压缩耗时测试

JSON 大小 格式化/压缩/总耗时(ms)
0.5 MB 46.4/48.1/94.5
1 MB 61.5/60.7/122.3
3 MB 159.2/157.0/316.2
5 MB 302.0/349.7/651.7
10 MB 422.3/424.9/847.2

3. 思维导图渲染性能(千级节点)

节点数 耗时 (首次 / 重排)
100 61.6/55.6ms,60FPS
500 47.5/57.3ms,60FPS
1000 53.0/51.5ms,60FPS
2000 78.9/90.7ms,60FPS

4. LZ-String 压缩率测试

原始大小 压缩后 压缩率
1 KB 556 B 60.6%
10 KB 2.7 KB 29.4%
100 KB 13.9 KB 15.0%
0.5 MB 41.3 KB 8.9%
1 MB 67.8 KB 7.2%
3 MB 144.1 KB 5.1%
5 MB 161.4 KB 4.7%

结论

  • 浏览器:按本次实测结果,小于 1 MB 的样本都可以在主流浏览器中直接打开;到 1 MB 这一档时,Firefox 已不再稳妥。
  • 聊天工具:真正限制分享的不是浏览器,而是各聊天工具输入框所能承载的最大长度。稳定可分享的原始 JSON 体积通常只有 6 KB - 20 KB 左右,只有 WhatsApp 这类上限特别高的场景,接近 1 MB
聊天工具 可分享体积
Telegram 约 20.4 KB
Discord 约 19.6 KB
WhatsApp 约 0.93 MB
Slack 约 11.7 KB
微信 约 6.8 KB
QQ 约 8.8 KB
企业微信 约 11.7 KB
钉钉 约 7.0 KB
飞书 约 11.7 KB

3.2 语法错误精准定位(解决:报错信息看不懂)

原生语法报错通常只给出一段异常字符串,不利于快速排查。这里的做法很直接:先把异常里的 position / line / column 提取出来,再统一转换成界面可读的提示。

typescript 复制代码
function parseJsonErrorDetails(message: string, source: string) {
  const raw = message.trim()
  const summary = raw.replace(/\s+at position \d+(?:\s*\(line \d+ column \d+\))?/i, '').trim() || raw

  let position: number | null = null
  let line: number | null = null
  let column: number | null = null

  const fullLocationMatch = raw.match(/at position (\d+)(?:\s*\(line (\d+) column (\d+)\))?/i)
  if (fullLocationMatch) {
    position = Number(fullLocationMatch[1])
    line = fullLocationMatch[2] ? Number(fullLocationMatch[2]) : null
    column = fullLocationMatch[3] ? Number(fullLocationMatch[3]) : null
  } else {
    const lineColumnMatch = raw.match(/line (\d+) column (\d+)/i)
    if (lineColumnMatch) {
      line = Number(lineColumnMatch[1])
      column = Number(lineColumnMatch[2])
    }
  }

  if (position !== null && (line === null || column === null)) {
    const calculated = getLineColumnFromPosition(source, position)
    line = calculated.line
    column = calculated.column
  }

  return { raw, summary, position, line, column }
}

3.3 智能语法自动修复(解决:常见错误要手改半天)

日常开发中,复制粘贴的 JSON 常存在中文引号、单引号、JS 注释、尾逗号等非法格式。这里不是追求"万能修复",而是优先处理最常见、最接近合法 JSON 的那一类输入。

⚠️ 修复边界场景说明

自动修复存在以下已知局限,会在 UI 提示用户确认:

边界场景 风险 处理方式
字符串内容含引号 可能把值修坏 标记为 lossy,需用户确认
全角引号出现在字符串内部 可能破坏原始文本 标记为 lossy
括号或分隔符连续缺失 修复范围可能扩大 优先做结构修复
NaN / Infinity 不属于标准 JSON 交给 JSON5 兜底或提示手动调整

安全修复 vs 有损修复

这里的 lossy 可以理解成"修是修好了,但结果可能不是你原本想表达的意思",所以界面上需要额外提示用户确认。

typescript 复制代码
type FixKind = 'safe' | 'lossy'

// safe:规则匹配修复,不破坏数据
// lossy:可能改变语义,需用户确认

修复流程核心思路 (根据当前 jsonFormat.worker.ts 整理):

typescript 复制代码
// 极简版:先尝试标准 JSON,再做低风险修复,最后兜底
function tryFixJSONInWorker(input: string, indentSize = 2): {
  fixed: string | null
  error: string | null
  fixKind: 'safe' | 'lossy' | null
} {
  const trimmed = input.trim()
  if (!trimmed) return { fixed: null, error: null, fixKind: null }

  try {
    return { fixed: JSON.stringify(JSON.parse(trimmed), null, indentSize), error: null, fixKind: null }
  } catch {}

  const repaired = trimmed
    .replace(/'/g, '"')
    .replace(/,(\s*[}\]])/g, '$1')
    .replace(/([{,]\s*)([A-Za-z_$][\w$]*)\s*:/g, '$1"$2":')

  try {
    return { fixed: JSON.stringify(JSON.parse(repaired), null, indentSize), error: null, fixKind: 'safe' }
  } catch (error) {
    const structural = tryStructuralFix(repaired)
    if (structural) return { fixed: structural.fixed, error: null, fixKind: structural.lossy ? 'lossy' : 'safe' }

    try {
      return { fixed: JSON.stringify(JSON5.parse(trimmed), null, indentSize), error: null, fixKind: 'safe' }
    } catch {
      return { fixed: null, error: (error as Error).message, fixKind: null }
    }
  }
}

正文只保留了最核心的三层思路:标准解析 -> 低风险修复 -> 结构修复 / JSON5 兜底 。完整正则规则和结构修复逻辑建议直接看仓库里的 src/workers/jsonFormat.worker.tssrc/lib/jsonStructuralFix.ts

语法自动修复效果展示

【语法自动修复界面】工具支持 JSON 语法自动修复,自动兼容中文引号、单引号、尾逗号、注释等不规范写法,兼容 JSON5 宽松语法。

3.4 树形可视化结构(解决:嵌套层级看不清)

针对深层嵌套 JSON,纯代码视图可读性很差。当前实现会把 JSON 映射成可展开的树节点,支持展开/折叠、双击编辑、删除和路径复制。下面这段更偏思路示意,不是完整原样源码:

typescript 复制代码
function formatCopiedPath(path: string) {
  const parts = splitNodePath(path)
  if (parts.length === 0) return '$'

  return parts.reduce((acc, part) => {
    if (/^\d+$/.test(part)) return `${acc}[${part}]`
    if (/^[A-Za-z_$][\w$]*$/.test(part)) return `${acc}.${part}`
    return `${acc}[${JSON.stringify(part)}]`
  }, '$')
}

function updateNodeAtPath(root: unknown, path: string, newData: unknown): unknown {
  const parts = splitNodePath(path)
  if (parts.length === 0) return newData
  // 真实实现会递归克隆父层节点,保证编辑后的结构可安全回写
  return root
}

树形可视化编辑效果

【树形可视化界面】JSON 树形可视化面板支持数据展开折叠、双击编辑节点、删除字段、复制 JSON 路径,直观展示对象与数组嵌套结构,适配多层级复杂 JSON 快速查看与修改。

3.5 思维导图可视化(解决:只看局部不够)

针对超复杂嵌套 JSON,树形视图仍存在层级局限,思维导图模式可横向展开完整结构,直观展示数组长度、对象属性数量,快速梳理接口整体数据结构。

3.5.1 文本测量与节点构建(先把节点尺寸算准)

完整源码详见 GitHub 仓库,正文仅保留核心逻辑片段:

typescript 复制代码
// 极简版:先算文本宽度,再递归构造节点树
function measureMindMapText(text: string, fontSize = 13, weight = 600): number {
  const ctx = getMindMapMeasureContext()
  if (!ctx) return text.length * fontSize * 0.62
  ctx.font = `${weight} ${fontSize}px monospace`
  return ctx.measureText(text).width
}

function buildMindMapTree(
  data: unknown,
  label: string,
  path: string,
  expandedPaths: Set<string>,
  depth = 0
): MindMapNode {
  if (data === null || typeof data !== 'object') {
    return createLeafNode(label, data, path, depth)
  }

  const entries = Array.isArray(data)
    ? data.map((v, i) => [String(i), v] as const)
    : Object.entries(data as Record<string, unknown>)

  const children = entries.map(([k, v]) =>
    buildMindMapTree(v, k, joinNodePath(path, k), expandedPaths, depth + 1)
  )

  return createBranchNode(label, path, depth, children, expandedPaths)
}

正文只保留"测量文本宽度 -> 递归构造节点"的主干。像折叠状态、徽章宽度、最大深度保护、节点统计这些细节,放完整源码里更适合,读起来不会淹没主线。

3.5.2 行式布局算法(让节点排布稳定)

思维导图采用行式布局(Tree Layout),递归计算节点坐标。正文只看主干逻辑就够了:

完整源码详见 GitHub 仓库,正文仅保留核心逻辑片段:

typescript 复制代码
// 极简版:先排子节点,再让父节点垂直居中
function positionMindMapRows(node: MindMapNode, leftEdgeX: number, startY: number): number {
  node.x = leftEdgeX

  if (node.collapsed || node.children.length === 0) {
    node.y = startY
    return startY + MINDMAP_ROW_GAP
  }

  const childLeft = leftEdgeX + (node.path === 'root' ? MINDMAP_ROOT_DISTANCE : MINDMAP_DEPTH_DISTANCE)
  let cursorY = startY

  node.children.forEach((child) => {
    cursorY = positionMindMapRows(child, childLeft, cursorY)
  })

  const firstChild = node.children[0]
  const lastChild = node.children[node.children.length - 1]
  node.y = (firstChild.y + lastChild.y) / 2

  return cursorY
}

这里正文只保留"横向递进 + 纵向居中"的核心规则。完整布局构建、边集合扁平化、缩放和平移处理更适合直接看源码,不然初学者很容易在细节里迷路。

思维导图渲染层的关键并不是 UI 花样,而是三件事:

  1. 先基于 JSON 构建一棵可折叠的 MindMapNode
  2. 再用行式布局算法递归计算坐标
  3. 最后交给 SVG + 绝对定位按钮渲染,并补上滚轮缩放与拖拽平移

这样做的好处是,布局逻辑和渲染逻辑解耦,后续无论想换成 Canvas、React Flow 还是别的可视化方案,数据层都可以复用。

思维导图渲染效果

【JSON 思维导图可视化界面】采用横向行式布局,支持画布缩放、拖拽平移、节点折叠,自动标注数组长度与对象属性数量,清晰梳理深层嵌套接口 JSON 整体层级结构。

3.6 11种编程语言代码自动生成(解决:重复写样板代码)

这一节的重点不是"支持多少种语言",而是 如何把多语言生成统一收口到一个 Worker 转换入口里。当前实现分成两类:

  • TypeScriptMySQL Schema 走内置生成器,方便做更细的定制
  • Java / Python / Go / Rust / Swift / Kotlin / C# / C++ / PHPquicktype-core

3.6.1 统一代码生成架构(入口统一,依赖按需加载)

typescript 复制代码
// 极简版:统一入口,按目标分发
async function convertInWorker(jsonData: string, lang: string): Promise<{ result: string }> {
  if (lang === 'typescript') {
    return { result: generateTypeScriptTypes(jsonData) }
  }
  if (lang === 'mysql') {
    return { result: generateAdvancedMySQLSchema(jsonData) }
  }
  return { result: await generateWithQuicktype(jsonData, lang) }
}

// 第三方生成器按需加载,避免拖慢首屏
async function generateWithQuicktype(input: string, language: string): Promise<string> {
  const [{ quicktype }, { InputData, jsonInputForTargetLanguage }] = await Promise.all([
    import('quicktype-core/dist/Run'),
    import('quicktype-core/dist/input/Inputs'),
  ])

  const jsonInput = jsonInputForTargetLanguage(language as any)
  await jsonInput.addSource({ name: 'Root', samples: [input] })

  const inputData = new InputData()
  inputData.addInput(jsonInput)

  const result = await quicktype({
    inputData,
    lang: language as any,
    rendererOptions: { 'just-types': 'true', 'no-comments': 'true' },
  })

  return result.lines.join('\n')
}

正文保留的是"统一入口 + 按需加载"的主干。完整语言列表、输出清理规则和不同目标格式的分支细节,放在源码里看会更清楚,也更不容易打断阅读节奏。

3.6.2 TypeScript 接口递归生成(需要更细控制时走内置)

完整源码详见 GitHub 仓库,正文仅保留核心逻辑片段:

typescript 复制代码
// 极简版:递归推断类型,遇到对象时生成接口
function generateTypeScriptTypes(input: string): string {
  const usedNames = new Set<string>()
  const interfaces: string[] = []

  const infer = (value: unknown, hint: string): string => {
    if (value === null) return 'null'
    const valueType = typeof value

    if (valueType === 'string') return 'string'
    if (valueType === 'number') return 'number'
    if (valueType === 'boolean') return 'boolean'

    if (Array.isArray(value)) {
      if (value.length === 0) return 'unknown[]'
      const nonNull = value.find((v) => v !== null && v !== undefined)
      const sample = nonNull === undefined ? value[0] : nonNull
      const sampleType = infer(sample, `${hint}Item`)
      const hasNull = value.some((v) => v === null)
      const itemType = hasNull ? `${sampleType} | null` : sampleType
      return itemType.includes('|') ? `(${itemType})[]` : `${itemType}[]`
    }

    if (valueType === 'object' && value) {
      const obj = value as Record<string, unknown>
      const interfaceName = uniqueInterfaceName(hint, usedNames)
      const lines = Object.entries(obj).map(([key, val]) => {
        const propName = toSafeTsPropertyName(key)
        const optional = val === null ? '?' : ''
        const propType = infer(val, `${interfaceName}${toPascalCase(key)}`)
        return `  ${propName}${optional}: ${propType};`
      })
      interfaces.push(`export interface ${interfaceName} {\n${lines.join('\n')}\n}`)
      return interfaceName
    }

    return 'unknown'
  }

  const data = JSON.parse(input)
  const rootType = infer(data, 'Root')
  if (rootType === 'Root') {
    return interfaces.join('\n\n')
  }
  const body = interfaces.join('\n\n')
  return body ? `${body}\n\nexport type Root = ${rootType};` : `export type Root = ${rootType};`
}

这里正文只保留"递归推断 -> 对象生成接口 -> 数组推断元素类型"的主干。像命名规范转换、属性名转义、接口名去重这些辅助函数,直接看源码会更合适,也不会打断正文节奏。

生成效果示例:

typescript 复制代码
// 输入 JSON: { "users": [{ "name": "张三", "age": 25, "scores": [90, 85] }] }

// 输出 TypeScript:
export interface User {
  name: string;
  age: number;
  scores: number[];
}

export type Root = User[];

// 输出 Java (通过 quicktype):
public class User {
  private String name;
  private Integer age;
  private List<Integer> scores;

  public String getName() { return name; }
  public void setName(String name) { this.name = name; }
  public Integer getAge() { return age; }
  public void setAge(Integer age) { this.age = age; }
  public List<Integer> getScores() { return scores; }
  public void setScores(List<Integer> scores) { this.scores = scores; }
}

// 输出 Go (通过 quicktype):
type User struct {
  Name   string  `json:"name"`
  Age    int     `json:"age"`
  Scores []int   `json:"scores"`
}

3.6.3 MySQL Schema 生成(面向落表场景)

MySQL Schema 生成的核心是基于样本数据推断列类型,策略如下:

数据特征 推断类型 说明
全是整数 BIGINT 整数字段
全是小数 DECIMAL(p,s) 精确小数,p=总位数,s=小数位
全是布尔 BOOLEAN 真/假
数字字符串 BIGINT/DOUBLE 纯数字字符串转为数值类型
枚举 ≤10 种值 ENUM(...) 有限离散值用枚举
字符串且较长 VARCHAR(n) 动态长度,n=最大长度
混合类型 JSON 无法统一,用 JSON 存储

完整源码详见 GitHub 仓库,正文仅保留核心逻辑片段:

typescript 复制代码
function generateAdvancedMySQLSchema(input: string, tableName = 'root_table'): string {
  const data = JSON.parse(input)

  function analyzeValues(values: unknown[]) {
    // 1. 先扫描样本值,判断是整数、小数、布尔、字符串还是混合类型
    // 2. 如果是少量离散字符串,优先推断为 ENUM
    // 3. 如果类型混杂,就退回 JSON 字段
    return inferColumnType(values)
  }

  if (Array.isArray(data)) {
    return buildTableFromArray(data, analyzeValues, tableName)
  }

  if (typeof data === 'object' && data !== null) {
    return buildTableFromObject(data as Record<string, unknown>, analyzeValues, tableName)
  }

  return buildTableFromScalar(data, analyzeValues, tableName)
}

正文保留的是"样本推断类型 -> 按输入形态组装表结构"的主干。像字段聚合、枚举推断、时间戳列、SQL 细节拼接这些实现,更适合直接看源码。

这部分的实际价值不只是"能生成代码",而是把一次性的样板劳动前置自动化。对接口联调、模型草拟、数据库落表都很省时间。

多语言代码自动生成效果

【多语言代码自动生成界面】一键将 JSON 样本批量转换 TypeScript、Java、Python、Go、Rust 等 11 种编程语言实体类与接口,内置 MySQL 数据表结构生成逻辑,自动推断字段类型输出标准化模板代码。

3.7 JSON 到 YAML/XML/TOML 的转换预览(解决:配置格式来回切换)

这一块和代码生成共用同一套 Worker 转换入口,区别只是目标从"代码语言"变成了"配置格式"。因此这里不再重复展开通信逻辑,只看格式转换本身。

typescript 复制代码
async function convertInWorker(jsonData: string, lang: string): Promise<{ result: string }> {
  switch (lang) {
    case 'yaml': {
      const { default: jsYAML } = await import('js-yaml')
      return { result: jsYAML.dump(JSON.parse(jsonData)) }
    }
    case 'xml': {
      const { create } = await import('xmlbuilder2')
      const data = JSON.parse(jsonData)
      const wrapped = Array.isArray(data) ? { root: { item: data } } : { root: data }
      return { result: create().ele(wrapped).end({ prettyPrint: true }) }
    }
    case 'toml': {
      const { default: stringifyToml } = await import('@iarna/toml/stringify.js')
      return { result: stringifyToml(JSON.parse(jsonData)) }
    }
    default:
      throw new Error(`Unsupported format: ${lang}`)
  }
}

这里有三个值得注意的点:

  • YAML 直接基于 js-yaml.dump(JSON.parse(jsonData))
  • XML 需要对数组额外包一层 root.item
  • TOML 用 @iarna/toml,适合配置类数据预览

如果后续你要把它扩成真正的转换,也可以继续沿用这套 Worker 接口,把"字符串 -> JSON"的解析逻辑补进去即可。

格式转换效果展示

【格式转换界面】支持 JSON 一键转换 YAML、XML、TOML 三种主流配置文件格式,依托 Web Worker 子线程离线转换,无需后端服务,适配项目配置文件迁移与多格式预览场景。

3.8 基于 LZ-String 实现链接分享功能(解决:调试数据怎么快速共享)

在日常团队协作、接口调试场景中,我们经常需要把当前编辑好的JSON结构、调试数据快速分享给同事。传统方案需要复制代码、发送文件,操作繁琐且无法同步实时内容。

本工具内置 LZ-String 前端压缩分享方案。做法并不复杂:把完整 JSON 压缩后塞进 URL hash,点击分享按钮时自动复制链接;别人打开后再自动解压还原。

核心优势

  • 零服务端:纯前端压缩解析,无存储成本、无数据上传
  • 即时复用:分享链接打开后自动还原内容,无需二次粘贴
  • 轻量接入:实现成本低,适合工具类页面快速补能力
typescript 复制代码
// ==================== LZ-String 压缩工具 ====================
// 依赖:lz-string 前端压缩库
import LZString from 'lz-string'

// ==================== 生成分享链接 ====================
/**
 * 生成 JSON 分享链接
 * 使用 URL hash 存储压缩数据(避免污染 URL query 参数)
 */
const copyShareUrl = useCallback(async () => {
  if (typeof window === 'undefined') return

  try {
    // 1. 压缩 JSON 内容
    const nextHash = input ? 'd=' + LZString.compressToEncodedURIComponent(input) : ''
    // 2. 拼接 URL:保持原有 query 参数,添加 hash
    const url = `${window.location.pathname}${window.location.search}${nextHash ? '#' + nextHash : ''}`
    // 3. 更新浏览器 URL(不刷新页面)
    window.history.replaceState(null, '', url)
    // 4. 复制完整链接到剪贴板
    await writeClipboard(window.location.href)
    showToast('复制成功', 'success')
  } catch {
    showToast('复制失败', 'error')
  }
}, [input, showToast])

// ==================== 页面加载时解析分享链接 ====================
useEffect(() => {
  const hash = window.location.hash;
  // 优先解析 LZ 压缩数据(#d=...)
  if (hash && hash.startsWith('#d=')) {
    try {
      const encodedData = hash.slice(3)  // 去掉 '#d=' 前缀
      const decodedData = LZString.decompressFromEncodedURIComponent(encodedData)
      if (decodedData) {
        setInput(decodedData)
        return
      }
    } catch {
      // 忽略解析错误,继续尝试其他方式
    }
  }

// ==================== URL 格式说明 ====================
// 分享链接格式:/zh-CN/json/format#d=<LZ压缩数据>
// 示例:/zh-CN/json/format#d=N4IgLg9gJgpgziAXAbVBAxgGwIYGcCWAdg...

【生成专属可还原的链接】采用 LZ-String 本地压缩 JSON 数据存入 URL Hash 实现分享,全程无服务端上传存储,一键复制分享链接,打开链接自动解压还原完整 JSON 内容,保护敏感调试数据隐私。

功能执行逻辑

  1. 用户点击【分享】按钮 → 获取当前输入的 JSON 字符串
  2. 通过 LZ-String 对 JSON 进行高比例压缩 + URL 安全编码
  3. 将压缩字符串拼接至当前页面 URL hash(#d=...
  4. 更新浏览器 URL(history.replaceState,不刷新页面)
  5. 自动复制链接到剪贴板,弹窗提示成功
  6. 他人打开链接,页面加载时自动解析 hash、解压还原完整 JSON

注意

从前面的基准测试看,LZ-String 对 JSON 的压缩率很高,浏览器本身对长 URL 的容忍度通常也比想象中更强;但真正限制分享体验的,往往不是浏览器,而是微信、QQ、企业微信、Slack、飞书这类聊天工具的消息长度上限。实际使用里,一旦原始 JSON 体积偏大,哪怕浏览器还能正常打开,分享链接也可能在聊天窗口里被截断、折叠或发送失败。因此这个能力更适合分享小到中等体积的调试数据;如果内容再大,优先建议改用文件导出生成文件。

3.9 历史记录与快捷键优化(解决:高频操作太琐碎)

除了核心处理链路,体验层还补了两类小功能:历史记录缓存,以及开发时高频使用的快捷键。

快捷键(Windows/Mac) 对应功能
Ctrl/Cmd + Shift + F 一键格式化 JSON
Ctrl/Cmd + Shift + C 复制当前结果

3.10 工程落地踩坑(这些问题不提前处理,线上很容易出错)

上面讲的是"怎么实现",这一节补几个更偏工程落地的实际问题。

3.10.1 Next.js 默认构建链路不一定适合 Worker

这类页面里,Worker 往往依赖独立入口、动态依赖和更细的缓存控制。如果完全跟着 Next.js 默认打包走,后期很容易遇到路径不稳定、构建耦合和调试不方便的问题。

这也是为什么这里单独用了 scripts/build-workers.mjs。好处不是"更高级",而是更可控:Worker 的入口、产物位置、hash 规则、watch 方式都能单独处理。

3.10.2 带 hash 的 Worker 文件如果不做清单映射,主线程很难稳定引用

线上环境为了利用缓存,Worker 文件通常会带 content hash;但文件名一变,主线程硬编码路径就会失效。

这里的做法是多写一个 manifest.json,把"逻辑名称 -> 实际构建文件"映射起来。主线程只认逻辑名,不直接关心真实 hash 文件名,这样上线切版本时更稳定。

最小复现示例:

typescript 复制代码
// 旧版本还叫 jsonFormat.worker-AAA.js
const worker = new Worker('/workers/jsonFormat.worker-AAA.js')
// 新版本文件名变成 BBB 后,这里就会 404

3.10.3 Worker 不是没有内存问题,只是把压力挪走了

把任务放进 Worker 后,页面输入会流畅很多,但这不等于内存问题消失了。尤其是大对象、历史版本、未清理请求长期堆积时,Worker 本身也会涨内存。

当前实现里至少做了三件基础保护:

  • 页面卸载时主动 terminate
  • requestId 追踪请求,避免 pending 任务失控
  • 通过 version 复用已解析结果,减少重复传输和重复解析

最小复现示例:

typescript 复制代码
const worker = new Worker('/workers/jsonFormat.worker.js')
setInterval(() => worker.postMessage({ jsonData: bigJson }), 16)
// 不 terminate、也不清理 pending,请求会持续堆积

3.10.4 低版本浏览器兼容性要提前设边界

这套方案本质上依赖现代浏览器能力:Worker、动态 import、较新的构建输出格式。如果目标用户包含低版本 Safari 或更老的企业内网浏览器,就要提前确认构建目标和降级策略。

这也是为什么文中配置里会看到 target: ['es2020']format: 'iife' 这类设置。它们不是装饰项,而是在现代能力和兼容成本之间做平衡。

3.10.5 Web Worker 工具落地的架构自查清单

如果你准备把类似的重计算任务挪进 Worker,正式上线前可以先对照下面这张小清单自查一遍:

自查项 是否确认
组件卸载时,是否正确调用了 worker.terminate() [ ]
重型依赖(如 quicktype-core)是否放在 Worker 内部通过 import() 按需加载? [ ]
高频触发场景里,是否使用 requestId、节流或防抖来避免通信队列堆积? [ ]
如果遇到较老浏览器,是否准备了 try...catch 降级到主线程执行的兜底方案? [ ]

这张表看起来简单,但基本覆盖了 Web Worker 工具最容易在线上翻车的几类问题:线程回收、依赖体积、消息堆积和兼容性边界。真正进入生产环境时,往往不是"能不能跑",而是这些细节决定它能不能长期稳定地跑。

四、常见问题FAQ(开发者高频疑问解答)

基础使用类

Q1:格式化后文件体积变大,影响使用吗?

不影响。格式化会添加缩进、换行提升可读性,适合调试查看;若需要接口传输、文件存储,可直接使用压缩功能一键转为单行精简JSON,缩小体积。

Q2:所有JSON语法错误都能自动修复吗?

可自动修复常规语法问题 (中文引号、单引号、尾逗号、注释、非法空格);针对括号缺失、字段缺失等结构性严重错误,会精准定位位置,需手动微调。注意:自动修复可能破坏字符串内容(如包含引号)。

Q3:工具处理数据是否会上传服务器?

全程本地离线处理,基于浏览器 Web Worker 运算,所有 JSON 数据、配置内容不会上传至任何服务器,适配私密接口、敏感配置调试场景。

Q4:生成的多语言代码可以直接用于项目吗?

自动生成的接口、结构体、实体类可直接作为项目基础模板,简单复核字段类型、补充注释、调整可选字段后,即可投入正式开发,大幅减少重复编码工作。

工程化踩坑类

Q5:超大 JSON 文件(50MB+)处理会崩溃吗?

有可能。Worker 可以缓解主线程卡顿,但不能突破浏览器内存上限。实际项目里更稳妥的做法是:对超大文件做体积提示、限制分享长度,并在必要时建议用户拆分处理。

Q6:分享链接 URL 太长被截断怎么办?

要区分"浏览器能打开"和"聊天工具能发出去"这两件事。按上面的实测结果,浏览器对长链接的容忍度更高,但微信、QQ、企业微信、Slack、飞书这类聊天工具通常在 2000-3000 字符附近就会开始截断,对应到原始 JSON 往往只有 6 KB - 20 KB 左右。超过这个范围后,更适合改走文件、临时存储或服务端短链,而不是继续依赖 URL 直接分享。

Q7:Worker 内存问题怎么排查?

可以重点看三件事:

  • 是否在组件卸载时正确 terminate
  • 是否存在未清理的 pending request
  • 是否把大对象长期保存在 Worker 内存中

Q8:特殊 JSON 值(NaN、Infinity)解析失败?

JSON 标准不支持这些值。当前实现的思路是优先走标准 JSON 修复,必要时再用 JSON5 兜底。

二次开发类

Q10:如何新增一种代码生成语言?

如果你有自己的项目源码,可以参考文中的 Worker 转换接口设计,在 jsonFormat.worker.ts 一类的转换入口中继续扩展语言支持:

typescript 复制代码
case 'rust': {
  // 使用 quicktype 按需生成
  return { result: await generateWithQuicktype(jsonData, 'rust') }
}

Q11:如何扩展格式转换(如 Properties)?

如果你要在自己的项目里扩展更多配置格式,可以继续沿用这一层按需加载的转换结构:

typescript 复制代码
case 'properties': {
  const { default: Properties } = await import('some-lib')
  return { result: Properties.stringify(JSON.parse(jsonData)) }
}

五、后续优化方向:补全工程化最后一公里

需要先说明一点:本文前面提到的功能,已经在当前线上版本中稳定运行。这里补充的两项内容,不是"还没做完的核心能力",而是下一阶段更偏工程化的优化方向。

之所以把它们单独拿出来写,是因为当这类 Web Worker 重计算工具真的进入长期维护阶段后,问题往往不再是"功能能不能做出来",而是"能不能更容易复用、在更多设备上保持稳定体验"。

5.1 框架解耦:把 Worker 通信层从 React Hook 中抽出来

当前工程里的 useJsonWorker 已经足够清晰,适合 React 页面直接使用;但它本质上仍然是一个 Hook,这意味着如果后续要迁移到 Vue、Svelte 或原生 JavaScript 项目,就不能直接复用这一层。

更自然的下一步,是把 Worker 通信层抽成一个与框架无关的客户端类 ,例如 JsonWorkerClient,让 React 只保留一层很薄的封装。这样做不会改变现有页面的使用方式,但能把"线程管理、消息分发、请求追踪、资源释放"这些真正通用的逻辑沉淀成独立模块。

核心形态大致会像这样:

typescript 复制代码
class JsonWorkerClient {
  connect(url: string) { /* 创建 Worker 并绑定 onmessage */ }
  post(action: string, payload: unknown) { /* 建立 requestId 并发送消息 */ }
  disconnect() { /* terminate + 清理 pending */ }
}

这样处理之后,React 项目仍然可以继续暴露 useJsonWorker;而其他框架只需要在各自的生命周期里管理 client.connect()client.disconnect() 即可。对现有功能没有破坏,但会明显提升这套方案的跨项目复用性。

5.2 大体积依赖的加载体验优化

当前代码生成链路已经通过按需加载,避免了把 quicktype-core 直接打进主线程首屏;但在低端移动设备上,第一次动态加载这类依赖时,仍然可能出现肉眼可感知的等待。

后续更值得补的一步,是把"依赖正在加载"这件事显式暴露给主线程,同时在 Worker 内部对这类重依赖做单例缓存。

一个更实用的方向是:

  • 状态回传 :在 Worker 侧加载前后主动发出 loading_startloading_ready 这类状态,主线程据此显示按钮 loading、骨架屏或禁用态
  • 模块缓存 :在 Worker 模块作用域缓存 quicktype-core 的导入结果,让首次加载之后的同类请求直接复用

核心思路大概像这样:

typescript 复制代码
let quicktypeRuntime: QuicktypeRuntime | null = null

async function getQuicktypeRuntime() {
  if (quicktypeRuntime) return quicktypeRuntime
  quicktypeRuntime = await loadQuicktypeRuntime()
  return quicktypeRuntime
}

这类优化不会改变功能边界,但会明显改善"第一次点击代码生成"时的体感,尤其是在低端手机、弱网或浏览器性能较弱的环境里更有价值。

六、总结与复用价值

这套实现来自一个真实工具页场景。JSON 只是切入点,更值得复用的是背后的拆分方式:当页面里同时存在解析、转换、压缩、预处理这类重任务时,可以把计算下沉到 Worker,把交互和状态留在主线程。

如果要把本文的方法迁移到别的项目里,我觉得最值得保留的是四点:

  • 先判断瓶颈是不是"计算阻塞",再决定是否上 Worker
  • 页面、业务状态、Worker 计算三层尽量分开
  • 主线程和 Worker 之间用稳定的动作协议通信
  • 体积较大的依赖尽量按需加载,不要默认进入首屏链路

同样的思路,不只适用于 JSON。像日志解析、配置文件互转、本地文件处理、代码生成这类"计算明显比展示更重"的页面,也可以参考这种拆法。

Web Worker 能缓解主线程阻塞,但不能替代更基础的优化。数据规模控制、缓存策略、交互节流、错误边界处理,仍然是同样重要的工程问题。

完整演示与开源源码:

整套 Web Worker 重型计算架构可复用在日志解析、配置转换、本地文件处理等各类前端工具场景,完整源码已开源,欢迎 Star 交流优化思路。

如果你也在项目里用过 Web Worker,或者踩过缓存、兼容性、长链接分享这些坑,欢迎在评论区分享你的处理方式;如果这篇文章对你有帮助,也欢迎去仓库点个 Star,后面我会继续补更通用的 Worker 工程化实践。

相关推荐
不知疲倦的老鸟2 小时前
Node.js 库在浏览器里跑不了的教训
react.js·next.js
Momo__4 天前
TypeScript NoInfer<T>——精准控制泛型推断的工具类型
前端·typescript
退休倒计时5 天前
【每日一题】LeetCode 146. LRU 缓存 TypeScript
算法·leetcode·缓存·typescript
terry6005 天前
5G视频短信服务商选型全攻略:通道资源、架构能力与成本评估2026最新标准
大数据·人工智能·5g·json·asp.net·信息与通信·数据库架构
前网易架构师-高司机5 天前
带标注的辣椒病叶数据集,识别率95.9%,可识别三种病害和健康叶子,9916张图,支持yolo,coco json,voc xml,文末有模型训练代码
yolo·json·数据集·病害·叶病·病叶·辣椒
PixelBai5 天前
JSON扁平化使用教程:从入门到精通
json
kyriewen6 天前
TypeScript 高级类型:我用 infer 写了一个类型安全的 EventBus,终于搞懂了泛型约束
前端·javascript·typescript
倾颜6 天前
从本地 Ollama 到线上多模型 Runtime:接入 DeepSeek / Qwen 的实战复盘
langchain·next.js·deepseek