手写高性能在线 JSON 工具|Web Worker 工程化打包 + 语法自动修复 + 多语言代码生成实战
前言:解决开发者的JSON高频痛点
日常开发中,JSON 是接口调试、配置编辑、日志排查的核心数据格式,但原生开发场景始终存在诸多痛点:单行压缩JSON难以阅读、语法报错无法精准定位、嵌套层级过深结构混乱、需要手动转换各类编程语言实体类。
市面上多数在线工具功能零散、存在广告、部分存在数据上传泄露风险,大文件解析还会出现页面卡顿问题。
这个 JSON 页面不是临时拼出来的小工具,而是我在真实工具站里持续打磨的一部分。最开始做它,是因为日常开发里处理 JSON 的频率太高了:数据一大页面就卡,错误提示不够直观,树形结构一深就不容易看清,代码生成和格式转换也总得在多个工具之间来回切换。
本文会围绕这些真实问题,拆解这个 纯前端 JSON 工具页面 的核心实现。表面上看,它讲的是 JSON;实际上更值得复用的是一套 Web Worker 落地思路:当前端页面里存在解析、格式化、转换、压缩这类重任务时,怎样把计算从主线程拆出去,同时不把工程结构搞乱。
阅读提示:先解释几个高频名词
manifest:可以理解成"构建产物清单",这里用来记录 Worker 最终带 hash 的文件名content hash:文件内容指纹。内容一变,文件名就变,适合处理浏览器缓存snapshot:一次解析后的结果快照,包含格式化结果、报错信息、统计信息等safe/lossy:safe表示基本不改原始语义;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.parse、JSON.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.tsx、useJsonFormatter.ts 和 useJsonWorker.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-core、js-yaml、xmlbuilder2一次性打进首屏
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 |
| 约 0.93 MB | |
| Slack | 约 11.7 KB |
| 微信 | 约 6.8 KB |
| 约 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.ts 与 src/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 花样,而是三件事:
- 先基于 JSON 构建一棵可折叠的
MindMapNode树 - 再用行式布局算法递归计算坐标
- 最后交给 SVG + 绝对定位按钮渲染,并补上滚轮缩放与拖拽平移
这样做的好处是,布局逻辑和渲染逻辑解耦,后续无论想换成 Canvas、React Flow 还是别的可视化方案,数据层都可以复用。
思维导图渲染效果
【JSON 思维导图可视化界面】采用横向行式布局,支持画布缩放、拖拽平移、节点折叠,自动标注数组长度与对象属性数量,清晰梳理深层嵌套接口 JSON 整体层级结构。
3.6 11种编程语言代码自动生成(解决:重复写样板代码)
这一节的重点不是"支持多少种语言",而是 如何把多语言生成统一收口到一个 Worker 转换入口里。当前实现分成两类:
TypeScript与MySQL Schema走内置生成器,方便做更细的定制Java / Python / Go / Rust / Swift / Kotlin / C# / C++ / PHP走quicktype-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 内容,保护敏感调试数据隐私。
功能执行逻辑:
- 用户点击【分享】按钮 → 获取当前输入的 JSON 字符串
- 通过 LZ-String 对 JSON 进行高比例压缩 + URL 安全编码
- 将压缩字符串拼接至当前页面 URL hash(
#d=...) - 更新浏览器 URL(
history.replaceState,不刷新页面) - 自动复制链接到剪贴板,弹窗提示成功
- 他人打开链接,页面加载时自动解析 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_start、loading_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 工程化实践。