简介:这份资源是一个基于 skpm 构建的 Sketch 插件实验项目,面向需要将 JSON 数据批量转换为 Sketch 设计文件的设计师与前端开发者,尤其适合熟悉 Sketch 插件机制、希望用脚本打通数据与设计稿流程的中级用户。压缩包共 9 个文件,以 json 配置与示例数据、js 插件逻辑脚本为主,另含 md 说明文档、png 图标资源及 lock 依赖锁定文件,整体约 81KB,体量轻便,便于快速阅读源码与二次改造。项目已吸引 672 人学习下载,具备一定参考价值。包内提供 sample.json 示例数据与 my-command.js 命令入口,配合 manifest.json 可了解插件注册与导入流程;README 说明依赖安装、build、watch、start 等开发命令及 Babel 自定义配置,读者可据此掌握从 JSON 结构到 Sketch 图层的映射思路,并在此基础上扩展自己的导入规则。
1. 设计稿交接的最后一公里:把 JSON 变成可编辑的 Sketch 文件
设计交付里最烦人的场景之一,是上游给过来一份结构清晰的 JSON——可能是设计 token、可能是页面结构描述、也可能是某个低代码平台导出的布局树——而下游设计师只认 Sketch 文件。手工在 Sketch 里一层层重建,几十个画板还能忍,上百个组件就是纯体力活,改一次 JSON 就得重来一遍。json-sketchapp 这个资源解决的就是这一段:它是一套基于 skpm 构建的 Sketch 插件工程,把 JSON 描述转换成 Sketch 里真实可编辑的图层,而不是一张扁平截图。适合谁?适合手里有结构化 JSON、又必须交付 Sketch 源文件的前端、设计系统维护者,以及想用 SketchJavaScript API 做批量出图自动化的工程师。Sketch 的插件生态里,skpm 是绕不开的脚手架,SketchJavaScript 是绕不开的运行时,这套源码正好把两者串起来给你看。
2. 拆开插件工程:skpm 脚手架与 SketchJavaScript 运行时怎么配合
拿到一个 Sketch 插件源码包,第一件事不是急着跑,而是先看清它的骨架。Sketch 插件本质是一个.sketchplugin包,里面是manifest.json加一段 JavaScript。skpm 做的事情,是把现代前端工程(npm 依赖、ES 模块、打包)翻译成 Sketch 能加载的旧式插件结构。理解这一层,后面改代码才不会一头雾水。
2.1 manifest.json 是插件的身份证
Sketch 启动时扫描插件目录,读的就是manifest.json。它决定了插件叫什么、命令怎么注册、入口脚本在哪。json-sketchapp 这类转换插件,通常至少注册一个命令,菜单里点一下就触发 JSON 读取与图层生成。一个典型结构长这样:
{ "name": "json-sketchapp", "identifier": "com.example.json-sketchapp", "version": "1.0.0", "description": "Convert JSON structure into editable Sketch layers", "author": "some developer", "commands": [ { "name": "Import JSON as Layers", "identifier": "import-json", "script": "./import-json.js", "handler": "onRun" } ], "menu": { "title": "json-sketchapp", "items": ["import-json"] } }identifier必须全局唯一,重复会导致插件加载冲突;script指向打包后的入口文件,skpm 构建时会自动改写这个路径;handler是脚本里被调用的函数名,默认onRun。很多人第一次改插件翻车,就是把handler名字改了却没同步改 JS 里的导出函数,结果点了菜单毫无反应,控制台也不报错——因为 Sketch 根本没找到那个函数。
2.2 skpm 的构建链路:从 src 到 .sketchplugin
skpm 的工程目录一般长这样:src/放源码,manifest.json放根目录,package.json里配skpm-build脚本。构建时 skpm 用 webpack 把src下的模块打成单文件,再塞进.sketchplugin/Contents/Sketch/目录。关键命令就三条:
# 安装依赖,注意 skpm 对 Node 版本有要求,建议 14 或 16 npm install # 开发模式:监听文件变化,自动重新构建并链接到 Sketch 插件目录 npm run watch # 生产构建:产出可分发的 .sketchplugin 包 npm run buildnpm run watch是日常开发的主力,它会在你保存文件后自动重建,并把插件软链到~/Library/Application Support/com.bohemiancoding.sketch3/Plugins/下。这里有个血泪经验:如果你之前手动装过同名插件,软链会失败但不一定报错,表现是改了代码 Sketch 里行为不变。排查方法是去插件目录看那个.sketchplugin到底是软链还是实体文件夹,ls -l一看便知。
2.3 SketchJavaScript 的运行时边界
Sketch 插件跑在一个受限的 JavaScript 环境里,不是完整 Node,也不是浏览器。你能用sketch/dom、sketch/ui这些官方模块,能读文件(通过sketch/settings或NSFileManager桥接),但没有fetch、没有fs的完整能力。json-sketchapp 读 JSON 的常见做法有两种:一是用sketch/ui弹文件选择框拿到路径,再用 Objective-C 桥接读文件;二是把 JSON 内容直接内联进脚本。前者灵活,后者适合固定模板批量生成。
import sketch from 'sketch/dom' import UI from 'sketch/ui' import fs from '@skpm/fs' export default function onRun() { // 弹出文件选择框,拿到用户选中的 JSON 路径 const panel = NSOpenPanel.openPanel() panel.setAllowedFileTypes(['json']) panel.beginSheetModalForWindow_completionHandler( sketch.getSelectedDocument().sketchObject.documentWindow(), (response) => { if (response !== NSModalResponseOK) return const path = panel.URLs().firstObject().path() // @skpm/fs 是 skpm 提供的文件读写垫片,能力有限但够读文本 const raw = fs.readFileSync(path, 'utf8') const data = JSON.parse(raw) buildLayers(data) } ) }这段代码里NSOpenPanel是 macOS 原生文件选择器,Sketch 插件可以直接调;@skpm/fs是 skpm 生态里的文件模块,读文本没问题,但别指望它做流式处理。buildLayers是你自己写的转换逻辑,下一章展开。参数上要注意setAllowedFileTypes只接受扩展名数组,传['.json']带点会匹配失败,这是很多人踩过的坑。
3. 从 JSON 到图层:转换逻辑的四个关键决策
插件骨架跑通之后,真正决定成败的是转换逻辑。JSON 是纯数据,Sketch 图层是有层级、有样式、有坐标的实体,中间要做映射。这一章把映射拆成四个决策点:结构怎么递归、坐标怎么算、样式怎么落、文本怎么处理。每个决策都对应源码里的一段逻辑,改的时候知道改哪里。
3.1 递归建组:用 Group 表达 JSON 的嵌套
JSON 的嵌套结构天然对应 Sketch 的 Group。一个节点如果有children,就建一个 Group,递归处理子节点;没有children,就按type建具体图层(矩形、文本、图片)。递归函数的核心是「先建父、再建子、最后把子塞进父」:
import sketch from 'sketch/dom' function buildLayers(node, parent) { // 有 children 说明是容器,建 Group 后递归 if (node.children && node.children.length) { const group = new sketch.Group({ name: node.name || 'group', parent: parent, frame: { x: node.x || 0, y: node.y || 0, width: node.width || 100, height: node.height || 100 } }) node.children.forEach(child => buildLayers(child, group)) return group } // 叶子节点按 type 分发 switch (node.type) { case 'text': return new sketch.Text({ text: node.text, parent, frame: toFrame(node) }) case 'shape': return new sketch.Shape({ parent, frame: toFrame(node) }) default: return null } }parent参数是关键,Sketch 的图层树靠它挂载,传错父节点会导致图层跑到画板外面。frame的宽高不能为 0,Sketch 对零尺寸图层处理很怪,可能直接不渲染,所以给个默认值兜底。递归深度上,JSON 嵌套超过十几层时建议加个depth参数限制,否则大文件会卡住主线程,Sketch 界面直接假死。
3.2 坐标系对齐:JSON 的绝对坐标 vs Sketch 的相对坐标
这是转换里最容易翻车的地方。JSON 里的x/y往往是相对页面左上角的绝对坐标,而 Sketch 的 Group 子图层坐标是相对父 Group 的。如果你直接把绝对坐标赋给子图层,图层会整体偏移,偏移量正好是父 Group 的位置。正确做法是建子图层时减去父节点的偏移:
function toFrame(node, parentOffset = { x: 0, y: 0 }) { return { x: (node.x || 0) - parentOffset.x, y: (node.y || 0) - parentOffset.y, width: node.width || 100, height: node.height || 100 } }递归时把当前节点的x/y作为parentOffset往下传。如果你的 JSON 本来就是相对坐标,那就别减,直接透传。判断依据看 JSON 来源:设计工具导出的通常是绝对坐标,低代码平台导出的可能是相对坐标。搞错这一层,生成出来的画板会像被揉过一样错位,而且很难一眼看出是坐标问题还是尺寸问题。
3.3 样式映射:把 JSON 的 style 对象翻译成 Sketch Style
JSON 里的样式字段五花八门,常见的有fill、border、borderRadius、opacity。Sketch 的样式模型是Style对象,填充是Fill数组,边框是Border数组。映射时要做类型转换,比如颜色字符串#FF0000要转成sketch.Style能认的MSColor或十六进制数值:
function applyStyle(layer, style = {}) { if (style.fill) { layer.style.fills = [{ color: style.fill, // 支持 #RRGGBB 或 #RRGGBBAA fillType: sketch.Style.FillType.Color }] } if (style.border) { layer.style.borders = [{ color: style.border.color || '#000000', thickness: style.border.width || 1, position: sketch.Style.BorderPosition.Inside }] } if (typeof style.opacity === 'number') { layer.style.opacity = style.opacity } }fillType不写会默认成图案填充,颜色不生效,这是隐蔽坑。BorderPosition.Inside是设计稿里最常用的边框位置,写Center会让边框压住内容。圆角在 Sketch 里是layer.style.cornerRadius,但只对矩形有效,Group 上设圆角需要开layer.style.cornerRadius配合hasClippingMask,源码里如果没处理,圆角会丢。
3.4 文本图层:字体、行高与自动尺寸
文本是转换里最娇气的图层。Sketch 的Text需要text内容、fontFamily、fontSize,行高通过lineHeight控制。JSON 里如果只给了text没给字体,Sketch 会用默认字体,中文可能变成方框。稳妥做法是给一套 fallback:
new sketch.Text({ text: node.text || '', parent: parent, frame: toFrame(node, parentOffset), style: { fontFamily: node.fontFamily || 'PingFang SC', fontSize: node.fontSize || 14, lineHeight: node.lineHeight || null, textColor: node.color || '#333333' }, fixedWidth: node.width != null // 有宽度就固定宽,否则自动撑开 })fixedWidth这个参数很关键:设true时文本按给定宽度换行,设false时按内容自动撑开。JSON 里给了width就设true,没给就设false,否则文本要么被截断要么撑破容器。lineHeight传null表示用字体默认行高,传数字则按像素算,别传字符串。
4. 避坑与排查:五个让插件「看起来没反应」的真实原因
插件类项目最折磨人的地方,是失败时往往没有明确报错。Sketch 的插件运行在沙盒里,异常可能被吞掉,界面只是「什么都没发生」。下面五条是我在调试这类转换插件时反复遇到的,按「现象 → 原因 → 解决」记下来,能省不少时间。
4.1 点了菜单没反应,控制台也干净
现象:菜单项能点,但画布毫无变化,Sketch 的插件控制台(Plugins → Run Script 或 Console)没有报错。原因多半是manifest.json里的handler和脚本导出函数名不一致,或者script路径指向了不存在的文件。Sketch 找不到入口时静默失败。解决:核对handler与export default function onRun是否同名,构建后去.sketchplugin/Contents/Sketch/下确认入口 JS 确实存在,路径大小写也要对。
4.2 图层生成了,但全部堆在画板左上角
现象:JSON 里明明有坐标,生成出来的图层全叠在 (0,0)。原因通常是坐标系没对齐——子图层用了绝对坐标,或者parentOffset没往下传。解决:在toFrame里打日志,把node.x和parentOffset.x都打出来,看差值是否等于父 Group 的位置。如果 JSON 本身是相对坐标,去掉减法逻辑。
4.3 颜色不生效,填充变成黑白格
现象:设了fill,图层却显示成默认的图案填充。原因是fillType没指定,Sketch 默认按图案处理。解决:显式写fillType: sketch.Style.FillType.Color。另外颜色字符串如果带透明度(#RRGGBBAA),部分 Sketch 版本解析异常,建议先转成MSColor再赋值。
4.4 大 JSON 一导入 Sketch 就卡死
现象:几十 KB 的 JSON 导入时 Sketch 转圈无响应,甚至崩溃。原因是递归在主线程同步执行,图层数量上千时阻塞 UI。解决:给递归加数量上限或分批处理,用sketch.UI.message提示进度;更彻底的做法是把转换拆成多次setTimeout让出主线程,或者先做 JSON 瘦身,去掉无用字段再导入。
4.5 watch 模式改了代码,Sketch 行为不变
现象:npm run watch显示构建成功,但 Sketch 里跑的还是旧逻辑。原因是插件目录里存在同名实体文件夹,软链没覆盖成功。解决:去~/Library/Application Support/com.bohemiancoding.sketch3/Plugins/下删掉旧的.sketchplugin,重新npm run watch,用ls -l确认是软链(带箭头)而非实体目录。改完重启 Sketch 一次更保险。
5. 进阶:把转换器做成可配置的模板引擎
基础版能跑通之后,真正拉开效率差距的是「可配置」。硬编码的转换逻辑每换一种 JSON 就要改代码,而实际项目里 JSON 结构经常变。我的做法是把字段映射抽成配置表,让转换器读配置而不是读死代码。下面是一个映射配置的示例,用表格表达字段对应关系,比散落在代码里的if/else好维护得多:
| JSON 字段 | Sketch 属性 | 默认值 | 说明 |
|---|---|---|---|
type | 图层类型 | shape | 决定建 Group / Text / Shape |
x/y | frame.x/frame.y | 0 | 绝对坐标需减父偏移 |
width/height | frame.width/frame.height | 100 | 零值兜底 |
fill | style.fills[0].color | 无 | 需配fillType |
fontSize | style.fontSize | 14 | 仅文本图层 |
children | 递归建 Group | 无 | 有则递归 |
配置化之后,转换函数变成「查表 + 分发」,新增字段只改配置不改逻辑。再进一步,可以把配置存成 JSON 文件,插件启动时读取,这样非开发者也能调映射关系。验证方法很直接:拿一份已知结构的 JSON,导入后对比 Sketch 图层面板里的层级和属性,逐项核对配置表,对不上的就是映射写错了。
还有一个我常用的技巧:在转换完成后自动选中生成的顶层 Group 并缩放视图到合适比例,省得每次导入后手动找图层。代码就两行,但体验提升明显:
const doc = sketch.getSelectedDocument() doc.selectedLayers = [topGroup] doc.sketchObject.contentDrawView().zoomToSelection()zoomToSelection是原生方法,比手动算缩放比例靠谱。从那以后我每次改完转换逻辑,都会先拿一份最小 JSON(两三层、一个文本一个矩形)跑一遍,确认坐标、颜色、文本三项都对,再上真实数据——这个习惯帮我挡掉了至少一半的「导入后错位」问题。希望帮到你。
本文还有配套的精品资源,点击获取