简介这是一款面向设计师与前端开发者的开源命令行工具 sketch-json-cli用于在 Sketch 设计文件与 JSON 数据之间双向转换便于设计稿版本管理、差异对比与自动化处理。资源压缩包共 8 个文件整体仅 34KB包含 2 个 JSON 配置/数据文件、1 个 JavaScript 主脚本、Markdown 说明文档、YAML CI 配置及许可证等结构紧凑可直接参考或二次开发。目前已有 390 人学习下载。借助该工具用户可将 .sketch 文件解析为可读、可搜索的 JSON 结构或从 JSON 还原为草图文件适用于需要接入设计数据流、跟踪设计资源变更或批量修改图层属性的进阶场景。附带的说明文档详细介绍了命令参数与使用示例能帮助理解 Sketch 文件内部格式及 CLI 工具的开发思路是探索设计自动化流程的实用样本。1. sketch-json-cli 是什么给设计稿文件一个可逆的 JSON 出口设计稿文件对很多开发团队来说是个黑匣子——平时只能靠设计工具打开脚本想读里面的图层名、页面结构、文本内容几乎无从下手。sketch-json-cli 这类工具解决的就是这个问题把草图文件完整地转换为 JSON 文本让脚本、差异对比、自动化检查都能拿到可读的数据需要交付或归档时再把 JSON 原样变回草图文件。整个过程可逆、无损转换前后的设计稿在工具里打开效果一致。它适合设计师做版本对比、前端读设计稿结构、以及任何想把设计稿纳入自动化流程的小团队。我最早遇到这个需求是在一个需要批量核对设计稿文本和图层命名的流程里。几十个草图文件横在面前纯靠人工打开一个个翻效率低到让人怀疑人生。把草图文件变成 JSON 之后一切突然就透明了。这篇文章把这条落地路径完整拆开格式原理、转换代码、参数设置、避坑清单以及进阶的自动差异对比用法。2. 为什么能用 JSON 承载草图从 zip 容器到文档树的拆解2.1 草图文件不是单一图片zip 容器里装的是文档树先说一个很多人不知道的事实常见的设计稿文件并不是单一编码的二进制格式而是一个 zip 容器。解压之后你能看到结构清晰的 JSON 文件、图片资源和元数据。如果你的工作流里只有「用设计工具打开」这一步那你永远看不到这层结构一旦用 zip 工具解开整个文档树就摊在面前了。我通常会在转换前先手动解包一次目的不是拿资源而是确认这份文件的内部布局。典型的文档树长这样路径内容document.json文档级配置包含页面列表、素材库引用、版本信息meta.json文件元数据包含应用版本、创建时间、文件格式版本pages/*.json每个页面的完整图层树、坐标、样式、文本内容images/ 或 assets/位图资源、图片参考这个结构意味着一个关键结论设计稿的「造型信息」本身就是 JSON。所谓转换为 JSON并不需要逆向解析只需要把 zip 里的 JSON 条目取出来所谓转回草图文件也只是按原结构重新打包。理解到这一层整个工具的实现思路就清晰了一大半。这也解释了为什么选择 JSON 作为中间格式是合理的它保留了图层树、样式、文本、布局的全部信息且是人类可读的。相比之下位图预览图只能看外观SVG 导出会丢失设计稿的分层和编辑信息。JSON 既不损失结构又方便脚本处理是双向转换最稳妥的载体。2.2 转换工具的核心选型zip 处理、JSON 读写与命令行骨架明确了格式本质后选型就很直接了。常见做法是选一个 Node.js 环境因为 JSON 处理是它的原生强项同时 zip 解包和打包都有成熟的库可用。你会用到三块能力zip 解包、zip 打包、JSON 读写。我一般不会引入设计软件相关的解析 SDK因为完全没有必要。设计稿文件的内部结构是标准的 zip JSON用通用工具就能处理。引入 SDK 反而会增加环境依赖让命令行工具跑不起来。这里唯一要注意的是 zip 库的选择解包用yauzl这类标准库打包用archiver两个库都有稳定的 API不会在压缩格式上做特殊处理。至于 CLI 骨架我的习惯是直接写 Node 脚本用process.argv解析参数不引入额外的框架。工具的核心逻辑就两步解包读取 JSON以及打包写回 JSON。参数设计得越少越好避免团队成员记不住命令。命令行入口只有两个子命令一个负责草图转 JSON一个负责 JSON 转草图。在动手写代码前还需要确认一个前提目标草图文件的 zip 包是否完整。有些文件在复制、传输过程中会损坏解包时会直接报错。所以我在转换脚本里会强制开启严格模式任何条目读不出来就立刻失败退出而不是跳过继续避免后面拿到一份残缺的 JSON。2.3 从草图文件到 JSON 的转换最小可运行的解包脚本下面这段代码是我会放在真实流程里的核心逻辑读取一个草图文件把内部的 JSON 条目全部解出来保持目录结构写入输出文件夹。代码用 Node.js 编写依赖yauzl和fs。// sketch2json.js草图文件 - JSON 目录 const fs require(fs); const path require(path); const yauzl require(yauzl); function sketchToJson(sketchFile, outDir) { // 输出目录必须存在避免写入时报 ENOENT fs.mkdirSync(outDir, { recursive: true }); return new Promise((resolve, reject) { yauzl.open(sketchFile, { lazyEntries: true, strictFileCuration: true }, (err, zipfile) { if (err) return reject(err); zipfile.readEntry(); zipfile.on(entry, (entry) { const fileName entry.fileName; // 跳过目录条目只处理文件 if (/\/$/.test(fileName)) { zipfile.readEntry(); return; } // 输出路径直接复用 zip 内路径保持 pages/ 等目录结构 const targetPath path.join(outDir, fileName); if (!targetPath.startsWith(outDir)) { // 防路径穿越恶意 zip 里可能带 ../必须拦截 zipfile.readEntry(); return; } fs.mkdirSync(path.dirname(targetPath), { recursive: true }); zipfile.openReadStream(entry, (err, readStream) { if (err) return reject(err); readStream.on(error, reject); // 以二进制方式写出不在这一层做任何编码转换 const writeStream fs.createWriteStream(targetPath); writeStream.on(error, reject); writeStream.on(finish, () { zipfile.readEntry(); }); readStream.pipe(writeStream); }); }); zipfile.on(end, resolve); zipfile.on(error, reject); }); }); } // 用法node sketch2json.js design.sketch ./json_out const [,, sketchFile, outDir] process.argv; sketchToJson(sketchFile, outDir).then( () console.log(converted), (e) { console.error(e); process.exit(1); } );这段代码的逻辑核心是逐步读取 zip 条目而不是一次性把所有文件载入内存。lazyEntries: true这个参数很关键它让yauzl等我们去主动调用readEntry()每处理完一个文件才读下一个避免大文件的条目列表一次性占用过多内存。strictFileCuration: true会对异常条目做严格校验文件损坏时尽早报错。我在代码里特意加了两层防护。第一层是路径穿越检查zip 条目名里如果带有../写入时会逃出目标目录这是压缩包处理的常见漏洞。第二层是fs.mkdirSync递归创建父目录确保pages/、images/这类子目录在写入前已经存在。输出时用二进制流直接管道不显式指定编码这样原始字节不会被意外改动。3. 把 JSON 变回草图文件反向转换的实现路径与参数设计3.1 JSON 转回草图文件的完整打包脚本反向转换不是把 JSON 文件逐个读出来那么简单关键在于重新打包 zip 时的参数控制。打包参数没设对生成的文件在设计工具里打开就会报「格式已损坏」。我在这部分踩过不少坑核心结论是压缩级别、条目顺序、文件编码三件事必须可控。下面这段代码是我用的打包脚本。它会遍历 JSON 输出目录里的所有文件按相对路径作为 zip 条目名重新生成草图文件。// json2sketch.jsJSON 目录 - 草图文件 const fs require(fs); const path require(path); const archiver require(archiver); function jsonToSketch(jsonDir, outputFile) { return new Promise((resolve, reject) { // 输出文件流archiver 会往这个流里写 zip 字节 const output fs.createWriteStream(outputFile); const archive archiver(zip, { // 固定压缩级别避免每次构建产生随机体积差异 zlib: { level: 6 } }); output.on(close, resolve); output.on(error, reject); archive.on(error, reject); // 把输出文件流接到 archiver 上然后开始写入 archive.pipe(output); // 遍历 JSON 目录把每个文件按相对路径写入 zip function walk(currentDir, prefix) { const entries fs.readdirSync(currentDir, { withFileTypes: true }); // 先列出再排序保证条目顺序稳定 entries.sort((a, b) a.name.localeCompare(b.name)); for (const entry of entries) { const fullPath path.join(currentDir, entry.name); const entryName prefix ? ${prefix}/${entry.name} : entry.name; if (entry.isDirectory()) { walk(fullPath, entryName); } else { // appendFile 以文件路径写入zip 条目名由第二个参数控制 archive.file(fullPath, { name: entryName }); } } } walk(jsonDir, ); archive.finalize(); }); } // 用法node json2sketch.js ./json_out output.sketch const [,, jsonDir, outputFile] process.argv; jsonToSketch(jsonDir, outputFile).then( () console.log(packed), (e) { console.error(e); process.exit(1); } );这段代码里最容易被忽略的是排序逻辑。readdirSync返回的条目顺序在文件系统层面是不确定的如果不加排序同一个 JSON 目录两次打包出来的 zip 条目顺序会有差异这会直接导致版本对比时出现大量无意义的差异行。排序后每次打包的字节序基本一致diff 工具的噪音会小很多。压缩级别level: 6是我试下来比较折中的选择。默认级别可能偏高大文件打包慢级别过低则输出体积偏大。实际使用中如果团队对文件体积特别敏感可以调到 9代价是打包耗时明显增加。我通常只在归档场景用 9日常转换用默认就好。还有一点必须补充archive.file()传入的第一个参数是磁盘路径第二个参数name才是 zip 内的条目名。如果直接用archive.file(fullPath)而不指定name打包出来的条目会带着完整磁盘路径打开时必然出问题。这是我最早踩过的一个隐蔽坑后面在避坑章节会展开细说。3.2 转回后的完整性校验打开前先验证这四步反转换完成后最忌讳的事情是直接丢给设计工具打开。打开报错再排查浪费的时间远超校验时间。我现在的习惯是转换之后立刻跑一套脚本校验四条检查全部通过才认为转换成功。第一用unzip -t验证 zip 完整性。这一步确认压缩包结构没有损坏是最基础的一层。# 校验 zip 完整性-t 表示 test unzip -t output.sketch # 预期输出末尾No errors detected in compressed data第二检查document.json里是否有页面列表字段。设计工具靠这个字段来决定打开后显示哪些页面。缺失或为空时文件能打开但内容空白这种失败最迷惑人。# 快速查看 document.json 里的页面引用数量 node -e const drequire(./json_out/document.json); console.log(d.pages ? d.pages.length : missing pages)第三把pages/目录下每个 JSON 文件用JSON.parse跑一遍。任何语法错误都会导致页面加载失败。这里要注意必须用原生JSON.parse不要用 eval 或 Function 构造避免执行文件里的未知代码。第四对比转换前后文件大小。草图文件转 JSON 再转回草图体积波动通常在 10% 以内这是压缩级别差异造成的正常范围。如果体积差了几倍那多半是资源文件在转换过程中被漏掉或压缩策略有问题需要回头检查images/目录是否完整。这套校验流程看起来多实际跑起来一条命令就完成。我通常会把它们合成一个verify.js脚本在 JSON 打包后自动执行。校验通过才把文件交给设计师校验失败直接终止流程并打印具体目录路径。这套机制救过我很多次值得放进你的工具链。4. CLI 参数设计与批处理转换怎么进入团队日常4.1 两个子命令与一套参数约定命令设计要少而稳工具做出来之后直接面对一个现实问题团队里不是每个人都记得住你脚本的调用方式。所以 CLI 参数必须少、稳定、语义清楚。我最开始的版本塞了十几个参数后来发现没人愿意用裁剪到现在只剩必要的那几个。草图转 JSON 的命令约定是sketch-json-cli sketch2json input.sketch -o ./json_outJSON 转草图的命令约定是sketch-json-cli json2sketch ./json_out -o output.sketch完整参数表如下参数作用默认行为-o, --output指定输出目录或文件自动生成时间戳目录--keep-temp保留中间临时目录转换成功后自动清理--no-assets跳过 images/assets 资源同时转换资源--prettyJSON 输出时格式化缩进保持原始压缩格式--pretty与--no-assets对草图文件体积影响很大默认关掉是稳妥的。--pretty这个参数我要多说一句。默认不加时转换输出的 JSON 是压缩状态体积小、比较快。加上之后会做缩进和换行适合拿去人工阅读或做结构性 diff。但它会显著改变文件体积和 diff 结果的颗粒度只要不是给人看的场景我都不建议开。退出码的设计也要定死0 表示成功1 表示参数错误2 表示转换失败。这样在 shell 脚本或者 CI 流程里可以直接拿退出码做分支判断不需要去解析 stdout 里的文本。错误信息统一写到 stderr避免和正常输出混在一起。4.2 批处理与增量转换上百个草图文件一次跑完单文件转换只是基础真正让团队效率提升的是批量能力。一个典型场景是某个版本的设计稿分了几十个文件要全部导出 JSON 做统一检查。一条 shell 循环就能解决问题。#!/usr/bin/env bash # 批处理全部草图文件每个文件独立输出目录 set -euo pipefail SRC_DIR~/design_files OUT_ROOT~/json_exports mkdir -p $OUT_ROOT count0 for sketch in $SRC_DIR/*.sketch; do name$(basename $sketch .sketch) out$OUT_ROOT/$name # 已存在的目录直接跳过实现增量转换 if [ -d $out ]; then echo skip $name else sketch-json-cli sketch2json $sketch -o $out count$((count 1)) fi done echo converted $count files这段脚本的核心是「目录存在就跳过」的增量策略。第一次全量转换后后续再跑只会处理新增或变更的文件速度很快也避免重复劳动。但这里有个前提如果设计稿内容变了但文件名没变增量策略会漏掉它。所以批处理脚本必须配合一个带超时校验的强制模式配合使用比如定期每周做一次全量刷新日常只做增量。我在批处理脚本里加上set -euo pipefail也是有意为之。set -e让脚本在任一命令失败时立即退出-u防止未定义变量pipefail确保管道中任何一段出错都不会被静默。批处理里如果某个文件转换失败静默跳过会让整个目录缺一块后续排查成本极高。还有一个细节值得注意批处理脚本的命名目录直接用了文件名如果两个不同目录下的草图文件重名输出目录会互相覆盖。真实流程里我一般会在输出目录前加上源目录的相对路径作为前缀或者在引入一个按修改时间命名的子目录彻底避免重名问题。这个改动虽小却能省掉不少让人头疼的文件覆盖事故。5. 转换实战中的避坑清单五类高频翻车现场与排查顺序5.1 转换后文件打不开设计工具报「格式已损坏」这是所有反向转换问题里出现频率最高的一个。表面现象是 JSON 转回草图文件后设计工具打开直接弹错误框不少新人第一反应是怀疑 JSON 数据损坏。我把这个坑拆开之后发现绝大多数原因不在 JSON 内容而在打包时的 zip 条目结构。常见的错误有两种一是把整个输出目录当作一个顶层文件夹打进了 zip导致文档 JSON 不在根目录二是archive.file()没有指定namezip 条目名带着完整磁盘路径。两种错误都会让设计工具找不到它期望的入口文件。解决办法是打包前对条目名做一层标准化。我在脚本里已经写了prefix逻辑实际流程中还需要加一条断言打包完成后用unzip -l列出条目确认document.json出现在列表的第一层而不是嵌套在某个目录下面。这一步检查可以在测试阶段自动化跑掉每次都验证能挡掉这个坑的九成以上。5.2 JSON 转回后图层全部丢失页面 JSON 没进对路径转换后的文件能打开但是整个画布是空的页面列表里什么都没有。这类失败比打不开更耗时间因为它表现得像「数据在但没加载出来」。排查到最后原因往往是pages/目录下的 JSON 文件没有被写回 zip。可能是在遍历目录时漏掉了深层子目录也可能是打包时把pages这个父目录本身过滤掉了。设计工具会扫描pages/目录来找页面文件找不到自然就显示空白。解决办法是在打包逻辑里对目录层级做完整性校验。打包完成后重新解包生成的文件对照原始 JSON 目录的文件清单逐项比对缺一个文件都算失败。我自己在工具里集成了一个--verify选项默认开启校验不过就退出并打印差异清单。这套思路完全是从生产环境的教训里提炼出来的没有捷径。5.3 中文字符与特殊文本乱码编码不统一是祸根转换后打开设计稿中文文本变成一串乱码或者 JSON 文件里直接出现\u转义序列。这类问题和 JSON 内容本身无关纯粹是编码处理不一致造成的。我在最初的脚本里犯过一个典型错误读取 JSON 条目时显式指定了utf8编码写回时又交给系统默认编码。在 Linux 和 macOS 上默认编码是 UTF-8但在某些 Windows 环境下默认编码可能是 GBK 或其他非 UTF-8 编码两边不一致就会产生乱码。解决办法是全程统一强制使用 UTF-8 且无 BOM。读写都用二进制流不做隐式编码转换JSON 内容在输出时显式用JSON.stringify生成写文件时指定encoding: utf8。BOM 头也要注意有些 JSON 解析器遇到 BOM 会直接判断为非法 JSON所以写入时一定不要带 BOM。5.4 每次转换 diff 结果巨大zip 打包顺序不稳定如果你把转换结果纳入版本管理就会发现一个恼人现象同样一份设计稿两次转换出来的 JSON 内容明明一样git diff 却显示上千行变更。这种情况会直接把自动差异对比的流程毁掉。原因有两个第一readdirSync的条目顺序在文件系统层面不确定导致打包顺序不稳定第二JSON 的 key 顺序在对象遍历时会按插入顺序输出而不同解析器处理后有差异。zip 格式本身对条目顺序不敏感所以文件能正常打开但 diff 时任何顺序差异都会被当成内容变更。解决办法是双管齐下。打包前对文件列表排序我已经在代码里体现了JSON 输出时固定 key 顺序可以用自定义的replacer参数对键名排序后再序列化。这样两次转换的结果在字节级别接近一致diff 才能回归到真正的设计改动上。5.5 超大设计稿转换内存溢出堆内存限制与时间成本一个几百 MB 的大草图文件转 JSON 时 Node 直接报JavaScript heap out of memory进程崩溃。常见做法里很多人会第一时间去调整NODE_OPTIONS但这个方向不对它只能延缓问题不能解决根本。真正的问题是代码把整个 JSON 内容一次性读进内存做处理。我见过最夸张的案例一份设计稿的页面 JSON 有几十 MB转成对象后加上引用关系内存占用直接膨胀好几倍。这种情况下调大堆内存确实能过关但每次转换都靠这个救命迟早翻车。更好的解决思路是流式处理。读取 JSON 时用JSON.parse配合stream分段解析或者干脆对大文件不做格式化直接搬运原始字节。工具里没有--pretty时就是走的这条路径。另外还可以设置一个体积阈值超过 100MB 的文件自动走流式通道小文件走常规通道速度和稳定性都能兼顾。6. 进阶技巧把转换结果做成设计稿的自动差异对比转换工具真正的价值不在转换本身而在转换之后能做什么。我目前用得最多的是把 JSON 接入自动差异对比让设计稿版本之间的改动变成一份可读的报告而不是靠人眼来回切换对比。具体的做法是两个版本的设计稿分别转成 JSON 目录再对关键文件做逐 key 对比。文本改动、图层位置变化、页面增删都能落到具体的字段上。// diff-sketch.js对比两个 JSON 目录的关键变更 const fs require(fs); const path require(path); function loadJson(relativePath, baseDir) { const full path.join(baseDir, relativePath); if (!fs.existsSync(full)) return null; return JSON.parse(fs.readFileSync(full, utf8)); } function diffKey(oldObj, newObj, prefix) { for (const key of Object.keys(newObj)) { const keyPath prefix ? ${prefix}.${key} : key; if (oldObj oldObj[key] ! undefined) { // 类型相同且都不是对象时比较值是否变更 if (typeof oldObj[key] ! object oldObj[key] ! newObj[key]) { console.log(CHANGED ${keyPath}: ${oldObj[key]} - ${newObj[key]}); } else if (typeof oldObj[key] object typeof newObj[key] object) { // value 字段单独处理避免把整个坐标对象都打出来 if (key value) { if (JSON.stringify(oldObj[key]) ! JSON.stringify(newObj[key])) { console.log(CHANGED ${keyPath}); } } else { diffKey(oldObj[key], newObj[key], keyPath); } } } else { console.log(ADDED ${keyPath}); } } } // 用法node diff-sketch.js old_version/page.json new_version/page.json const [,, oldFile, newFile] process.argv; const oldJson JSON.parse(fs.readFileSync(oldFile, utf8)); const newJson JSON.parse(fs.readFileSync(newFile, utf8)); diffKey(oldJson, newJson, );这段脚本的对比策略是刻意做了取舍的。它只关注三个东西新增的 key、值变化的value字段、以及结构层面的嵌套对象。像坐标数组这类频繁变动的字段我没有做逐位对比因为设计稿微调一下位置就会造成大量噪音。真正的变更报告需求绝大多数时候关注的是文本、名称、引用关系这些有业务含义的字段。我个人的习惯是把这个 diff 脚本接到批处理流程的最后一步转换完成自动跑一轮对比输出一份变更清单再归档。有一回就是因为这个脚本定位到某个页面的文本字段被误改在交付前及时拦住了问题。从那之后我每次做批量转换都会随手把差异对比打开这个习惯已经变成了流程的一部分。如果你正在做设计稿管理的自动化希望这套思路能帮到你。本文还有配套的精品资源点击获取