本文做者是來自凹凸實驗室
高露
, 他在作內部設計中臺 quark 項目中,協做開發了Quark for Sketch
插件,在開發過程有許多經驗總結,在本文裏與你們分享。javascript
Sketch 是很是流行的 UI 設計工具,2014年隨着 Sketch V43 版本增長 Symbols 功能、開放開發者權限,吸引了大批開發者的關注。html
目前 Sketch 開發有兩大熱門課題:① React 組件渲染成 sketch 由 airbnb 團隊發起,② 使用 skpm 構建開發 Sketch 插件。java
Sketch 插件開發相關資料較少且不太完善,咱們開發插件過程當中能夠重點參考官方文檔,只是有些陳舊。官方有提供 JavaScript API 藉助 CocoaScript bridge 訪問內部 Sketch API 和 macOS 框架進行開發插件(Sketch 53~56 版 JS API 在 native MacOS 和 Sketch API 暴露的特殊環境中運行),提供的底層 API 功能有些薄弱,更深刻的就須要瞭解掌握 Objective-C 、 CocoaScript 、AppKit、Sketch-Headers。react
Sketch Plugin 是一個或多個 scripts 的集合,每一個 script 定義一個或多個 commands。Sketch Plugin 是以 .sketchplugin
擴展名的文件夾,包含文件和子文件夾。嚴格來講,Plugin 其實是 OS X package,用做爲 OS X bundle。ios
Bundle 具備標準化分層結構的目錄,其保存可執行代碼和該代碼使用的資源。git
Bundles 包含一個 manifest.json
文件,一個或多個 scripts 文件(包含用 CocoaScript 或 JavaScript 編寫的腳本),它實現了 Plugins 菜單中顯示的命令,以及任意數量的共享庫腳本和資源文件。github
mrwalker.sketchplugin
Contents/
Sketch/
manifest.json
shared.js
Select Circles.cocoascript
Select Rectangles.cocoascript
Resources/
Screenshot.png
Icon.png
複製代碼
最關鍵的文件是 manifest.json
文件,提供有關插件的信息。web
小貼士:json
Sketch 插件包可使用 skpm 在構建過程當中生成,skpm 提供 Sketch 官方插件模版:canvas
skpm/skpm
- The simplest possible plugin setup. (default)skpm/with-prettier
- A plugin setup featuring linting with ESLint and code formatting with Prettier.skpm/with-datasupplier
- A template to create DataSupplier plugins (check our blog for more info)skpm/with-webview
- A template to create plugins displaying some rich UI in a WebView (check sketch-module-web-view for more info)💁 Tip: Any Github repo with a 'template' folder can be used as a custom template:
skpm create <project-name> --template=<username>/<repository>
manifest.json
文件提供有關插件的信息,例如做者,描述,圖標、從何處獲取最新更新、定義的命令 **(commands) **、調用菜單項 (menu) 以及資源的元數據。
{
"name": "Select Shapes",
"description": "Plugins to select and deselect shapes",
"author": "Joe Bloggs",
"homepage": "https://github.com/example/sketchplugins",
"version": "1.0",
"identifier": "com.example.sketch.shape-plugins",
"appcast": "https://excellent.sketchplugin.com/excellent-plugin-appcast.xml",
"compatibleVersion": "3",
"bundleVersion": 1,
"commands": [
{
"name": "All",
"identifier": "all",
"shortcut": "ctrl shift a",
"script": "shared.js",
"handler": "selectAll"
},
{
"name": "Circles",
"identifier": "circles",
"script": "Select Circles.cocoascript"
},
{
"name": "Rectangles",
"identifier": "rectangles",
"script": "Select Rectangles.cocoascript"
}
],
"menu": {
"items": ["all", "circles", "rectangles"]
}
}
複製代碼
聲明一組 command 的信息,每一個 command 以 Dictionary
數據結構形式存在。
context
上下文參數。若未指定 handler,Sketch 會默認調用對應 script 中 onRun
函數com.xxxx.xxx
格式,不要過長Sketch 加載插件會根據指定的信息,在菜單欄中有序顯示命令名。
在瞭解了 Sketch 插件結構以後,咱們再來了解一下,sketch提供的官方 API: Actions API, Javascript API。
Sketch Actions API 用於監聽用戶操做行爲而觸發事件,例如 OpenDocumen(打開文檔)、CloseDocument(關閉文檔)、Shutdown(關閉插件)、TextChanged(文本變化)等,具體詳見官網:developer.sketch.com/reference/a…
manifest.json 文件,配置相應 handlers。
示例:當 OpenDocument 事件被觸發時調用 onOpenDocument handler 。
"commands" : [
...
{
"script" : "my-action-listener.js",
"name" : "My Action Listener",
"handlers" : {
"actions": {
"OpenDocument": "onOpenDocument"
}
},
"identifier" : "my-action-listener-identifier"
}
...
],
複製代碼
**my-action-listener.js **
export function onOpenDocument(context) {
context.actionContext.document.showMessage('Document Opened')
}
複製代碼
Action 事件觸發時會將 context.actionContext
傳遞給相應 handler
。注意有些 Action 包含兩個狀態begin
和 finish
,例如 SelectionChanged
,需分別訂閱 SelectionChanged.begin
和 SelectionChanged.finish
,不然會觸發兩次事件。
Sketch 插件開發大概有以下三種方式:① 純使用 CocoaScript 腳本進行開發,② 經過 Javascript + CocoaScript 的混合開發模式, ③ 經過 AppKit + Objective-C 進行開發。Sketch 官方建議使用 JavaScript API 編寫 Sketch 插件,且官方針對 Sketch Native API 封裝了一套 JS API,目前還未涵蓋全部場景, 若須要更豐富的底層 API 需結合 CocoaScript 進行實現。經過 JS API 能夠很方便的對 Sketch 中 Document
、Artboard
、Group
、Layer
進行相關操做以及導入導出等,可能須要考慮兼容性, JS API 原理圖以下:
CocoaScript 實現 JavaScript 運行環境到 Objective-C 運行時的橋接功能,可經過橋接器編寫 JavaScript 外部腳本訪問內部 Sketch API 和 macOS 框架底層豐富的 API 功能。
小貼士:
Mocha 實現提供 JavaScript 運行環境到 Objective-C 運行時的橋接功能已包含在CocoaScript中。
CocoaScript 創建在 Apple 的 JavaScriptCore 之上,而 JavaScriptCore 是爲 Safari 提供支持的 JavaScript 引擎,使用 CocoaScript 編寫代碼實際上就是在編寫 JavaScript。CocoaScript 包括橋接器,能夠從 JavaScript 訪問 Apple 的 Cocoa 框架。
藉助 CocoaScript 使用 JavaScript 調 Objective-C 語法:
object.name()
object.setName('Sketch')
,object.name='sketch'
var/const/let
設置類型注意:詳細 Objective-C to JavaScript 請參考 Mocha 文檔
示例:
// oc: MSPlugin 的接口 valueForKey:onLayer:
NSString * value = [command valueForKey:kAutoresizingMask onLayer:currentLayer];
// cocoascript:
const value = command.valueForKey_onLayer(kAutoresizingMask, currentLayer);
// oc:
const app = [NSApplication sharedApplication];
[app displayDialog:msg withTitle:title];
// cocoascript:
const app = NSApplication.sharedApplication();
app.displayDialog_withTitle(msg, title)
// oc:
const openPanel = [NSOpenPanel openPanel]
[openPanel setTitle: "Choose a location…"]
[openPanel setPrompt: "Export"];
// cocoascript:
const openPanel = NSOpenPanel.openPanel
openPanel.setTitle("Choose a location…")
openPanel.setPrompt("Export")
複製代碼
Sketch 插件系統能夠徹底訪問應用程序的內部結構和 macOS 中的核心框架。Sketch 是用 Objective-C 構建的,其 Objective-C 類經過 Bridge (CocoaScript/mocha) 提供 Javascript API 調用,簡單的瞭解 Sketch 暴露的相關類以及類方法,對咱們開發插件很是有幫助。
使用 Bridge 定義的一些內省方法來訪問如下信息:
String(context.document.class()) // MSDocument
const mocha = context.document.class().mocha()
mocha.properties() // array of MSDocument specific properties defined on a MSDocument instance
mocha.propertiesWithAncestors() // array of all the properties defined on a MSDocument instance
mocha.instanceMethods() // array of methods defined on a MSDocument instance
mocha.instanceMethodsWithAncestors()
mocha.classMethods() // array of methods defined on the MSDocument class
mocha.classMethodsWithAncestors()
mocha.protocols() // array of protocols the MSDocument class inherits from
mocha.protocolsWithAncestors()
複製代碼
當輸入插件定製的命令時,Sketch 會去尋找改命令對應的實現函數, 並傳入 context
變量。context
包含如下變量:
MSPluginCommand
對象,當前執行命令MSDocument
對象 ,當前文檔MSPluginBundle
對象,當前的插件 bundle,包含當前運行的腳本NSString
當前執行腳本的絕對路徑NSURL
對象NSArray
對象,包含了當前選擇的全部圖層。數組中的每個元素都是 MSLayer
對象小貼士:MS 打頭類名爲 Sketch 封裝類如圖層基類 MSLayer、文本層基類 MSTextLayer 、位圖層基類 MSBitmapLayer,NS 打頭爲 AppKit 中含有的類
const app = NSApplication.sharedApplication()
function initContext(context) {
context.document.showMessage('初始執行腳本')
const doc = context.document
const page = doc.currentPage()
const artboards = page.artboards()
const selectedArtboard = page.currentArtboard() // 當前被選擇的畫板
const plugin = context.plugin
const command = context.command
const scriptPath = context.scriptPath
const scriptURL = context.scriptURL
const selection = context.selection // 被選擇的圖層
}
複製代碼
##Sketch 插件開發上手
前面咱們瞭解了許多 Sketch 插件開發知識,那接下來實際上手兩個小例子: ① 建立輔助內容面板窗口, ② 側邊欄導航。爲了方便開發,咱們在開發前需先進行以下操做:
崩潰保護
當 Sketch 運行發生崩潰,它會停用全部插件以免循環崩潰。對於使用者,每次崩潰重啓後手動在菜單欄啓用所需插件很是繁瑣。所以能夠經過以下命令禁用該特性。
defaults write com.bohemiancoding.sketch3 disableAutomaticSafeMode true
複製代碼
插件緩存
經過配置啓用或禁用緩存機制:
defaults write com.bohemiancoding.sketch3 AlwaysReloadScript -bool YES
複製代碼
該方法對於某些場景並不適用,如設置 COScript.currentCOScript().setShouldKeepAround(true)
區塊會保持常駐在內存,那麼則須要經過 coscript.setShouldKeepAround(false)
進行釋放。
WebView 調試
若是插件實現方案使用 WebView 作界面,可經過如下配置開啓調試功能。
defaults write com.bohemiancoding.sketch3 WebKitDeveloperExtras -bool YES
複製代碼
首先咱們先熟悉一下 macOS 下的輔助內容面板, 以下圖最左側 NSPanel 樣例, 它是有展現區域,可設置樣式效果,左上角有可操做按鈕的輔助窗口。
Sketch 中要建立以下內容面板,須要使用 macOS 下 AppKit
框架中 NSPanel
類,它是 NSWindow
的子類,用於建立輔助窗口。內容面板外觀樣式設置,可經過 NSPanel
類相關屬性進行設置, 也可經過 AppKit
的NSVisualEffectView
類添加模糊的背景效果。內容區域則可經過 AppKit
的 WKWebView
類,單開 webview
渲染網頁內容展現。
const panelWidth = 80;
const panelHeight = 240;
// Create the panel and set its appearance
const panel = NSPanel.alloc().init();
panel.setFrame_display(NSMakeRect(0, 0, panelWidth, panelHeight), true);
panel.setStyleMask(NSTexturedBackgroundWindowMask | NSTitledWindowMask | NSClosableWindowMask | NSFullSizeContentViewWindowMask);
panel.setBackgroundColor(NSColor.whiteColor());
// Set the panel's title and title bar appearance
panel.title = "";
panel.titlebarAppearsTransparent = true;
// Center and focus the panel
panel.center();
panel.makeKeyAndOrderFront(null);
panel.setLevel(NSFloatingWindowLevel);
// Make the plugin's code stick around (since it's a floating panel)
COScript.currentCOScript().setShouldKeepAround(true);
// Hide the Minimize and Zoom button
panel.standardWindowButton(NSWindowMiniaturizeButton).setHidden(true);
panel.standardWindowButton(NSWindowZoomButton).setHidden(true);
複製代碼
// Create the blurred background
const vibrancy = NSVisualEffectView.alloc().initWithFrame(NSMakeRect(0, 0, panelWidth, panelHeight));
vibrancy.setAppearance(NSAppearance.appearanceNamed(NSAppearanceNameVibrantLight));
vibrancy.setBlendingMode(NSVisualEffectBlendingModeBehindWindow);
// Add it to the panel
panel.contentView().addSubview(vibrancy);
複製代碼
webview
渲染const wkwebviewConfig = WKWebViewConfiguration.alloc().init()
const webView = WKWebView.alloc().initWithFrame_configuration(
CGRectMake(0, 0, panelWidth, panelWidth),
wkwebviewConfig
)
// Add it to the panel
panel.contentView().addSubview(webView);
// load file URL
webview.loadFileURL_allowingReadAccessToURL(
NSURL.URLWithString(url),
NSURL.URLWithString('file:///')
)
複製代碼
咱們開發複雜的 Sketch 插件,通常都要開發側邊欄導航展現插件功能按鈕,點擊觸發相關操做。那開發側邊欄導航,咱們主要使用 AppKit
中的那些類呢,有 NSStackView
、 NSBox
、NSImage
、 NSImageView
、NSButton
等,大體核心代碼以下:
// create toolbar
const toolbar = NSStackView.alloc().initWithFrame(NSMakeRect(0, 0, 40, 400))
threadDictionary[SidePanelIdentifier] = toolbar
toolbar.identifier = SidePanelIdentifier
toolbar.setSpacing(8)
toolbar.setFlipped(true)
toolbar.setBackgroundColor(NSColor.windowBackgroundColor())
toolbar.orientation = 1
// add element
toolbar.addView_inGravity(createImageView(NSMakeRect(0, 0, 40, 22), 'transparent', NSMakeSize(40, 22)), 1)
const Logo = createImageView(NSMakeRect(0, 0, 40, 30), 'logo', NSMakeSize(40, 28))
toolbar.addSubview(Logo)
const contentView = context.document.documentWindow().contentView()
const stageView = contentView.subviews().objectAtIndex(0)
const views = stageView.subviews()
const existId = views.find(d => ''.concat(d.identifier()) === identifier)
const finalViews = []
for (let i = 0; i < views.count(); i++) {
const view = views[i]
if (existId) {
if (''.concat(view.identifier()) !== identifier) finalViews.push(view)
} else {
finalViews.push(view)
if (''.concat(view.identifier()) === 'view_canvas') {
finalViews.push(toolbar)
}
}
}
// add to main Window
stageView.subviews = finalViews
stageView.adjustSubviews()
複製代碼
詳細見開源代碼: github.com/o2team/sket… (歡迎 star 交流)
當插件運行時,Sketch 將會建立一個與其關聯的 JavaScript 上下文,可使用 Safari 來調試該上下文。
在 Safari 中, 打開 Developer
> 你的機器名稱 > Automatically Show Web Inspector for JSContexts
,同時啓用選項 Automatically Pause Connecting to JSContext
,不然檢查器將在能夠交互以前關閉(當腳本運行完時上下文會被銷燬)。
如今就能夠在代碼中使用斷點了,也能夠在運行時檢查變量的值等等。
JavaScriptCore 運行 Sketch 插件的環境 也有提供相似調試 JavaScript 代碼打 log 的方式,咱們能夠在關鍵步驟處放入一堆 console.log/console.error
等進行落點日誌查看。
有如下幾種選擇能夠查看日誌:
~/Library/Logs/com.bohemiancoding.sketch3/Plugin Output.log
文件skpm log
命令,該命令能夠輸出上面的文件(執行 skpm log -f
能夠流式地輸出日誌)console.log
打日誌查看。SketchTool 包含在 Sketch 中的 CLI 工具,經過 SketchTool 可對 Sketch 文檔執行相關操做:
sketchtool 二進制文件位於 Sketch 應用程序包中:
Sketch.app/Contents/Resources/sketchtool/bin/sketchtool
複製代碼
設置 alias
:
alias sketchtool="/Applications/Sketch.app/Contents/Resources/sketchtool/bin/sketchtool"
複製代碼
使用:
sketchtool -h # 查看幫助
sketchtool export artboards path/to/document.sketch # 導出畫板
sketchtool dump path/to/document.sketch # 導出 Sketch 文檔 JSON data
sketchtool metadata path/to/document.sketch # 查看 Sketch 文檔元數據
sketchtool run [Plugin path] # 運行插件
複製代碼
注意
:SketchTool 須要 OSX 10.11或更高版本。
AppKit, 構建 Sketch 的一個主要 Apple 框架
Foundation(基礎), 更重要的 Apple 課程和服務
歡迎關注凹凸實驗室博客:aotu.io
或者關注凹凸實驗室公衆號(AOTULabs),不定時推送文章: