如何製作 Sketch 外掛

公子發表於2020-10-13

Sketch 是近些年比較流行的 UI 設計軟體,它比起之前常用的 Illustrator 或者 Photoshop 比較好的地方在於小巧功能簡單但足夠,同時對 Mac 的觸控板支援更加友好。另外它的外掛系統也要比 Adobe 更加友好,大量的外掛幫助我們解決協同和效率上的問題。

Sketch 外掛最大的好處在於可以直接使用 JavaScript 進行開發,並提供了許多配套的開發工具。下面我就以幫助設計師同學快速插入佔點陣圖的外掛 Placeholder 為例,帶大家一步一步的瞭解如何進行 Sketch 外掛開發。

在進行外掛開發之前,我們需要了解一些基礎的知識。Sketch 是一套原生 Objective-C 開發的軟體,它之所以能支援使用 JS 開發,是因為它使用 CocoaScript 作為外掛的開發語言。它就像是一座橋(Bridge),能讓我們在外掛中寫 OC 和 JS,然後 Sketch 將基礎方法進行了封裝,實現了一套 JavaScript API,這樣我們就能使用 JS 開發 Sketch 外掛了。

注: 關於如何開發外掛,官方提供了一份入門教程《Create a plugin》,在閱讀下文之前,也可以花 2~3min 先看看這篇官方教程,內容比較簡短。

需求整理

在進行外掛開發之前,我們捋一捋我們需要實現的功能。http://placeimg.com/ 是一個專門用來生成佔點陣圖的網站,我們將利用該網站提供的服務製作一個生成指定大小的佔點陣圖並插入到 Sketch 畫板中的功能。外掛會提供一個皮膚,可以讓使用者輸入尺寸、分類等可選項,同時提供插入按鈕,點選後會在畫板插入一張圖片圖層。

使用 skpm 初始化專案

skpm 是 Sketch 官方提供的外掛管理工具,類比於 Node.js 中的 npm。它集外掛的建立、開發、構建、釋出等多項功能於一體,我們在很多場景都需要使用它。安裝的話比較簡單,直接使用 npm 全域性安裝即可。

npm install -g skpm

按照官方教程,安裝完畢之後我們就可以使用 skpm create 命令來初始化專案目錄了。當然 skpm 是支援基於模板初始化的,官方倉庫也列舉了一些模板,我們可以使用 --temlate 來指定模板進行初始化。不過處於教學的目的,我這裡就還是使用官方預設的模板建立了。

➜  ~ skpm create sketch-placeimg
✔ Done!


To get started, cd into the new directory:
  cd sketch-placeimg

To start a development live-reload build:
  npm run start

To build the plugin:
  npm run build

To publish the plugin:
  skpm publish

skpm 內部會使用 webpack 進行打包編譯,執行 npm run build 會生成 sketch-placeimg.sketchplugin 目錄,該目錄就是最終的外掛目錄。雙擊該目錄,或者將該目錄拖拽到 Sketch 介面上就成功安裝外掛了。和 webpack --watch 類似,執行 npm run watch 的話對監聽檔案變化實時編譯,在開發中非常有幫助。

注: 不要使用 npm start 進行開發,它攜帶的 --run 命令會使得構建速度特別慢。雖然它帶 Live Reload 功能會很方便,但在官方未修復該問題前還是不建議大家使用。

專案結構入門

建立好的模板目錄結構如下,為了幫助大家理解,我們來簡單的介紹下這些目錄和檔案。

.
├── README.md
├── assets
│   └── icon.png
├── sketch-assets
│   └── icon.sketch
├── sketch-placeimg.sketchplugin
│   └── Contents
│       ├── Resources
│       │   └── icon.png
│       └── Sketch
│           ├── manifest.json
│           ├── my-command.js
│           └── my-command.js.map
├── node_modules
├── package.json
└── src
    ├── manifest.json
    └── my-command.js

package.json

和大多數 JS 專案一樣,skpm 建立的專案中也會有 package.json 檔案。該檔案除了像之前一樣記錄了專案的依賴和快捷命令之外,還增加了 skpm 欄位用來對 skpm 進行配置,預設的值如下。

{
  ...
  "skpm": {
    "name": "sketch-placeimg",
    "manifest": "src/manifest.json",
    "main": "sketch-placeimg.sketchplugin",
    "assets": [
      "assets/**/*"
    ],
    "sketch-assets-file": "sketch-assets/icons.sketch"
  },
  ...
}

這裡指定了該外掛的名稱為 sketch-placeimg,外掛的 manifest 檔案為 src/manifest.jsonmain 表示的是最終生成的外掛目錄名稱。assets 則表示的外掛依賴的圖片等相關素材,在編譯的時候會將命中該配置的檔案拷貝到 <main>/Contents/Resources 目錄下。

manifest.json

manifest.json 這個檔案大家可以理解為是 Sketch 外掛的 package.json 檔案。我們來看看預設生成的 manifest.json

{
  "$schema": "https://raw.githubusercontent.com/sketch-hq/SketchAPI/develop/docs/sketch-plugin-manifest-schema.json",
  "icon": "icon.png",
  "commands": [
    {
      "name": "my-command",
      "identifier": "sketch-placeimg.my-command-identifier",
      "script": "./my-command.js"
    }
  ],
  "menu": {
    "title": "sketch-placeimg",
    "items": [
      "sketch-placeimg.my-command-identifier"
    ]
  }
}

看到 $schema 就有 JSON Schema 那味了,它對應的 JSON 檔案地址告訴我們可以在裡面配置那些欄位。其實最重要的其實就是上面列出來的 commandsmenu 兩個欄位。

commands 標記了外掛有哪些命令,這裡只有一個命令,命令的名稱(name)是 my-command,該命令的 ID(identifier)為 sketch-placeimg.my-command-identifier,對應的執行指令碼為 ./my-command.js

menu 則標記了該外掛的導航選單配置,比如示例這裡它指定了該外掛在外掛選單中的名稱(title)為 sketch-placeimg,並擁有一個子選單,對應的是 ID 為sketch-placeimg.my-command-identifier的命令。通過這個 ID,選單的行為就和執行指令碼關聯起來了。

appcast.xml

manifest.json 預設的示例中有兩個比較重要的欄位沒有配置,那就是 versionappcastversion 很明顯就是用來表示當前外掛的版本的。而 appcast 它的值是一個 XML 的 URL 地址,該 XML 裡面包含了該外掛所有的版本以及該版本對應的下載地址。Sketch 會將 version 對應的版本和 appcast 對應的 XML 進行對比,如果發現有新的版本了,會使用該版本對應的下載地址下載外掛,執行線上更新外掛。一個 appcast.xml 檔案大概是這樣的格式。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
  <channel>
    <item>
      <enclosure url="https://github.com/lizheming/sketch-placeimg/releases/download/v0.1.1/sketch-placeimg.sketchplugin.zip" sparkle:version="0.1.1"/>
    </item>
    <item>
      <enclosure url="https://github.com/lizheming/sketch-placeimg/releases/download/v0.1.0/sketch-placeimg.sketchplugin.zip" sparkle:version="0.1.0"/>
    </item>
  </channel>
</rss>

如果是通過 skpm publish 命令去釋出外掛的話,會自動在根目錄生成一個 .appcast.xml 檔案。當然按照官方文件 《Update a plugin》 所說,你也可以手動生成。

resource

從上面的內容我們可以知道,skpm 會通過 package.json 中指定的 manifest 檔案讀取所有 commands 對應的 script 檔案作為編譯入口檔案,將這些文件編譯打包輸出到 <main>/Contents/Sketch 目錄。所有的 assets 配置對應的檔案會拷貝到 <main>/Contents/Resources 目錄中。最終完成外掛的生成。

換句話來說只想要走 webpack 打包編譯的話就必須是外掛的命令才行。如果有一些依賴的非外掛類資源,比如外掛嵌入的 HTML 頁面依賴的 JS 檔案想要走編譯的話,就需要使用 resource 這個配置了。resource 配置中配置的檔案會走 webpack 的編譯打包,並輸出到 <main>/Contents/Resources 目錄中。

外掛開發

一些基本原理了解清楚之後我們就可以進行外掛的開發了。首先我們需要使用者點選外掛選單之後開啟一個皮膚,該皮膚可以配置尺寸、分類等基礎資訊。

Sketch 外掛中我們可以使用原生寫法進行皮膚的開發,但是這樣寫起 UI 來說比較麻煩,而且對前端同學來說入門比較高。所以一般大家都會採用 WebView 載入網頁的形式進行開發。原理基本上等同於移動端採用 WebView 載入網頁一樣,客戶端呼叫 WebView 方法載入網頁,通過例項的 webContents.executeJavaScript()方法進行外掛到網頁的通訊,而網頁中則使用被重定義的 window.postMessage 與外掛進行通訊。

sketch-module-web-view

想要在外掛中載入網頁,需要安裝 Sketch 封裝好的 sketch-module-web-view 外掛。

npm install sketch-module-web-view --save-dev
// src/my-command.js
import BrowserWindow from 'sketch-module-web-view';
export default function() {
  const browserWindow = new BrowserWindow({
    width: 510,
    height: 270,
    resizable: false,
    movable: false,
    alwaysOnTop: true,
    maximizable: false,
    minimizable: false
  });
  browserWindow.loadURL(require('../resources/webview.html'))
}

當你做完這些你會發現點選外掛選單後什麼都沒有發生,這是因為還需要更改一下配置。大家可以看到我們最後是使用了 require() 引入了一個 HTML 檔案,而官方預設的模板是沒有提供 HTML 引入的支援的,所以我們需要為 HTML 檔案增加對應的 webpack loader。

我們這裡需要的是 html-loader@skpm/extract-loader 兩款 Loader。前者是用來解析處理 HTML 中存在的包括 <link /> 或者 <img /> 之類的 HTML 程式碼中可能存在的資源關聯情況。而後者則是用來將 HTML 檔案拷貝到 <main>/Contents/Resources 目錄並返回對應的 file:/// 格式的檔案路徑 URL,用來在外掛中進行關聯。

npm install html-loader @skpm/extract-loader --save-dev

Sketch 外掛官方為我們自定義 webpack 配置也預留好了入口,在專案根目錄中建立 webpack.skpm.config.js 檔案,它匯出的方法接收的引數中第一個則是外掛最終的 webpack 配置,我們直接在這基礎上進行修改即可。

// webpack.skpm.config.js
module.exports = function (config, entry) {
  config.module.rules.push({
    test: /\.html$/,
    use: [
      { loader: "@skpm/extract-loader" },
      {
        loader: "html-loader",
        options: {
          attributes: {
            list: [
              { tag: 'img', attribute: 'src', type: 'src' },
              { tag: 'link', attribute: 'href', type: 'src' }
            ]
          }
        }
      }
    ]
  });
}

html-loader 外掛在新版裡對配置格式做了一些修改,所以之前很多老的教程中的配置都會報錯。當然如果你有更多的外掛需求也可以按照這個流程往配置物件中新增。之後我們再執行 npm run watch,點選選單就可以看到我們預期的頁面了。

注: 官方是提供了一套帶有 sketch-module-web-view 模組的模板的,這裡只是為了能更清楚的給大家解釋清楚外掛的原理和流程所以和他家一步一步的進行說明。真實的開發場景中建議大家直接使用以下命令進行快速初始化。

skpm create <plugin-name> --template=skpm/with-webview

React 的整合

皮膚這塊我準備使用 React 進行開發,主要是有 React Desktoop 這個 React 元件,能夠很好的在 Web 中模擬 Mac OSX 的 UI 風格(雖然也就幾個表單沒什麼好模擬的就是了)。

令人開心的是 skpm 預設的 webpack 配置已經增加了 React 的支援,所以我們不需要額外的增加 webpack 的配置,只需要把 React 相關的依賴安裝好就可以進行開發了。

npm install react react-dom react-desktop --save-dev

增加 webview.js 入口檔案。由於該檔案需要走 webpack 編譯,但是又不是外掛命令的執行檔案,所以我們需要像上文說的,將入口檔案加入到 package.jsonskpm.resources 配置中。

// package.json
{
  "skpm": {
    "resources": [
      "resources/webview.js"
    ]
  }
}

// resources/webview.js
import React from 'react';
import ReactDOM from 'react-dom';

function App() {
  return (<>
    <p>Hello World!</p>
    <hr />
    via: <em>@lizheming</em>
  </>)
}

ReactDOM.render(<App />, document.getElementById('app'));

webview.html 也需要改造一下,引入 JS 入口檔案。這裡需要注意一下 ../resource_webview.js 這個引用檔案地址,這是 JS 入口檔案編譯後最終的檔案地址。主要是因為 HTML 檔案最終會生成到 <name>.sketchplugin/Resources/_webpack_resources 目錄下,而 JS 入口檔案會將 / 分隔符替換成 _ 分隔符,生成在 <name>.sketchplugin/Resources 目錄下。

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8" />
    <title>PlaceIMG</title>
  </head>
  <body>
    <div id="app"></div>
    <script src="../resources_webview.js"></script>
  </body>
</html>

注:

  1. HTML 檔案生成到 _webpack_resources 配置
  2. JS 入口檔案生成到 Resource 目錄配置

皮膚開發

流程打通了之後接下來我們可以專心進行皮膚的開發了。皮膚開發這塊就不多描述了,無非就是前段頁面的編寫而已,最後外掛皮膚大概是長這樣子的。

-_-||嗯,其實我就是想和大家講下流程硬上 React 的…

選擇完畢點選插入後,呼叫 postMessage() 方法將最終的配置傳遞給外掛。

//resources/webview.js
import React, {useReducer} from 'react';

function App() {
  const [{width, height, category, filter}, dispatch] = useReducer(
    (state, {type, ...payload}) => ({...state, ...payload}),
    {width: undefind, height: undefined, category: 'any', filter: 'none'}
  );
  const onInsert = _ => postMessage('insert', width, height, category, filter);
  return (
    <button onClick={onInsert}>插入</button>
  );
}
注:Web 原生的 postMessage() 方法的語法為 postMessage(message, targetOrigin, [transfer])。事件名稱和事件引數都應該序列化之後通過 message 引數傳入。

Sketch 外掛中的 postMessage() 方法是注入方法,它對原生的方法進行了複寫,所以引數格式上會與原生的不一樣。注入方法的實現可參見 sketch-module-web-view 程式碼

在外掛中,我們監聽 insert 事件,獲取到使用者選擇的配置之後給生成圖片圖層插入到畫板中。

//src/my-command.js
import sketch, { Image, Rectangle } from 'sketch/dom';
import BrowserWindow from 'sketch-module-web-view';

export default function() {
  const browserWindow = new BrowserWindow({...});
  browserWindow.webContents.on('insert', function(width, height, category, filter) {
    const url = 'https://placeimg.com/' + [width, height, category, filter].join('/');
    new Image({
      image:  NSURL.URLWithString(url),
      parent: getSelectedArtboard(),
      frame: new Rectangle(0, 0, width, height),
    });
    return browserWindow.close();
  });
}

外掛釋出

最終我們的外掛的主體功能就開發完畢了。下面我們就可以進行外掛的釋出了。我們可以直接使用 skpm publish 進行釋出,它需要你通過 skpm publish --repo-url 或者是 package.json 中的 repository 欄位為外掛指定 Github 倉庫地址。

Personal Access Token 頁面為 skpm 申請新的 Token,記得勾選上 repo 操作的許可權。使用 skpm login <token> 進行登入之後,skpm 就獲得了操作專案的許可權。

最後通過 skpm publish <version> 就可以成功釋出了。如前文所說,釋出後會在專案目錄建立 .appcast.xml 檔案,同時會發布一條對應版本的 Release 記錄,提供外掛的 zip 包下載地址。執行完 publish 操作後,如果發現你的外掛還沒有在外掛中心倉庫中列出來,還會詢問你是否提交個 PR 把自己的外掛增加上。

當然如果你的外掛不方便釋出到 Github 上,也可以使用前文所說的手工釋出,執行 skpm build 後對生成的 <name>.sketchplugin 目錄進行打包即可。

外掛除錯

上文的示例外掛比較簡單,所以沒有使用特別多的除錯手段。在官方教程《Debug a plugin》中描述了多種可以進行除錯的方式。用的比較多的還是日誌除錯方式,可以使用系統的 Console.app 檢視日誌,也可以使用 skpm log -f 外掛日誌。

文件裡說的大部分是外掛的除錯,WebView 內的前端程式碼除錯會更簡單一點。WebView 窗體右鍵審查元素即可使用 Safari 的開發者工具進行除錯了。

注:外掛本身的程式碼本質是客戶端程式碼,WebView 本質是前端程式碼,所以兩者的除錯和日誌輸出位置都是有區別的,這裡要注意區分。

後記

以上就是開發 Sketch 的一些基礎知識和簡單流程,其它的就是多去看一下 Sketch API 文件了。不過在實際的使用中 Sketch 的這套 JavaScript API 並不是非常完美,部分功能可能還暫時需要使用原生 API 區別。這時候可以多 Google 一下,能找到很多前人的實現,節省自己的工作量。

本文主要是介紹了一套 JavaScript API + WebView 的偏前端的開發方式,程式碼我都已經放到 Github 上 https://github.com/lizheming/...,大家可以自行查閱和下載。除了這種方式之外,我們也可以使用 OC + WebView 甚至是純 OC 客戶端的方式去開發外掛。使用純客戶端開發的話效能會比 JavaScript API 的形式好一點,但是對於不瞭解 OC 開發的前端同學來說上手難度還是比較高的。

除了 Sketch 之外,Figma 也是一款非常棒的 UI 設計軟體。它基於 Web 開發,天生跨平臺,更提供了更加易用的協作模式,解決 UI 開發中的多人協作問題。感興趣的同學也可以去了解一下。

參考資料:

  1. 《Sketch外掛開發總結》

相關文章