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