编辑器、命令行与自动化工具链
结论:编辑器解决「看得清、改得对」,命令行解决「批量、可重复」。两边各有几个能用十年的工具。

结论:编辑器按场景选——工具站与移动端用 CodeMirror 6,需要完整 IDE 体验才上 Monaco;批量与自动化交给 jq 加语法/结构/风格三层 CI 校验,脚本传参一律用 jq --arg。
手工检查 JSON 的时代该结束了:能让机器重复做的事,不要靠人眼。
一、浏览器里的编辑器
1.1 CodeMirror 6 的最小集成
import { EditorView, basicSetup } from 'codemirror';
import { json, jsonParseLinter } from '@codemirror/lang-json';
import { linter, lintGutter } from '@codemirror/lint';
const view = new EditorView({
parent: document.getElementById('editor')!,
doc: '{"hello": "json"}',
extensions: [
basicSetup,
json(),
linter(jsonParseLinter()),
lintGutter(),
EditorView.updateListener.of((update) => {
if (update.docChanged) {
// 防抖后再做重活,别在每次按键时格式化
scheduleParse(update.state.doc.toString());
}
}),
],
});
四个要点:
json()提供语法高亮与括号匹配,不要自己写;jsonParseLinter()给出行列级错误标记,配合lintGutter()在行号旁显示图标;updateListener必须防抖,每次按键都JSON.parse十万行会明显卡;- 大文档要开
EditorView.lineWrapping之外的优化,避免一次性渲染整篇(CodeMirror 6 默认按视口渲染,这点比旧版好)。
1.2 折叠与大纲
CodeMirror 6 内置折叠能力,JSON 场景下常按「对象/数组」折叠:
import { foldGutter, foldKeymap } from '@codemirror/language';
import { keymap } from '@codemirror/view';
配上 foldGutter() 与 keymap.of(foldKeymap),用户可以用快捷键收起长数组。
1.3 与 Monaco 的取舍
| 维度 | CodeMirror 6 | Monaco |
|---|---|---|
| 体积 | 小,模块化 | 大(数 MB) |
| 移动端 | 支持较好 | 体验一般 |
| 语言服务 | 需自建 | 内置丰富 |
| 定制成本 | 低 | 中 |
工具站、移动端优先 → CodeMirror;需要完整的 IDE 体验(多文件、跳转、补全)→ Monaco。
二、命令行:jq 的六个高频配方
jq 是 JSON 领域的 sed + awk,语法只学一小部分就能覆盖八成日常需求。
# 1. 取字段(最常用)
jq '.data.items[].name' response.json
# 2. 过滤 + 重组
jq '.data.items[] | select(.price < 10) | {id, price}' response.json
# 3. 统计(长度、求和、最大)
jq '[.items[] | .price] | {count: length, total: add, max: max}' response.json
# 4. 分组聚合
jq '[.items[]] | group_by(.category) | map({category: .[0].category, count: length})' response.json
# 5. 合并多个文件成数组
jq -s '.' part-*.json > merged.json
# 6. 逐行处理 JSON Lines
jq -c 'select(.level == "error")' app.jsonl
# 常用输出开关
jq -r '.data.name' # 去掉引号,方便管道给 shell
jq -c '.' # 紧凑输出,一行
jq --arg id "$ID" '.id == $id' # 安全传参,避免拼接注入
--arg 这一条值得单独记:直接把变量拼进表达式会被内容里的引号破坏,--arg 由 jq 负责转义。
2.1 其他工具一览
| 工具 | 用途 | 一句话 |
|---|---|---|
yq |
YAML/JSON/XML 互转 | 语法与 jq 类似,配置类文件主力 |
dasel |
多格式统一查询 | 支持 JSON/YAML/TOML/XML/CSV |
fx |
交互式 JSON 浏览 | 终端里点点看,适合探索 |
jless |
终端分页浏览 | 类似 less,支持折叠与搜索 |
gron |
把 JSON 摊平成可 grep 的行 | 排查「这个字段在哪一层」利器 |
ajv-cli |
schema 校验 | CI 里校验 JSON 文件 |
gron 的思路特别值得知道:
$ echo '{"a":{"b":[1,2]}}' | gron
json = {};
json.a = {};
json.a.b = [];
json.a.b[0] = 1;
json.a.b[1] = 2;
摊平之后 grep 就能定位路径,比在编辑器里逐层展开快得多。
三、CI 里的三层校验
| 层次 | 工具 | 失败意味着 |
|---|---|---|
| 语法 | JSON.parse / jq empty |
文件本身写坏了 |
| 结构 | ajv-cli + schema |
字段缺失、类型不对 |
| 风格 | Prettier | 格式不统一,diff 噪音大 |
# 语法:批量校验仓库里所有 json
find . -name '*.json' -not -path './node_modules/*' -print0 \
| xargs -0 -n1 jq empty
# 结构:按 schema 校验
npx ajv-cli validate -s schemas/order.json -d 'fixtures/orders/*.json' --strict=false
# 风格:只检查不修改
npx prettier --check "**/*.json"
三层都要有,因为它们的错误信息完全不同:
jq empty 只说「哪一行坏了」,ajv 说「哪个字段不符合哪个关键字」,Prettier 只说「格式不一致」。
四、pre-commit 钩子
# .husky/pre-commit 示例(节选)
npx lint-staged
{
"lint-staged": {
"*.json": ["prettier --write", "jq empty"]
}
}
注意 package.json、tsconfig.json 这类工具配置文件不要轻易让 Prettier 重排缩进——
某些工具对格式敏感(虽然合法 JSON 不该敏感,但现实里踩过坑)。可以单独排除。
五、调试技巧
jq -e当断言用:表达式结果为false或null时退出码为 1,可直接写进脚本做检查;jq 'paths'列出所有路径,快速了解陌生文档的结构;jq '.. | objects | keys'找出所有出现过的键名,适合排查拼写不一致;- 编辑器里先格式化再对比:未格式化的 diff 全是噪音,
/diff类结构化对比更准。
六、检查清单
- [ ] 编辑器错误标记精确到行列,且解析做了防抖;
- [ ] 团队脚本统一用
jq --arg传参,不拼字符串; - [ ] CI 三层校验齐全(语法/结构/风格);
- [ ] pre-commit 只处理暂存文件,避免全量扫描拖慢提交;
- [ ] 工具版本在
package.json或.tool-versions中锁定。
常见问题
如何在 CodeMirror 6 与 Monaco 之间选择?
工具站、移动端优先选 CodeMirror 6:体积小、模块化、移动端支持较好,语法高亮、括号匹配与折叠都能按需组合。需要多文件、跳转、补全这类完整 IDE 体验时才上 Monaco,代价是数 MB 的体积与更高的定制成本。
为什么 CI 里要同时保留语法、结构与风格三层校验?
三层报错信息完全不同:jq empty 只说哪一行坏了,ajv 说哪个字段不符合哪个关键字,Prettier 只说格式不一致。缺一层就会让另一层的错误被误读,所以三层都要有,且前一层失败时立即停止。
能否只靠 pre-commit 而不在 CI 里校验?
不能只靠 pre-commit。钩子只处理暂存文件,能在一分钟内给反馈,但无法覆盖全量与绕过提交的改动;CI 负责求全。常见做法是 pre-commit 只跑暂存文件,全量扫描放在 CI。