如何使用 JavaScript、HTML 和 CSS 構建 Sketch 插件(第 2 部分)

已發表: 2022-03-10
快速總結↬在我們關於構建 Sketch 插件的教程的第二部分中,我們將繼續構建我們的用戶界面,然後我們將繼續討論實際生成圖層馬賽克和優化最終插件代碼。

如第 1 部分所述,本教程適用於了解和使用 Sketch 應用程序並且不害怕涉足代碼的人。 要從中獲得最大收益,您至少需要具備一些編寫 JavaScript(以及可選的 HTML/CSS)的基本經驗。

在本教程的前一部分中,我們了解了組成插件的基本文件,以及如何創建插件的用戶界面。 在第二部分也是最後一部分,我們將學習如何將用戶界面連接到核心插件代碼以及如何實現插件的主要功能。 最後但同樣重要的是,我們還將學習如何優化代碼以及插件的工作方式。

構建插件的用戶界面:讓我們的 Web 界面和 Sketch 插件代碼相互“對話”

接下來我們需要做的是在我們的 Web 界面和 Sketch 插件之間建立通信。

當我們點擊網頁界面中的“應用”按鈕時,我們需要能夠從我們的網頁界面向 Sketch 插件發送消息。 此消息需要告訴我們用戶輸入了哪些設置——例如步數、旋轉量、要創建的副本數等。

WKWebView使這項任務對我們來說更容易一些:我們可以使用window.webkit.messageHandlers API 從 Web 界面的 JavaScript 代碼向 Sketch 插件發送消息。

在我們的 Sketch 代碼方面,我們可以使用另一種方​​法addScriptMessageHandler:name: (或addScriptMessageHandler_name )來註冊一個消息處理程序,當它接收到從我們的插件 Web 界面發送的消息時將調用該消息處理程序。

讓我們首先確保我們可以從我們的 Web UI 接收消息。 轉到我們的ui.js文件的createWebView函數,並添加以下內容:

 function createWebView(pageURL){ const webView = WKWebView.alloc().init(); // Set handler for messages from script const userContentController = webView.configuration().userContentController(); const ourMessageHandler = ... userContentController.addScriptMessageHandler_name( ourMessageHandler, "sketchPlugin" ); // Load page into web view webView.loadFileURL_allowingReadAccessToURL(pageURL, pageURL.URLByDeletingLastPathComponent()); return webView; };

在這裡,我們使用 web 視圖的userContentController屬性來添加我們命名為“sketchPlugin”的消息處理程序。 這個“用戶內容控制器”是確保消息從我們的 Web 視圖中傳遞出來的橋樑。

跳躍後更多! 繼續往下看↓

您可能已經註意到上述代碼的一些奇怪之處:我們作為消息處理程序添加的對象ourMessageHandler還不存在! 不幸的是,我們不能只使用常規的 JavaScript 對像或函數作為處理程序,因為這種方法需要某種原生對象。

對我們來說幸運的是,我們可以通過使用MochaJSDelegate來解決這個限制,這是我編寫的一個迷你庫,可以使用常規的 JavaScript 創建我們需要的原生對象。 您需要手動下載並將其保存在Sketch/MochaJSDelegate.js下的插件包中。

為了使用它,我們需要先將它導入ui.js 。 在文件頂部添加以下內容:

 const MochaJSDelegate = require("./MochaJSDelegate");

現在我們可以使用MochaJSDelegate創建消息處理程序的類型addScriptMessageHandler:name:期望:

 function createWebView(pageURL){ const webView = WKWebView.alloc().init(); // Set handler for messages from script const userContentController = webView.configuration().userContentController(); const scriptMessageHandler = new MochaJSDelegate({ "userContentController:didReceiveScriptMessage:": (_, wkMessage) => { /* handle message here */ } }).getClassInstance(); userContentController.addScriptMessageHandler_name( scriptMessageHandler, "sketchPlugin" ); // Load page into web view webView.loadFileURL_allowingReadAccessToURL(pageURL, pageURL.URLByDeletingLastPathComponent()); return webView; };

我們剛剛添加的代碼創建了我們需要的原生對象。 它還在該對像上定義了一個名為userContentController:didReceiveScriptMessage:的方法——然後使用我們想要的消息作為第二個參數調用此方法。 由於我們實際上還沒有發送任何消息,所以我們稍後必須回到這裡並添加一些代碼來實際解析和處理我們收到的消息。

接下來,我們需要在 Web 界面中添加一些代碼來向我們發送這些消息。 前往/Resources/web-ui/script.js 。 您會發現我已經編寫了大部分代碼來處理檢索用戶將在其中輸入選項的 HTML <inputs />的值。

我們還剩下要做的是將實際發送值的代碼添加到我們的 Sketch 代碼中:

找到apply函數並將以下內容添加到它的末尾:

 // Send user inputs to sketch plugin window.webkit.messageHandlers.sketchPlugin.postMessage(JSON.stringify({ stepCount, startingOptions, stepOptions }));

這裡我們使用前面提到的window.webkit.messageHandlers API 來訪問我們上面註冊為sketchPlugin的消息處理程序。 然後使用包含用戶輸入的 JSON 字符串向其發送消息。

讓我們確保一切設置正確。 回到/Sketch/ui.js 。 為了確保我們收到預期的消息,我們將修改我們之前定義的方法,以便在收到消息時顯示一個對話框:

 function createWebView(pageURL){ // ... const scriptMessageHandler = new MochaJSDelegate({ "userContentController:didReceiveScriptMessage:": (_, wkMessage) => { const UI = require("sketch/ui"); UI.alert("Hey, a message!", wkMessage.body()); } }).getClassInstance(); userContentController.addScriptMessageHandler_name( scriptMessageHandler, "sketchPlugin" ); // ... };

現在運行插件(您可能需要先關閉已打開的任何現有 Mosaic 窗口),輸入一些值,然後單擊“應用”。 您應該會看到如下所示的警報 - 這意味著一切都已正確連接並且我們的消息已成功通過! 如果沒有,請返回前面的步驟並確保一切都按照描述完成。

圖像顯示了單擊插件 UI 中的“應用”按鈕後應該看到的對話框。
單擊應用後,您應該看到的對話框出現。 (大預覽)

現在我們能夠從我們的界面向我們的插件發送消息,我們可以繼續編寫代碼,該代碼實際上對這些信息有用:生成我們的圖層馬賽克。

生成圖層馬賽克

讓我們盤點一下為了實現這一目標需要做些什麼。 稍微簡化一下,我們的代碼需要做的是:

  1. 查找當前文檔。
  2. 查找當前文檔的選定圖層。
  3. 複製所選圖層(我們將其稱為模板圖層)x 次。
  4. 對於每個副本,通過用戶設置的特定值(數量)調整其位置、旋轉、不透明度等。

現在我們有了一個合理的計劃,讓我們繼續寫。 堅持我們模塊化代碼的模式,讓我們在Sketch/文件夾中創建一個新文件, mosaic.js ,並向其中添加以下代碼:

 function mosaic(options){ }; module.export = mosaic;

我們將使用這個函數作為這個模塊的唯一導出,因為一旦我們導入它,它就可以使用更簡單的 API ——我們可以使用從 Web 界面獲得的任何選項來調用mosaic()

我們需要採取的前兩個步驟是獲取當前文檔,然後是其選定層。 Sketch API 有一個用於文檔操作的內置庫,我們可以通過導入sketch/dom模塊來訪問它。 我們現在只需要Document對象,所以我們將顯式地取出它。 在文件頂部,添加:

 const { Document } = require("sketch/dom");

Document對像有一個專門用於訪問我們可以使用的當前文檔的方法,稱為getSelectedDocument() 。 一旦我們有了當前的文檔實例,我們就可以通過文檔的selectedLayers屬性訪問用戶選擇的任何層。 但是,在我們的例子中,我們只關心單層選擇,所以我們只會抓取用戶選擇的第一層:

 function mosaic(options){ const document = Document.getSelectedDocument(); const selectedLayer = document.selectedLayers.layers[0]; }; module.export = mosaic;

注意:您可能一直期望selectedLayers本身是一個數組,但事實並非如此。 相反,它是Selection類的一個實例。 這是有原因的: Selection類包含一堆有用的幫助方法,用於操作選擇,如 clear、map、reduce 和 forEach。 它通過layer屬性暴露了實際的層數組。

我們還添加一些警告反饋,以防用戶忘記打開文檔或選擇某些內容:

 const UI = require("sketch/ui"); function mosaic(options){ const document = Document.getSelectedDocument(); // Safety check: if(!document){ UI.alert("Mosaic", "️ Please select/focus a document."); return; } // Safety check: const selectedLayer = document.selectedLayers.layers[0]; if(!selectedLayer){ UI.alert("Mosaic", "️ Please select a layer to duplicate."); return; } }; module.export = mosaic;

現在我們已經編寫了第 1 步和第 2 步的代碼(查找當前文檔和選定層),我們需要處理第 3 步和第 4 步:

  • 複製模板層 x 次。
  • 對於每個副本,通過用戶設置的特定值調整其位置、旋轉、不透明度等。

讓我們從options中提取我們需要的所有相關信息開始:複製的次數、開始選項和步驟選項。 我們可以再次使用解構(就像我們之前對Document所做的那樣)將這些屬性從options中提取出來:

 function mosaic(options) { // ... // Destructure options: var { stepCount, startingOptions, stepOptions } = options; }

接下來,讓我們清理輸入並確保步數始終至少為 1:

 function mosaic(options) { // ... // Destructure options: var { stepCount, startingOptions, stepOptions } = options; stepCount = Math.max(1, stepCount); }

現在我們需要確保模板層的不透明度、旋轉等都與用戶期望的起始值相匹配。 由於將用戶的選項應用於圖層將是我們要做的很多事情,因此我們將把這項工作轉移到它自己的方法中:

 function configureLayer(layer, options, shouldAdjustSpacing){ const { opacity, rotation, direction, spacing } = options; layer.style.opacity = opacity / 100; layer.transform.rotation = rotation; if(shouldAdjustSpacing){ const directionAsRadians = direction * (Math.PI / 180); const vector = { x: Math.cos(directionAsRadians), y: Math.sin(directionAsRadians) }; layer.frame.x += vector.x * spacing; layer.frame.y += vector.y * spacing; } };

而且因為間距只需要在副本之間而不是模板層之間應用,我們添加了一個特定的標誌, shouldAdjustSpacing ,我們可以設置為truefalse取決於我們是否將選項應用於模板層或不是。 這樣我們可以確保將旋轉和不透明度應用於模板,而不是間距。

回到mosaic方法,現在讓我們確保將起始選項應用於模板層:

 function mosaic(options){ // ... // Configure template layer var layer = group.layers[0]; configureLayer(layer, startingOptions, false); }

接下來,我們需要創建我們的副本。 首先,讓我們創建一個變量,我們可以使用它來跟踪當前副本的選項是什麼:

 function mosaic(options){ // ... var currentOptions; // ... }

由於我們已經將起始選項應用到模板層,我們需要採用我們剛剛應用的那些選項並添加stepOptions的相對值,以便將選項應用到下一層。 由於我們還將在循環中多次執行此操作,因此我們還將將此工作移至特定方法stepOptionsBy

 function stepOptionsBy(start, step){ const newOptions = {}; for(let key in start){ newOptions[key] = start[key] + step[key]; } return newOptions; };

之後,我們需要編寫一個循環來複製前一層,將當前選項應用於它,然後偏移(或“步進”)當前選項以獲得下一個副本的選項:

 function mosaic(options) { // ... var currentOptions = stepOptionsBy(startingOptions, stepOptions); for(let i = 0; i < (stepCount - 1); i++){ let duplicateLayer = layer.duplicate(); configureLayer(duplicateLayer, currentOptions, true); currentOptions = stepOptionsBy(currentOptions, stepOptions); layer = duplicateLayer; } }

一切都完成了——我們已經成功編寫了插件應該做的核心! 現在,我們需要連接起來,這樣當用戶真正點擊“應用”按鈕時,我們的馬賽克代碼就會被調用。

讓我們回到ui.js並調整我們的消息處理代碼。 我們需要做的是解析我們得到的選項的 JSON 字符串,以便將它們變成我們可以實際使用的對象。 一旦我們有了這些選項,我們就可以用它們調用mosaic函數。

首先,解析。 我們需要更新我們的消息處理函數來解析我們得到的 JSON 消息:

 function createWebView(pageURL){ // ... const scriptMessageHandler = new MochaJSDelegate({ "userContentController:didReceiveScriptMessage:": (_, wkMessage) => { const message = JSON.parse(wkMessage.body()); } }); }

接下來,我們需要將其傳遞給我們的mosaic函數。 然而,這並不是我們在ui.js中的代碼真正應該做的事情——它應該主要關注在屏幕上顯示與界面相關的東西所必需的東西——而不是創建馬賽克本身。 為了將這些職責分開,我們將向createWebView添加第二個參數,該參數接受一個函數,每當我們從 Web 界面接收選項時,我們都會調用該函數。

讓我們將此參數命名為onApplyMessage

 function createWebView(pageURL, onApplyMessage){ // ... const scriptMessageHandler = new MochaJSDelegate({ "userContentController:didReceiveScriptMessage:": (_, wkMessage) => { const message = JSON.parse(wkMessage.body()); onApplyMessage(message); } }); }

我們還需要修改我們導出的方法loadAndShow ,以獲取這個onApplyMessage參數並將其傳遞給createWebView

 function loadAndShow(baseURL, onApplyMessage){ // ... const webView = createWebView(pageURL, onApplyMessage); }

最後,轉到main.js 。 我們現在需要導入我們的mosaic函數,並使用我們從插件用戶界面收到的選項調用它:

 const mosaic = require("./mosaic"); function onRun(context){ UI.loadAndShow(context.scriptURL, options => { mosaic(options); }); };

我們快完成了!

但是,如果我們現在運行代碼並單擊插件界面中的“應用”按鈕,則不會發生任何事情。 為什麼? 原因在於 Sketch 腳本的運行方式:默認情況下,它們“存活”到腳本的底部,之後 Sketch 會銷毀它並釋放它正在使用的任何資源。

這對我們來說是個問題,因為這意味著我們需要異步發生的任何事情(在這種情況下,就是在到達我們代碼的底部之後),比如接收消息,因為我們的腳本已被破壞,所以不能。 這意味著我們不會從 Web 界面收到任何消息,因為我們無法接收和回复它們!

有一種方法可以使用Fibers向 Sketch 發出信號,我們需要我們的腳本在此之後保持活力。 通過創建一個 Fiber,我們告訴 Sketch 正在發生一些異步的事情,它需要保留我們的腳本。 然後 Sketch 只會在絕對必要的時候銷毀我們的腳本(比如用戶關閉 Sketch,或者當 Mosaic 插件需要更新時):

 // ... const Async = require("sketch/async"); var fiber; function onRun(context){ if(!fiber){ fiber = Async.createFiber(); fiber.onCleanup(() => { UI.cleanup(); }); } UI.loadAndShow(context.scriptURL, options => { mosaic(options); }); };

瞧! 現在讓我們試試我們的插件。 在 Sketch 中選擇一個圖層,輸入一些設置,然後單擊應用:

現在讓我們試試我們的插件——在 Sketch 中選擇一個圖層,輸入一些設置,然後單擊“應用”。

最終改進

現在我們已經實現了插件的大部分功能,我們可以嘗試“縮小”一點,看看大圖。

改善用戶體驗

如果您在當前狀態下使用過插件,您可能已經註意到,當您嘗試編輯馬賽克時,會出現最大的摩擦點之一。 創建一個後,您必須點擊撤消,調整選項,然後單擊“應用”(或按 Enter)。 在您離開文檔並稍後返回它之後,這也使得編輯馬賽克變得更加困難,因為您的撤消/重做歷史將被清除,讓您自己手動刪除重複的圖層。

在更理想的流程中,用戶只需選擇一個 Mosaic 組,調整選項並觀看 Mosaic 更新,直到他們得到他們正在尋找的確切排列。 為了實現這一點,我們有兩個問題需要解決:

  1. 首先,我們需要一種方法將構成馬賽克的副本組合在一起。 Sketch 提供了 Groups 的概念,我們可以用它來解決這個問題。
  2. 其次,我們需要一種方法來區分普通的、用戶創建的組和 Mosaic 組。 Sketch 的 API 還為我們提供了一種在任何給定層上存儲信息的方法,我們可以將其用作方式標記,然後將組標識為我們的“特殊”馬賽克組之一。

讓我們重新審視我們在上一節中編寫的邏輯來解決這個問題。 我們的原始代碼遵循以下步驟:

  1. 查找當前文檔。
  2. 查找當前文檔的選定圖層。
  3. 複製所選圖層(我們將其稱為模板圖層)x 次。
  4. 對於每個副本,通過用戶設置的特定值(數量)調整其位置、旋轉、不透明度等。

為了使我們的新用戶流成為可能,我們需要將這些步驟更改為:

  1. 抓取當前文檔。
  2. 抓取當前文檔的選定圖層。
  3. 確定所選圖層是否為馬賽克組。
    • 如果是其他圖層,則將其用作模板圖層並轉到步驟 4。
    • 如果Mosaic 組,則將其中的第一層視為模板層,然後轉到步驟 5。
  4. 將模板層包裹在一個組內,並將該組標記為馬賽克組。
  5. 從組內刪除除模板層之外的所有層。
  6. 複製模板層 x 次。
  7. 對於每個副本,通過用戶設置的特定值調整其位置、旋轉、不透明度等。

我們有了三個新步驟。 對於第一個新步驟,第 3 步,我們將創建一個名為findOrMakeSpecialGroupIfNeeded的函數,該函數將查看傳遞給它的圖層以確定它是否是一個馬賽克組。 如果是,我們就退貨。 由於用戶可能會選擇嵌套在 Mosaic 組深處的子圖層,因此我們還需要檢查所選圖層的父圖層以判斷它們是否也是我們的 Mosaic 組之一:

 function findOrMakeSpecialGroupIfNeeded(layer){ // Loop up through the parent hierarchy, looking for a special group var layerToCheck = layer; while(layerToCheck){ if(/* TODO: is mosaic layer? */){ return layerToCheck; } layerToCheck = layerToCheck.parent; } };

如果我們無法找到 Mosaic 組,我們只需將傳入的圖層包裹在Group中,然後將其標記為 Mosaic 組。

回到文件頂部,我們現在也需要拉出 Group 類:

 const { Document, Group } = require("sketch/dom");
 function findOrMakeSpecialGroupIfNeeded(layer){ // Loop up through the parent hierarchy, looking for a special group var layerToCheck = layer; while(layerToCheck){ if(/* TODO: is mosaic layer? */){ return layerToCheck; } layerToCheck = layerToCheck.parent; } // Group const destinationParent = layer.parent; const group = new Group({ name: "Mosaic Group", layers: [ layer ], parent: destinationParent }); /* TODO: mark group as mosaic layer */ return group; };

現在我們需要填補空白(待辦事項)。 首先,我們需要一種方法來確定一個群體是否屬於我們的特殊群體之一。 在這裡,Sketch 庫的Settings模塊來幫助我們。 我們可以使用它在特定層上存儲自定義信息,也可以將其讀回。

一旦我們在文件頂部導入模塊:

 const Settings = require("sketch/settings");

然後我們可以使用它提供的兩個關鍵方法setLayerSettingForKeylayerSettingForKey來設置和讀取圖層的數據:

 function findOrMakeSpecialGroupIfNeeded(layer){ const isSpecialGroupKey = "is-mosaic-group"; // Loop up through the parent hierarchy, looking for a special group var layerToCheck = layer; while(layerToCheck){ let isSpecialGroup = Settings.layerSettingForKey(layerToCheck, isSpecialGroupKey); if(isSpecialGroup) return layerToCheck; layerToCheck = layerToCheck.parent; } // Group const destinationParent = layer.parent; layer.remove(); // explicitly remove layer from it's existing parent before adding it to group const group = new Group({ name: "Mosaic Group", layers: [ layer ], parent: destinationParent }); Settings.setLayerSettingForKey(group, isSpecialGroupKey, true); return group; };

現在我們已經有了一個方法來處理在鑲嵌組中包裝圖層(或者,如果已經是鑲嵌組,則返回它),我們現在可以在安全檢查之後將其插入到我們的主mosaic方法中:

 function mosaic(options){ // ... safety checks ... // Group selection if needed: const group = findOrMakeSpecialGroupIfNeeded(selectedLayer); }

接下來,我們將添加一個循環以從組中刪除除模板層(這是第一個)之外的所有層:

 function mosaic(options) { // ... // Remove all layers except the first: while(group.layers.length > 1){ group.layers[group.layers.length - 1].remove(); } }

最後,我們將確保組的大小適合其新內容,因為用戶可能最初選擇了嵌套在舊組中的層(我們可能已刪除的層)。

我們還需要確保將當前選擇設置為我們的馬賽克組本身。 這將確保如果用戶對同一個馬賽克組進行大量快速更改,它不會被取消選擇。 在我們已經編寫的複製圖層的代碼之後,添加:

 function mosaic(options) { // ... // Fit group to duplicates group.adjustToFit(); // Set selection to the group document.selectedLayers.clear(); group.selected = true; }

再次嘗試插件。 您應該會發現現在編輯馬賽克更加順暢!

改進界面

您可能會注意到的另一件事是顯示窗口和其中的界面之間缺乏同步,因為它們都同時變得可見。 這是因為當我們顯示窗口時,Web 界面並不能保證加載完成,所以有時它會在之後“彈出”或“閃入”。

解決此問題的一種方法是偵聽 Web 界面何時完成加載,然後才顯示我們的窗口。 有一個方法webView:didFinishNavigation: ,噹噹前頁面完成加載時,WKWebView 將調用該方法。 我們可以使用它來準確獲取我們正在尋找的通知。

回到ui.js ,我們將擴展我們創建的MochaJSDelegate實例來實現這個方法,它會依次調用我們將傳遞給createWebViewonLoadFinish參數:

 function createWebView(pageURL, onApplyMessage, onLoadFinish){ const webView = WKWebView.alloc().init(); // Create delegate const delegate = new MochaJSDelegate({ "webView:didFinishNavigation:": (webView, navigation) => { onLoadFinish(); }, "userContentController:didReceiveScriptMessage:": (_, wkMessage) => { const message = JSON.parse(wkMessage.body()); onApplyMessage(message); } }).getClassInstance(); // Set load complete handler webView.navigationDelegate = delegate; // Set handler for messages from script const userContentController = webView.configuration().userContentController(); userContentController.addScriptMessageHandler_name(delegate, "sketchPlugin"); // Load page into web view webView.loadFileURL_allowingReadAccessToURL(pageURL, pageURL.URLByDeletingLastPathComponent()); return webView; };

回到loadAndShow方法,我們將對其進行調整,使其僅在 Web 視圖加載後才顯示窗口:

 function loadAndShow(baseURL, onApplyMessage){ // ... const window = createWindow(); const webView = createWebView(pageURL, onApplyMessage, () => { showWindow(window); }); window.contentView = webView; _window = window; };

答對了! 現在我們的窗口僅在 web 視圖完成加載時顯示,避免了煩人的視覺閃爍。

結論

恭喜,你已經構建了你的第一個 Sketch 插件!

如果您想安裝和使用 Mosaic,可以從 GitHub 下載完整的插件。 在您出發之前,這裡有一些資源可能會在您接下來的旅程中派上用場:

  • developer.sketchapp.com 關於 Sketch 插件開發的官方資源。 包含幾個有用的指南,以及 Sketch JavaScript 庫的 API 參考。
  • sketchplugins.com 一個很棒且有用的 Sketch 插件開發者社區。 非常適合回答您所有迫切的問題。
  • github.com/sketchplugins/plugin-directory 官方,Sketch 插件的中央 GitHub 存儲庫。 你可以在這裡提交你的插件並與 Sketch 社區的其他人分享!