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