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