sketch插件开发入门实践
- 2019 年 12 月 5 日
- 筆記
序言
sketch是一款轻量、易用的矢量设计工具。尽管如此,在使用过程中有些功能还是未能满足,亦或者在设计或开发流程中有些工作还略显繁琐,所幸sketch有提供API供我们开发一些插件来解决使用过程中遇到的问题。本着能用工具解决就用工具解决的懒癌患者原则,我们这个课程就来学习如何开发sketch 插件。
sketch插件的本质就是一些脚本的集合,且官方提供的是JavaScript API,因此这个课程希望你有JavaScript的基础。整个课程通过手把手教你开发一个sketch插件,来达到学习和熟悉开发流程、常用API的目的。
sketch插件结构
那么sketch插件究竟包含了什么东西呢,我们来看看。右键点击-查看包内容
可以看到以下目录结构
- Resources
用来放icon图片等静态资源。
- manifest.json
这是一个json文件,它包含了名称,描述和作者姓名等信息。定义了插件的命令名称、在sketch显示的菜单选项等。
- identifier
指定插件的唯一标识符。Sketch在内部使用此字符串来跟踪插件,为其存储设置等。
- commands
是一个数组,定义用户执行的一个或多个命令。定义的每项命令具有以下属性:
1.name
命令的显示名称。此值在插件菜单中使用。
2.identifier
一个字符串,指定命令的唯一标识符。这用于将命令映射到操作,而不论命令名称如何更改。
3.shortcut
一个可选的字符串,用于指定该命令的快捷键,例如:ctrl t,cmd t,ctrl shift t。
4.script
插件包的Sketch文件夹中用于实现此命令的脚本的相对路径。
5.handler
此命令调用的函数。如果未指定,则一般直接运行export的函数
- menu
嵌套地定义插件在sketch展示的菜单列表。
1.title
一个字符串,为子菜单的标题。
2.items
包含次级子菜单项目的数组,它可以包含两种类型:
(1)命令标识符的字符串;
(2)数组(相当于次次级子菜单)。
开发一个插件
接下来我们尝试做一个批量切图的插件。主要的交互功能是这样的。选择需要导出切片的图层,点击使用插件,弹出导出图片参数设置,输入宽高、选择图片类型和倍数,点击确定,选择保存路径,导出图片。批量切图的交互流程大致是这样。
1.选择需要切图的图层

2.使用插件

3.输入需要批量导出切片的尺寸以及倍数

4.导出


这个插件的完整代码 https://github.com/lulu0729/sketch-slice-plugin
初始化项目
为了初始化我们的整个插件项目,将使用到skpm——一个用于创建,构建和发布插件的管理器。
首先安装skpm
命令行输入以下命令
npm install -g skpm
然后创建一个插件,命令行输入
skpm create sketch-slice-plugin --template=skpm/with-webview
这个表示是要创建一个带webview模板的插件,
我们会有一个输入导出图片参数用的弹窗,这个弹窗就是用webview实现。
创建完毕后,得到这样一个目录
目录结构
assets
我们可能需要放一些图片或HTML等资源文件,可以在放在assets文件夹里,这样在构建插件的时候,会一并打包进去。
而最后生成插件的目录是这样的:

assets里的资源文件将放在Resources里,因此在编写时要以路径"../Resources/xx"来引入资源。
src
主要就是js脚本文件集合以及前文提到的mainifest.json。
查看log
官方提到有3种方法可以查看log。
1.使用 sketch-dev-tools(https://github.com/skpm/sketch-dev-tools)。这个是一个sketch插件,然而它会监听用户的所有操作,所以十分耗费性能。我在使用时候经常闪退,所以暂时不推荐
2.用mac 自带的Console.app,输入你的插件名称,可以筛选出对应的log。
3.打开~/Library/Logs/com.bohemiancoding.sketch3/Plugin Output.log
这个文件,可以查看到完整的log。
调试
命令行输入
defaults write ~/Library/Preferences/com.bohemiancoding.sketch3.plist AlwaysReloadScript -bool YES
这样在修改保存脚本代码的时候都会自动重新安装加载插件。
Hello World
在./src目录下创建脚本my-command.js,
写入
const UI = require("sketch/ui");//引入sketch自带的toast模块 export default function(context){ UI.message("Hello World"); }
在./src/manifest.json中写入运行插件时运行my-command.js具体如下
{ "compatibleVersion": 3, "bundleVersion": 1, "commands": [ { "name": "my-command", "identifier": "sketch-slice-plugin.my-command-identifier", "script": "./my-command.js", "handlers": { "run": "onRun", "actions": { "Shutdown": "onShutdown" } } } ], "menu": { "title": "sketch-slice-plugin", "items": [ "sketch-slice-plugin.my-command-identifier" ] }
命令行输入
defaults write ~/Library/Preferences/com.bohemiancoding.sketch3.plist AlwaysReloadScript -bool YES
打开sketch,选择插件运行,就可以看到一个'Hello World'的toast。
接下来我们正式进入插件的开发。
构建一个webview操作界面
首先开始写插件的操作界面。如果用object-c来写可能会比较复杂,skpm提供了一个模块sketch-module-web-view,用于创建一个webview,以便更方便地写出操作界面。
安装
命令行输入
npm install -S sketch-module-web-view
创建webview
在./resources目录下打开webview.html,编写一个HTML的操作界面,具体代码如下
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>sketch-slice-plugin</title> <link rel="stylesheet" href="./style.css" /> </head> <body> sketch-slice-plugin <div> <div class="box"> <h1>请输入切片大小</h1> <label>宽度:</label> <input type="number" placeholder="单位:px" id="inputWidth" /> <br /> <label>高度:</label> <input type="number" placeholder="单位:px" id="inputHeight" /> <br /> <label class="attr-name">导出图片类型</label> <br /> <input id="png" class="formats" type="checkbox" checked="value" value="png" /> <label>png</label> <br /> <input id="jpg" class="formats" type="checkbox" checked="value" value="jpg" /> <label>jpg</label> <input id="svg" class="formats" type="checkbox" checked="value" value="svg" /> <label>svg</label> <br /> <input id="scale1x" class="scales" type="checkbox" checked="value" value="1" /> <label>@1x</label> <br /> <input id="scale2x" class="scales" type="checkbox" checked="value" value="2" /> <label>@2x</label> <input id="scale3x" class="scales" type="checkbox" checked="value" value="3" /> <label>@3x</label> <br /> <button id="btnExport">导出切片</button> </div> </div> <div id="answer"></div> <!-- notice the "../" here. It's because webview.js will be compiled in a different folder --> <script src="../webview.js"></script> </body> </html>
在同目录下的style.css写一下样式~
/* some default styles to make the view more native like */ html { box-sizing: border-box; background: transparent; /* Prevent the page to be scrollable */ overflow: hidden; /* Force the default cursor, even on text */ cursor: default; } *, *:before, *:after { box-sizing: inherit; margin: 0; padding: 0; position: relative; /* Prevent the content from being selectionable */ -webkit-user-select: none; user-select: none; } input, textarea { -webkit-user-select: auto; user-select: auto; } body { background-color: #fff; }
一个基本的操作界面就写好了。接下来要在sketch中打开一个webview,以便能访问到我们写的html界面。
在my-command.js中写入
/*my-command.js*/ //引入依赖的webview模块 const BrowserWindow = require("sketch-module-web-view"); const { getWebview } = require("sketch-module-web-view/remote"); const webviewIdentifier = "sketch-slice-plugin.webview"; export default function(context) { const options = { identifier: webviewIdentifier, width: 400, height: 380 };//设置webview视窗大小等参数 const browserWindow = new BrowserWindow(options); const webContents = browserWindow.webContents; //加载html browserWindow.loadURL(require("../resources/webview.html")); }
运行一下插件,可以看到webview界面。
webview和plugin间的通信
有了webview界面,需要让webview与plugin进行交互通信。
而browserWindow提供了API。我们先来学习下。
plugin调用webview的函数
在webview定义了一个函数
window.updatePreview = function (text) { console.log(text); };
在plugin调用这个函数并传入参数
let text = "send a message"; win.webContents.executeJavaScript( "updatePreview('" + text + "')" );
从WebView向插件传递信息
webview
pluginCall('nativeLog', unit , value);
plugin
win.webContents.on('nativeLog', (key, value) => { Settings.setSettingForKey(key,value); });
获取选择的图层
sketch提供了API让我们能获取到选择的图层,以便对图层进行一些处理
在my-command.js写入
// we will also need a function to transform an NSArray into a proper JavaScript array // the `util` package contains such a function so let's just use it. const { toArray } = require("util"); export default function(context) { ... // 通过context.selection获取到选择的图层,并用toArray函数转成JavaScript数组,以便后续我们进行处理 const selection = toArray(context.selection); ... }
获取输入的参数
我们要获取到在webview输入的切片参数。
在./resources目录下新建webview.js,写入代码
//webview.js // call the plugin from the webview //监听button点击事件 document.getElementById("btnExport").addEventListener("click", () => { //获取输入的宽高值、倍数、输出图片类型 let width = document.getElementById("inputWidth").value || "", height = document.getElementById("inputHeight").value || "", scales = document.getElementsByClassName("scales"); formats = document.getElementsByClassName("formats"); let scalesArray = [], formatsArray = []; // 对倍数、图片类型的参数处理成数组 Array.prototype.filter.call(scales, scale => { if (scale.checked) { scalesArray.push(scale.value); } }); Array.prototype.filter.call(formats, format => { if (format.checked) { formatsArray.push(format.value); } }); let formatsStr = Array.prototype.join.call(formatsArray); // 向plugin通信 window.postMessage("getOptions", { width: width, height: height, formats: formatsStr, scales: scalesArray }); });
plugin
//my-commond.js export default function(context) { ... // add a handler for a call from web content's javascript webContents.on("getOptions", options => { //这样就能拿到webview传来的options搞事情了 handlerSelection(selection, options); }); ... }
处理图层handlerSelection
下面来写整个插件的核心部分handlerSelection,用于接收选择图层selection和参数options后处理图层并导出想要的切片。
我们先在./src目录下新建selection.js
//selection.js module.exports = { handlerSelection(selection, options) { let slices = []; //初始化切片数组 let opt = handlerExportOpt(options); //处理导出切片的参数,后续会详解如何实现这个函数 selection.forEach(layer => { let slice = handlerSlice(layer, options);//生成切片,,后续会详解如何实现这个函数 //slice push进数组 slices.push(slice); }); //用sketch自带的api 将切片批量导出 sketch.export(slices, opt); } };
并在my-commond.js导入这个模块
//my-commond.js ... const { handlerSelection } = require("./selection.js"); ...
处理导出切片的参数
我们导出切片的路径需要打开一个对话框来进行选择:
//selection.js /** 导出路径的panel */ function setSavePanel() { //使用object-c的api,打开一个保存路径的对话框 let savePanel = NSSavePanel.savePanel(); //设置对话框的标题等参数 savePanel.setTitle("Export"); savePanel.setNameFieldLabel("Export to"); savePanel.setShowsTagField(false); savePanel.setCanCreateDirectories(true); if (savePanel.runModal() != NSOKButton) { //如果点击了取消按钮,则返回false log("cancel save"); return false; } else { //否则返回选择的路径 return savePanel.URL().path(); } }
接下来讲解下怎么处理导出切片的参数。
//selection.js /** 处理导出切片的参数 */ function handlerExportOpt(options) { let url = setSavePanel();//获取导出切片的保存路径 //返回所需的参数对象 return { output: url,//导出路径 formats: options.formats || "png",//导出图片的类型 scales: options.scales[0] || [1],//导出图片的倍数 "group-contents-only": true//去除背景 }; }
总结一下整个步骤就是,获取导出路径和处理好切片的参数,并将这个对象返回。
生成切片
回顾前面的代码,在处理切片参数后,对选择的图层依次生成一个切片,并将切片push进slices数组中。
//selection.js handlerSelection(selection, options) { ... selection.forEach(layer => { let slice = handlerSlice(layer, options); //slice push进数组 slices.push(slice); }); ... }
而生成切片的函数实现如下:
//selection.js /* 生成切片 */ function handlerSlice(layer, options) { //根据宽高计算并新建切片 //新建一个包含图层的group,用于包含图层和切片 let group = MSLayerGroup.groupWithLayer(layer); let groupName = toJSString(layer.name()); //获取图层的名称 group.setName(groupName); //将group名设置为图层的名称 let slice = MSSliceLayer.sliceLayerFromLayer(layer); //用sketch提供的object-c API创建一个切片 let layerFrame = layer.frame(); let sliceFrame = slice.frame(); //切片设置为输入的宽高,若未输入宽高,则按照图层的实际大小设置切片宽高 sliceFrame.setWidth(options.width || layerFrame.width()); sliceFrame.setHeight(options.height || layerFrame.height()); //计算切片与图层的位置差 let sliceX = Math.floor((layerFrame.width() - sliceFrame.width()) / 2); let sliceY = Math.floor((layerFrame.height() - sliceFrame.height()) / 2); // let sliceXFloor = Math.floor(sliceX); // let sliceYFloor = Math.floor(sliceY); //按照位置差移动切片位置,使图层居中于切片中心 sliceFrame.setX(sliceX); sliceFrame.setY(sliceY); //返回这个切片 return slice; }
批量导出切片
最后一步是用sketch提供的API批量导出切片
module.exports = { handlerSelection(selection, options) { ... //slice批量导出 sketch.export(slices, opt); } };
总结
至此,一个批量截图的插件便完成了。
sketch 官方还提供了很多其他API,可以在https://developer.sketch.com/reference/api/ 查看,但这只是官方放出的javascript API,可能还不能完全满足开发需求,其他一些开发经验可以在这个社区交流学习: