简介:sketch-json-cli 是一款面向 Sketch 设计协作与版本管理场景的命令行工具,适合前端工程师、设计系统维护者以及需要将设计稿纳入代码仓库管理的团队使用。它解决的核心问题是 Sketch 二进制文件难以直接 diff 与追踪变更,通过命令行即可在 .sketch 与 JSON 之间双向转换,让设计文件也能像代码一样做版本对比与流程化处理。资源包共 8 个文件,以 json 配置、js 主逻辑脚本、md 说明文档为主,另含 yml 持续集成配置、license 授权文件与 lock 依赖锁定文件,压缩包约 34KB,体量轻便、结构清晰。目前已有 390 人学习下载。借助其中的入口脚本与依赖清单,读者可以快速完成全局安装并上手转换命令,理解 CLI 参数设计思路,进而把设计稿转换环节接入自动化流程,为设计版本管理与团队协作提供可复用的工具基础。
1. sketch-json-cli:草图文件与 JSON 互转,到底在解决什么问题
设计稿和代码之间的鸿沟,很多时候不是设计工具本身造成的,而是「格式不互通」造成的。Sketch 文件本质上是一个 ZIP 包,里面塞满了二进制 plist、JSON 碎片和资源文件,直接拿文本编辑器打开就是一堆乱码。团队想用脚本批量改图层名、想用 Git 做版本对比、想把设计稿里的颜色和间距抽出来喂给代码生成器,都会卡在「读不进去」这一步。sketch-json-cli 这类工具要干的事很明确:把 .sketch 文件解成人类可读、程序可解析的 JSON,改完之后再原路打包回 .sketch,让 Sketch 能正常打开。它适合三类人:需要批量处理设计稿的前端工程师、想给设计系统做自动化校验的团队、以及希望把设计数据接入 CI 流程的 DevOps。核心价值不是「转换」这个动作,而是转换之后你能对 JSON 做什么——diff、merge、脚本批处理、接入 json 查询函数做字段提取,这些才是真正省时间的地方。
2. 拆开 .sketch 的黑匣子:文件结构与 JSON 映射关系
2.1 为什么 .sketch 能转成 JSON
Sketch 从 43 版本之后就把文件格式改成了 ZIP 归档。你把design.sketch的后缀改成.zip,解压出来会看到这样的目录结构:
design.sketch/ ├── document.json ├── meta.json ├── user.json ├── pages/ │ ├── page-uuid-1.json │ └── page-uuid-2.json ├── images/ │ └── ... └── previews/ └── preview.pngdocument.json存的是全局设置和页面索引,meta.json记录版本和插件信息,pages/目录下每个 JSON 对应一个画板页面,里面是图层树。图层树本身就是嵌套的 JSON 对象,每个节点有_class、do_objectID、frame、style等字段。也就是说,Sketch 官方自己就是用 JSON 来描述设计稿的,sketch-json-cli 做的事情是帮你自动完成「解压 → 定位 JSON → 修改 → 重新压缩」这条链路,而不是发明一种新格式。
2.2 转换工具的核心逻辑
一个可靠的转换流程分四步走。第一步,把 .sketch 当 ZIP 读,用内存流解压,避免落盘产生临时文件。第二步,遍历所有.json条目,解析成对象树,同时保留images/里的二进制资源不动。第三步,把解析后的对象树序列化成格式化 JSON 输出,或者接受外部修改后的 JSON 重新写回。第四步,按原目录结构重新打包成 ZIP,改后缀为 .sketch。
这里有个关键点:Sketch 对 JSON 的键顺序不敏感,但对do_objectID的唯一性和引用关系非常敏感。如果你在 JSON 里删了一个图层节点,但别处还有symbolID或sharedStyleID指向它,Sketch 打开时就会报错或者丢内容。所以转换工具通常会在写回前做一次引用完整性检查。
2.3 用 Node.js 跑通最小转换
下面这段代码演示了不依赖任何第三方 CLI,直接用 Node.js 内置模块完成 .sketch 到 JSON 的提取:
const fs = require('fs'); const path = require('path'); const AdmZip = require('adm-zip'); // 需要 npm install adm-zip // 将 .sketch 文件解压并提取所有 JSON 内容 function sketchToJson(sketchPath, outputDir) { const zip = new AdmZip(sketchPath); const entries = zip.getEntries(); entries.forEach(entry => { // 只处理 .json 文件,图片等二进制资源跳过 if (entry.entryName.endsWith('.json')) { const content = entry.getData().toString('utf8'); const parsed = JSON.parse(content); // 解析验证,格式错误会在这里抛出 const outPath = path.join(outputDir, entry.entryName); fs.mkdirSync(path.dirname(outPath), { recursive: true }); // 格式化输出,方便人工阅读和 git diff fs.writeFileSync(outPath, JSON.stringify(parsed, null, 2), 'utf8'); console.log(`提取: ${entry.entryName}`); } }); } sketchToJson('./design.sketch', './output');逻辑说明:AdmZip把 .sketch 当普通 ZIP 读,getEntries()拿到所有条目。只对.json后缀做解析和格式化输出,二进制资源原样保留在 ZIP 里不提取。JSON.parse在这里起到校验作用,如果 Sketch 文件损坏导致 JSON 不合法,会直接抛异常,方便定位问题。
参数说明:sketchPath是输入文件路径,outputDir是输出目录。JSON.stringify的第三个参数2控制缩进空格数,设成0可以压缩体积,设成2或4方便阅读。生产环境如果文件很大,建议用流式处理替代getData()全量读取。
2.4 从 JSON 还原回 .sketch
反向操作要把修改后的 JSON 重新塞回 ZIP 结构:
const AdmZip = require('adm-zip'); const fs = require('fs'); const path = require('path'); // 将 JSON 目录重新打包为 .sketch 文件 function jsonToSketch(jsonDir, originalSketchPath, outputPath) { // 以原始 .sketch 为模板,保留图片等二进制资源 const zip = new AdmZip(originalSketchPath); const entries = zip.getEntries(); entries.forEach(entry => { if (entry.entryName.endsWith('.json')) { const jsonPath = path.join(jsonDir, entry.entryName); if (fs.existsSync(jsonPath)) { const newContent = fs.readFileSync(jsonPath, 'utf8'); JSON.parse(newContent); // 写回前再次校验,防止非法 JSON 破坏文件 zip.updateFile(entry.entryName, Buffer.from(newContent, 'utf8')); console.log(`更新: ${entry.entryName}`); } } }); zip.writeZip(outputPath); console.log(`已生成: ${outputPath}`); } jsonToSketch('./output', './design.sketch', './design-modified.sketch');逻辑说明:以原始 .sketch 作为模板而不是从零构建 ZIP,这样images/和previews/里的二进制资源自动保留。遍历原始 ZIP 中的 JSON 条目,如果输出目录里有同名文件就替换内容。写回前再做一次JSON.parse校验,避免手工编辑引入语法错误导致 Sketch 打不开。
参数说明:jsonDir是修改后的 JSON 目录,originalSketchPath是原始文件路径(提供二进制资源模板),outputPath是输出路径。zip.updateFile会覆盖同名条目,不会新增重复项。
注意:不要用
zip.addFile往已有条目上追加,会产生重名条目,Sketch 解析时行为不可预期。必须用updateFile。
3. 批量处理与自动化:把转换接进工作流
3.1 批量转换多个草图文件
单个文件转换只是起点,实际项目里往往有几十个页面文件需要统一处理。下面这个脚本遍历目录下所有 .sketch 文件并批量导出 JSON:
#!/bin/bash # 批量将 sketch 文件转换为 json 目录 INPUT_DIR="./sketches" OUTPUT_BASE="./json-output" mkdir -p "$OUTPUT_BASE" for sketch_file in "$INPUT_DIR"/*.sketch; do # 取文件名去掉扩展名作为输出子目录名 basename=$(basename "$sketch_file" .sketch) out_dir="$OUTPUT_BASE/$basename" mkdir -p "$out_dir" node -e " const { sketchToJson } = require('./converter'); sketchToJson('$sketch_file', '$out_dir'); " echo "完成: $basename" done逻辑说明:用 Bash 做外层遍历,每个文件分配独立输出目录,避免不同文件的document.json互相覆盖。basename命令去掉.sketch后缀作为目录名,保证可追溯。
参数说明:INPUT_DIR和OUTPUT_BASE按实际路径调整。如果文件名包含空格,for循环需要改成find ... -print0配合while read -d ''的方式处理。
3.2 用 JSON 查询函数做字段提取
转换出来的 JSON 是嵌套图层树,想快速拿到所有文本图层的内容,可以用递归查询。下面这个函数提取指定页面下所有_class为text的节点:
// 递归提取图层树中所有文本节点的内容 function extractTextLayers(node, results = []) { if (!node || typeof node !== 'object') return results; // 命中文本图层,记录名称和内容 if (node._class === 'text' && node.attributedString) { results.push({ name: node.name, content: node.attributedString.string, frame: node.frame }); } // 递归遍历 layers 数组 if (Array.isArray(node.layers)) { node.layers.forEach(child => extractTextLayers(child, results)); } return results; } const pageJson = require('./output/pages/page-uuid-1.json'); const texts = extractTextLayers(pageJson); console.log(`共找到 ${texts.length} 个文本图层`); texts.forEach(t => console.log(`${t.name}: ${t.content}`));逻辑说明:图层树是递归结构,每个节点可能有layers子数组。判断_class === 'text'来识别文本节点,从attributedString.string取实际文案。返回结果包含名称、内容和位置信息,方便后续做文案校对或国际化提取。
参数说明:node是当前遍历节点,results是累积数组(默认空数组,递归时传入同一引用)。如果只想查特定页面,把对应pages/下的 JSON 传进来即可。
3.3 接入 Git 做版本对比
JSON 格式最大的好处是 Git diff 可读。把output/目录纳入版本管理后,每次设计稿更新都能看到具体哪个图层改了颜色、哪个文本换了内容。但要注意两点:一是do_objectID每次保存可能变化,导致 diff 噪音大;二是格式化缩进要统一,否则整个文件都会显示为改动。
常见做法是在转换后做一次 ID 归一化,把随机 UUID 替换成基于图层路径的稳定哈希。这样只有真正的内容变化才会出现在 diff 里。具体实现可以用crypto.createHash('md5').update(layerPath).digest('hex')生成稳定 ID,遍历时按路径拼接。
提示:如果团队用 CI 做设计稿校验,建议把 JSON 转换步骤放在 pre-commit hook 里,每次提交 .sketch 时自动更新 JSON 目录,保证两者始终同步。
4. 避坑与排查:转换过程中最容易翻车的 5 个点
4.1 转换后 Sketch 打开报「文件已损坏」
现象:JSON 改完打包回去,Sketch 提示文件损坏或直接闪退。
原因:最常见的是 JSON 语法错误,比如手工编辑时多了一个逗号、少了一个引号。其次是 ZIP 压缩级别或条目顺序和原始文件差异过大,Sketch 对 ZIP 结构有一定兼容性要求。
解决:写回前必须做JSON.parse校验,这一步不能省。另外用zip.updateFile而不是重建整个 ZIP,保持原有条目顺序。如果还是报错,用原始 .sketch 做二进制对比,确认meta.json里的version字段没有被改动。
4.2 图片资源丢失或显示为空白
现象:转换后图层位置和文字都在,但图片全部变成空白占位。
原因:反向打包时没有以原始 .sketch 为模板,而是从零创建 ZIP,导致images/目录下的二进制资源没有被包含进去。
解决:jsonToSketch必须以原始文件为模板,只替换 JSON 条目,二进制资源原样保留。如果原始文件已经丢失,需要从 JSON 里的imageRef字段找到资源引用,但资源本身无法凭空恢复。
4.3 图层 ID 冲突导致内容错乱
现象:打开后某些图层消失,或者多个图层重叠在一起。
原因:手工在 JSON 里复制粘贴图层节点时,do_objectID重复了。Sketch 用这个 ID 做唯一索引,重复会导致解析异常。
解决:任何新增图层节点都必须生成新的唯一 ID。可以用crypto.randomUUID()生成,格式保持和 Sketch 一致的大写 UUID。批量复制时写个脚本统一替换 ID 字段。
4.4 大文件转换内存溢出
现象:处理超过 100MB 的 .sketch 文件时 Node.js 进程崩溃,报JavaScript heap out of memory。
原因:AdmZip默认把整个 ZIP 读进内存,加上 JSON 解析后的对象树,内存占用可能是原文件的 5 到 10 倍。
解决:启动时加--max-old-space-size=4096提高内存上限,或者改用流式 ZIP 处理库。对于超大文件,建议按页面拆分处理,每次只加载一个pages/*.json,处理完释放引用。
4.5 JSON 键顺序变化引发无意义 diff
现象:每次转换后 Git 显示整个文件都被修改,但实际上内容没变。
原因:JSON.stringify默认按对象键的插入顺序输出,而不同版本的工具或手工编辑可能改变键顺序。
解决:在JSON.stringify的第二个参数传入一个固定的键排序数组,或者用递归函数对所有对象按键名排序后再序列化。这样只要内容不变,输出就完全一致。
5. 进阶技巧:用 JSON Patch 做精准修改与回滚
当你已经能稳定转换之后,真正提升效率的做法不是每次全量编辑 JSON,而是用 JSON Patch 做增量修改。JSON Patch 是一组操作指令,描述「把某个路径的值改成什么」,天然适合做设计稿的自动化调整和回滚。
假设你要把所有文本图层的字号从 14 改成 16,不需要遍历整个 JSON 树手动改,而是生成一个 patch 文件:
const jsonpatch = require('fast-json-patch'); // 原始 JSON 和修改后的 JSON 对比,自动生成 patch const original = require('./output/pages/page-uuid-1.json'); const modified = JSON.parse(JSON.stringify(original)); // 遍历修改字号(简化示例,实际需要递归定位) function bumpFontSize(node) { if (node._class === 'text' && node.style?.textStyle?.encodedAttributes?.MSAttributedStringFontAttribute) { const attr = node.style.textStyle.encodedAttributes.MSAttributedStringFontAttribute; if (attr.attributes?.size === 14) { attr.attributes.size = 16; } } if (Array.isArray(node.layers)) node.layers.forEach(bumpFontSize); } bumpFontSize(modified); // 生成 patch const patch = jsonpatch.compare(original, modified); console.log(JSON.stringify(patch, null, 2)); // 输出类似: [{ op: "replace", path: "/layers/0/.../size", value: 16 }]逻辑说明:jsonpatch.compare自动对比两个对象树,生成最小操作集。这个 patch 可以存成文件、可以审查、可以应用到其他类似结构的页面上。如果改错了,把 patch 里的op从replace改成反向操作就能回滚。
参数说明:original是基准 JSON,modified是修改后的 JSON。compare返回操作数组,每个操作包含op(操作类型)、path(JSON Pointer 路径)、value(新值)。fast-json-patch需要单独安装。
这套做法的好处在于:patch 文件体积极小,适合做 code review;可以批量应用到多个页面;出问题时回滚成本极低。我自己的习惯是每次批量修改前先跑一次compare生成 patch 存档,改完确认无误再合并,相当于给设计稿操作留了一颗后悔药。
还有一个实用技巧是结合json 查询函数做条件筛选。比如只对名称包含「Button」的图层做修改,可以在遍历时加一层名称匹配,避免误伤其他元素。这种「查询 + patch」的组合,比全量替换安全得多,也更适合接入自动化流水线。
希望帮到你。
本文还有配套的精品资源,点击获取