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,可能還不能完全滿足開發需求,其他一些開發經驗可以在這個社區交流學習: