简介:面向Sketch设计师与前端开发者的实验性插件json-sketchapp,可将JSON文件直接转换为Sketch可编辑的设计文件,解决从数据源快速生成UI设计稿、实现设计自动化的问题。压缩包共9个文件,以JSON配置、JavaScript脚本为主,辅以PNG图标、Markdown说明文档和依赖锁文件,整体仅81KB,结构轻量清晰。目前已有671人学习下载。资源内含完整的skpm插件工程,包含插件清单、核心命令脚本、示例数据文件、依赖配置及自定义Babel配置说明,覆盖从安装构建到调试运行的完整链路。透过阅读源码与示例,能快速掌握Sketch插件开发流程、JSON转Sketch的实现思路以及skpm构建工具的使用方法,适合希望拓展插件开发技能或探索设计稿自动化生成路径的开发者实践学习。
1. 1. json-sketchapp 解决的是「JSON 到设计稿」这最后一公里
做设计系统、做运营配置后台、做国际化文案管理的人,十有八九都撞过这样一面墙:数据侧已经把文案、结构、配色整理成了干净的 JSON,设计侧却还在手工建画板、逐个拖文本框、对着参数表敲字号。json-sketchapp这类把 JSON 文件转换成 Sketch 文件的插件,就是来拆这面墙的——它把「数据 → 设计稿」的转换变成一条可重复的流水线,让批量出图、数据同步、多语言设计稿生成都有了自动化基础。这篇文章会从 Sketch 文件本身的构成讲起,带你搭一个最小可用的导入插件,再把文本、颜色、symbol 这些高频场景的参数调整方法一次说透,最后聊聊我做这套方案时踩过的坑。适合已经会写一点 JavaScript、想在 Sketch 里做重复劳动自动化的前端或设计师。
2. 先看文件本质再写插件:Sketch 文件、图层树和 JSON 的映射关系
很多人第一次拆 .sketch 文件时都会愣一下:这个设计工具的原生格式,居然不是某种二进制私有格式,而是一个 zip 压缩包。搞清楚这一点,你后面所有的工作——不管是写插件还是直接在命令行里生成 .sketch——都会变得豁然开朗。
2.1 .sketch 不是魔改的位图:一个 zip 包里的几种 JSON
把任意一个 .sketch 文件的后缀改成 .zip 再解压,你能看到一套清晰的结构。入口是document.json,它记录了页面引用、图层树、样式数据、symbol 定义等几乎所有内容;旁边还躺着meta.json和user.json,前者保存版本号、应用信息、页面 ID 列表,后者保存用户自己加的一些非破坏性数据。你还会看到preview/目录,里面是一张可供系统快速预览的位图。不同版本里这个结构会有些微调整,比如某些版本把每个 page 单独拆成文件放进pages/目录,但document.json作为总入口这一点始终没变。
这个本质决定了你的插件有两种完全不同的实现路线。一种是在 Sketch 进程里跑,用官方提供的sketch模块一层层创建图层,再让 Sketch 自己负责序列化与保存;另一种是不开 Sketch,直接用 JS 构造document.json的完整数据结构,用 JSZip 打包成 .sketch 文件。第一种适合交互式导入,用户能在画布上看到结果后手动微调;第二种适合 CI 流水线,服务器上没有任何设计工具也能产出可预览的设计文件。我建议先掌握第一种,它的心智负担小、出错时可借助 Sketch 的撤销功能找回,等你对图层的 JSON 字段足够熟悉之后再上第二种。
2.2 把 JSON 字段映射成图层树:一张映射表定全局
无论走哪条路线,你都得先定一套「业务 JSON → Sketch 图层」的映射规则。这里最忌讳的是在代码里为每一种业务字段写死逻辑,因为需求一变你的插件就要跟着改。常见做法是给 JSON 约定一个通用结构,让输入数据自带type和children,插件按这几个通用类型递归渲染。我一般会把类型收敛成四种:group表示容器、text表示文本、rect表示色块、symbol表示复用组件,界面元素再复杂,最终都能拆成这四样。
映射表长这样:JSON 里的x、y、width、height直接对应 Sketch 图层的frame;name对应图层名,也是设计稿里团队协作的锚点;opacity对应图层的透明度;children递归生成子图层。文本类型额外拿text字段当显示内容,拿style.fontSize、style.color、style.align控制字形;rect则拿style.backgroundColor或style.fill决定填充。映射规则定好后,你的插件代码就只剩一层薄薄的翻译逻辑,新增一种 UI 组件时也只需要扩展group的组合方式,而不是往插件里堆分支。
2.3 插件如何被 Sketch 加载:manifest 是起点
Sketch 插件本质上也是一个目录,只要里面放一个manifest.json文件并声明命令入口,Sketch 启动时就会扫描并加载它。这个发现机制直接决定了你插件的组织方式。一个典型结构是:根目录里放manifest.json,旁边放一个plugin.js存放命令处理函数,再加上package.json声明开发依赖和构建脚本。你在 Sketch 的 Plugins 菜单里看到的每一个菜单项,都由manifest.json里的commands数组声明;数组里的某个命令挂了哪个脚本文件、哪个函数,也全在声明里写明白。
如果你以前写过 Atom、VS Code 插件,对这个模型应该很眼熟:命令即函数,菜单即数组。manifest.json里有两个字段容易理解错:identifier是全局唯一标识,直接影响 Sketch 的偏好设置存储路径,一旦发布就不能随便改,否则用户会丢失已保存的配置;handler对应脚本文件导出的函数名,Sketch 在调用时会把一个带有当前文档、选中图层等信息的context对象传进来。后续步骤里我会带你把这些字段落成一个能跑的插件。
3. 搭出最小可用的 JSON 导入插件:从 manifest 到首图落地
这一章的目标很简单:双击插件菜单里的一个命令,弹窗选择一个 JSON 文件,当前页面里就出现转好的图层。代码不长,但请你一行行敲一遍,因为后面所有复杂功能都是在这个骨架上长出来的。
3.1 工程目录和 manifest.json 的最小写法
先建一个目录,名字随意,比如json-to-sketch-plugin。目录下放两个文件:manifest.json和plugin.js。manifest.json里声明一个命令,对应导入行为:
{ "identifier": "com.example.json-to-sketch", "name": "JSON to Sketch", "version": "1.0.0", "description": "Convert a JSON file into Sketch layers", "commands": [ { "name": "Import JSON File", "identifier": "import-json", "script": "plugin.js", "handler": "onImportJSON", "shortcut": "cmd shift j" } ], "menu": { "title": "JSON to Sketch", "items": ["import-json"] } }这段声明里有三个设计点值得你多看一眼。第一,script只写文件名不写目录前缀,Sketch 会把它解析为相对 manifest.json 的同级路径,所以你的脚本最好跟 manifest 平级或放在子目录里并写完整相对路径。第二,handler的值必须和plugin.js里导出的函数名完全一致,包括大小写,否则菜单点了没反应。第三,shortcut不是必填项,如果你给快捷键,尽量用组合键避免和 Sketch 自带快捷键冲突,我在实际使用中遇到过cmd shift j被其他插件占用的情形,后来改成cmd shift n才消停。
3.2 读取 JSON 文件并创建第一个画板
plugin.js里先实现最外层流程:弹文件选择框、读取文件、解析 JSON、取当前页、渲染根节点。这里用 Sketch 的sketchnpm 模块写,它是官方维护的插件开发库,能避开直接操作MSXXX原生类的复杂度。
const sketch = require('sketch') const UI = require('sketch/ui') const fs = require('@skpm/fs') export function onImportJSON(context) { const document = sketch.getSelectedDocument() const page = document.selectedPage // 1. 弹出系统文件选择框,只允许选 JSON const panel = NSOpenPanel.openPanel() panel.setAllowsMultipleSelection(false) panel.setCanChooseDirectories(false) panel.setAllowedFileTypes(['json']) // 2. 用户点了“打开”才继续,取消就什么都不做 if (panel.runModal() !== NSFileHandlingPanelOKButton) return // 3. 读文件,解析成 JS 对象 const filePath = panel.URL().path() const raw = fs.readFileSync(filePath, 'utf8') const data = JSON.parse(raw) // 4. 渲染数据里的根节点 renderNode(data, page) // 5. 让 Sketch 回到页面视图,并清理一次选择状态 document.sketchObject.showWindow() UI.message('JSON imported: ' + data.name) }参数与逻辑说明:NSOpenPanel是 macOS 系统级的原生面板,走它而不是自己写文件路径输入框,是为了让用户获得可信赖的文件选取体验,也避免了路径字符串里空格转义的问题。@skpm/fs是给 Sketch 插件用的 Node 风格文件模块,提供readFileSync这类同步方法,能让你在命令函数里按顺序写流程而不会陷入回调地狱。第 5 步的showWindow()很重要,因为插件在执行时 Sketch 可能处于后台状态,不主动唤回窗口用户会以为插件没反应。
3.3 递归渲染 children:一组能直接复制的函数
核心的renderNode函数负责把 JSON 里的一个节点翻译成 Sketch 图层。因为是递归的,所以它能处理任意层级的嵌套。下面这组函数覆盖了group和rect两种类型,text和symbol我放在下一章单独展开:
function renderNode(node, parent) { // 1. 算出每个图层通用的 frame const frame = { x: node.x || 0, y: node.y || 0, width: node.width || 100, height: node.height || 100 } // 2. 按 type 分派创建逻辑 if (node.type === 'group' || (node.children && node.children.length)) { const group = new sketch.Group({ name: node.name || 'Group', parent: parent, frame: frame }) node.children.forEach(child => renderNode(child, group)) } else if (node.type === 'rect') { const shape = new sketch.ShapePath({ name: node.name || 'Rect', parent: parent, frame: frame, shapeType: sketch.ShapeType.Rectangle }) if (node.style && node.style.backgroundColor) { shape.style.fills = [ { color: colorFromHex(node.style.backgroundColor), fillType: sketch.FillType.Color } ] } } } function colorFromHex(hex, alpha = 1) { const s = hex.replace('#', '') const r = parseInt(s.substring(0, 2), 16) / 255 const g = parseInt(s.substring(2, 4), 16) / 255 const b = parseInt(s.substring(4, 6), 16) / 255 return { red: r, green: g, blue: b, alpha: alpha } }这段代码有几个容易忽略的参数点。第一,shapeType: sketch.ShapeType.Rectangle必须显式指定,否则ShapePath会生成一条没有闭合的路径,你在画布上看到的就是一根线而不是色块。第二,style.fills是一个数组而不是单个对象,这是 Sketch 数据模型的设计:一个图层可以叠加多层填充。如果你只赋一个对象,虽然大多数情况下也能显示,但当你未来想通过脚本读取并修改填充时,数组结构会让你少踩很多坑。第三,颜色值必须转成 0 到 1 的浮点数再传进去,直接塞255会被 Sketch 当作非法颜色而静默丢掉。
4. 把真实场景的 JSON 变好看:文本、颜色、间距与组件复用
跑通最小流程之后,你马上会遇到更现实的问题:业务 JSON 里不是只有方块,还有大段文案、不同的对齐方式、半透明遮罩、需要复用的组件。这一章把这些高频需求逐个拆开,并给出关键参数的选择思路。
4.1 文本图层与样式:字号字重和行高的映射表
文本是 JSON 转 Sketch 里最容易被低估的一环。很多第一次做的人只是把字符串塞进new sketch.Text()里,结果发现字体跟设计稿不一致、行高过挤、长文本溢出画板。我的经验是:文本节点的 JSON 结构应该和设计稿抽象对齐,而不是和 HTML 对齐。
function renderTextNode(node, parent) { const text = new sketch.Text({ name: node.name || node.text || 'Text', parent: parent, text: node.text || '', frame: { x: node.x || 0, y: node.y || 0, width: node.width || 200, height: node.height || 50 }, style: { alignment: alignMap[node.style.align || 'left'], textColor: colorFromHex(node.style.color || '#000000'), fontSize: node.style.fontSize || 14, lineHeight: node.style.lineHeight || null } }) return text } // 把 JSON 里的对齐名字映射成 Sketch 的枚举值 const alignMap = { left: 0, center: 1, right: 2 }这段映射里你会发现alignment用的是数字枚举而不是可读字符串,这是 Sketch 底层数据结构的约束,你写在 JSON 业务文件里的"center"必须经过alignMap转换。另一个值得调整的参数是lineHeight:它接收一个具体的数值,单位是像素,而不是 CSS 里的倍数写法。如果你在 JSON 里传1.5,Sketch 不会自动乘字号,只会把行高设为 1.5 像素,整段文字会糊成一团。我通常在转换层里先算好:lineHeight = Math.round(fontSize * node.style.lineHeight)。
4.2 颜色解析从 hex 到 Sketch color 对象:别忽略 alpha
上一章给的colorFromHex只处理了六位 hex 的情况,实际业务里你还会碰到八位 hex 和rgb()描述的颜色。我的建议是统一先转成 0 到 1 的浮点 RGBA 对象再进入渲染层,这样不管输入长什么样,下游代码只认一种结构。
function colorFromHex(hex, alpha = 1) { // 去掉可能出现的 # 前缀 const clean = hex.replace('#', '') // 支持 rgba 形式:#RRGGBBAA let r, g, b, a = alpha if (clean.length === 8) { r = parseInt(clean.substring(0, 2), 16) / 255 g = parseInt(clean.substring(2, 4), 16) / 255 b = parseInt(clean.substring(4, 6), 16) / 255 a = parseInt(clean.substring(6, 8), 16) / 255 } else { r = parseInt(clean.substring(0, 2), 16) / 255 g = parseInt(clean.substring(2, 4), 16) / 255 b = parseInt(clean.substring(4, 6), 16) / 255 } return { red: r, green: g, blue: b, alpha: a } }参数说明:Sketch 的颜色对象字段名是red、green、blue、alpha,而不是r、g、b。你如果按 CSS 习惯传r会被静默忽略。另外注意 Sketch 从某个主版本开始引入了共享色板(Swatch),颜色填充在选择色板时其内部字段里会包含一个swatchReference引用;对于插件从 JSON 导入这种场景,直接给内联颜色就好,不用关心共享色板,Sketch 渲染时会按red/green/blue/alpha四个字段兜底。
4.3 用 symbol 复用组件:JSON 里按 name 找 master
设计系统里按钮、卡片这类组件一定是做成 symbol 的。JSON 转换层里如果只会粗暴地生成一堆基础图层,那转出来的设计稿就没法享受「改一处自动同步所有实例」的福利。合理做法是:JSON 节点里声明"type": "symbol", "symbolName": "Button/Primary",插件在文档里搜索同名 symbol master,然后创建SymbolInstance。
function renderSymbolNode(node, parent, document) { // 1. 在文档里找到同名 symbol master const symbols = document.getSymbols() const master = symbols.find(s => s.name === node.symbolName) // 2. 找不到就回退成全组基础图层,并给出提示 if (!master) { UI.message('Symbol not found: ' + node.symbolName) renderNode({ ...node, type: 'group', children: [] }, parent) return } // 3. 创建实例 const instance = new sketch.SymbolInstance({ name: node.name || node.symbolName, parent: parent, frame: { x: node.x || 0, y: node.y || 0, width: node.width || master.frame.width, height: node.height || master.frame.height } }) instance.symbolId = master.symbolId }两个关键参数值得你记一下:symbolId不是名字,而是 symbol master 的唯一 ID,Sketch 在保存实例时只认这个 ID 不认名字,所以你即便找到了 master,忘了调instance.symbolId = master.symbolId也会导致一个空壳实例。另一个是frame的宽高:能继承 master 默认尺寸就继承,因为设计系统里组件尺寸通常由 master 里的约束规则决定,手动传 JSON 里的宽高容易打乱内部布局。
5. json-sketchapp 常见问题排查:坐标、字体、格式兼容性
自动化的价值在于可重复,可重复的前提是稳定。插件跑一次和跑一百次结果必须一致,但 JSON 数据千变万化,意外总是藏在边界条件里。这一章把最常见的几类翻车现场按「现象 → 原因 → 解决」梳理出来,你可以直接当成排查手册用。
5.1 图层错位或 y 轴方向不对
现象:转出来的图层整体倒挂,或者元素堆叠在一起。原因:JSON 使用的是数学坐标系,y 轴向上增长,而 Sketch 的坐标系原点在画板左上角,y 轴向下增长;更隐蔽的情况是你的 JSON 里用百分比定位,比如"x": "12.5%",渲染层直接赋给了 frame 的x。解决:在渲染前统一把 y 翻转一次,y = containerHeight - node.y - node.height;百分比字符串则先基于容器尺寸换算成绝对像素。不要试图在 Sketch 层面解决,因为 Sketch 的图层坐标系不可配置。
5.2 字体总是被替换成默认字体
现象:每次导入后文本都变成系统默认字体,设计稿里指定过的字重、字距全丢。原因一:JSON 里写的是字体显示名,比如「苹方-简」或「PingFang SC」,而 Sketch 底层需要的是 PostScript 名称「PingFangSC-Regular」;原因二:当前系统根本没装目标字体,Sketch 只能回调默认渲染。解决:在插件里维护一个「显示名 → PostScript 名」的映射表,并在导入时检测NSFont是否存在,不存在就主动UI.message提示缺字体,而不是让 Sketch 静默替换。你在自己的机器上跑得好好的,换到 CI 机器上字体挂了,90% 是这个问题。
5.3 颜色偏色、透明度丢失
现象:方块颜色比 JSON 里写的暗一截,或者原本半透明的遮罩变成了实心。原因:最常见的是把 255 制 RGB 直接当成 0 到 1 传入,比如red: 255,Sketch 会把它截断成1.0,导致颜色过饱和;透明度丢失则是八位 hex 的 A 通道没有解析,或者你在colorFromHex里没有处理alpha参数。解决:在colorFromHex统一把各通道除以 255 并强转浮点;八位 hex 必须解析最后两位为 alpha。排查时在插件里打印JSON.stringify(color),看一眼red字段是不是 0 到 1 之间的浮点数,问题基本当场暴露。
5.4 插件菜单点了没反应或 Sketch 卡死
现象:点击菜单命令后没有任何窗口弹出,或者 Sketch 直接转圈卡住。原因一:manifest.json里handler函数名和脚本导出名不一致,Sketch 找不到入口却不报错;原因二:你在命令函数里用了async/await而 Sketch 的 JavaScript 运行时对异步支持不完整,Promise 永远 pending;原因三:递归渲染时 JSON 里有循环引用,node.children无限嵌套。解决:先清理循环引用或用Map记录已访问节点;命令函数保持同步写法,需要异步读取时用fs.readFileSync这类同步函数;最后打开 Sketch 的 Plugin Output 控制台看报错日志——这是排查一切插件问题的起点。
5.5 直接生成 .sketch 文件时提示文件损坏
现象:你不开 Sketch,在命令行里用 JSZip 把构造好的document.json打包成 .sketch,双击打开却提示文件损坏。原因多半不是 JSON 语法错误,而是结构里缺了关键字段:meta.json里的pages数组没有引用真实的页面do_objectID,或者document.json里的图层缺少do_objectID字段。Sketch 打开文件时会先做 ID 一致性校验,引用对不上就直接判文件损坏,而不是部分加载。解决:构造时对每个图层和页面用 UUID 生成器赋上do_objectID,并确保document.json里的页面数组与meta.json里的pagesAndArtboards引用一致。这个坑最隐蔽,因为文件在 zip 层面是完整的,看不出任何结构异常。
6. 不止于插件:在 CI 流水线里生成 Sketch 文件的一个前置习惯
到最后这一章,我想给你一个让你少走很多弯路的技术取向:不要把插件能力只局限在「Sketch 打开时手动导入」,试着把你验证过的 JSON 转图层逻辑拆成纯函数,让同样的代码能在 Node 环境里跑,直接产出 .sketch 文件。这样你的设计稿生成流程就能挂到 CI 上——数据库里的配置、多语言文案、运营位素材更新,都能在提交时自动生成一份可分发的新版本设计稿,设计师只需要在关键节点打开文件做人工确认。
我一般会分两步做。第一步,把读取文件、处理颜色、计算 frame 的纯逻辑抽到一个converter.js里,它不依赖sketch模块,只做「业务 JSON → 中间图层描述」的转换;第二步,写一个 Node 脚本读取中间图层描述,拼装带_class字段的 Sketch 图层数组。下面这段是中间图层描述的一个最小例子,核心是搞清楚 Sketch 序列化时每个图层的_class名和do_objectID:
// build-document.js 中的图层构造函数片段 const { v4: uuid } = require('uuid') function makeTextLayer(desc) { return { _class: 'MSTextLayer', do_objectID: uuid(), frame: { _class: 'rect', x: desc.x, y: desc.y, width: desc.width, height: desc.height }, attributedString: { _class: 'attributedString', string: desc.text || '' }, style: { _class: 'style', textStyle: { _class: 'textStyle', encodedAttributes: { MSAttributedStringFontAttribute: 'font-key', MSAttributedStringColorAttribute: desc.color || '#000000' } } } } } module.exports = { makeTextLayer }需要注意的是:直接构造document.json这条路线的门槛远高于写插件,类名拼写、属性嵌套层次、样式序列化格式都随 Sketch 版本在演进,你很难一次写对。我的习惯是先手动在 Sketch 里创建一个只含一个文本图层的文件,解包看它序列化后的最小 JSON 结构,再拿它当模板反向生成。这个「解剖一个真实文件再模仿它」的习惯,让我躲过了很多从文档里看字段名的猜测。希望这招对你也管用。愿你做完这套插件之后,每次拿到 JSON 都能稳稳出稿,不再为手工排版熬夜补量。
本文还有配套的精品资源,点击获取