手把手教你写一个sketch插件

December 17, 2023
测试
测试
测试
测试
24 分钟阅读

序言

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,可能还不能完全满足开发需求,其他一些开发经验可以在这个社区交流学习:

继续阅读

更多来自我们博客的帖子

如何安装 BuddyPress
由 测试 December 17, 2023
经过差不多一年的开发,BuddyPress 这个基于 WordPress Mu 的 SNS 插件正式版终于发布了。BuddyPress...
阅读更多
Filter如何工作
由 测试 December 17, 2023
在 web.xml...
阅读更多
如何理解CGAffineTransform
由 测试 December 17, 2023
CGAffineTransform A structure for holding an affine transformation matrix. ...
阅读更多