Sketch 是近些年比較流行的 UI 設計軟件,它比起之前常用的 Illustrator 或者 Photoshop 比較好的地方在于小巧功能簡單但足夠,同時對 Mac 的觸摸板支持更加友好。另外它的插件系統也要比 Adobe 更加友好,大量的插件幫助我們解決協同和效率上的問題。
Sketch 插件最大的好處在于可以直接使用 JavaScript 進行開發,并提供了許多配套的開發工具。下面我就以幫助設計師同學快速插入占位圖的插件 Placeholder 為例,帶大家一步一步的了解如何進行 Sketch 插件開發。
在進行插件開發之前,我們需要了解一些基礎的知識。Sketch 是一套原生 Objective-C 開發的軟件,它之所以能支持使用 JS 開發,是因為它使用 CocoaScript1作為插件的開發語言。它就像是一座橋(Bridge),能讓我們在插件中寫 OC 和 JS,然后 Sketch 將基礎方法進行了封裝,實現了一套 JavaScript API2,這樣我們就能使用 JS 開發 Sketch 插件了。
注: 關于如何開發插件,官方提供了一份入門教程《Create a plugin》3,在閱讀下文之前,也可以花 2~3min 先看看這篇官方教程,內容比較簡短。
需求整理
在進行插件開發之前,我們捋一捋我們需要實現的功能。http://placeimg.com/ 是一個專門用來生成占位圖的網站,我們將利用該網站提供的服務制作一個生成指定大小的占位圖并插入到 Sketch 畫板中的功能。插件會提供一個面板,可以讓使用者輸入尺寸、分類等可選項,同時提供插入按鈕,點擊后會在畫板插入一張圖片圖層。
使用 skpm 初始化項目
skpm 是 Sketch 官方提供的插件管理工具,類比于 Node.js 中的 npm。它集插件的創建、開發、構建、發布等多項功能于一體,我們在很多場景都需要使用它。安裝的話比較簡單,直接使用 npm 全局安裝即可。
npm install -g skpm
按照官方教程,安裝完畢之后我們就可以使用 skpm create 命令來初始化項目目錄了。當然 skpm 是支持基于模板初始化的,官方倉庫也列舉了一些模板4,我們可以使用 --temlate 來指定模板進行初始化。不過處于教學的目的,我這里就還是使用官方默認的模板創建了。
? ~ skpm create sketch-placeimg
? Done!
To get started, cd into the new directory:
cd sketch-placeimgTo start a development live-reload build:
npm run startTo build the plugin:
npm run buildTo publish the plugin:
skpm publish
skpm 內部會使用 webpack 進行打包編譯,運行 npm run build 會生成 sketch-placeimg.sketchplugin 目錄,該目錄就是最終的插件目錄。雙擊該目錄,或者將該目錄拖拽到 Sketch 界面上就成功安裝插件了。和 webpack --watch 類似,運行 npm run watch 的話對監聽文件變化實時編譯,在開發中非常有幫助。
注: 不要使用 npm start 進行開發,它攜帶的 --run 命令會使得構建速度特別慢 5。雖然它帶 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.json。main 表示的是最終生成的插件目錄名稱。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 文件地址告訴我們可以在里面配置那些字段。其實最重要的其實就是上面列出來的 commands 和 menu 兩個字段。
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 默認的示例中有兩個比較重要的字段沒有配置,那就是 version 和 appcast。version 很明顯就是用來表示當前插件的版本的。而 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》6所說,你也可以手動生成。
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 7 這個 React 組件,能夠很好的在 Web 中模擬 Mac OSX 的 UI 風格(雖然也就幾個表單沒什么好模擬的就是了)。
令人開心的是 skpm 默認的 webpack 配置已經增加了 React 的支持,所以我們不需要額外的增加 webpack 的配置,只需要把 React 相關的依賴安裝好就可以進行開發了。
npm install react react-dom react-desktop --save-dev
增加 webview.js 入口文件。由于該文件需要走 webpack 編譯,但是又不是插件命令的執行文件,所以我們需要像上文說的,將入口文件加入到 package.json 的 skpm.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 配置 8
2.JS 入口文件生成到Resource 目錄配置9
面板開發
流程打通了之后接下來我們可以專心進行面板的開發了。面板開發這塊就不多描述了,無非就是前段頁面的編寫而已,最后插件面板大概是長這樣子的。
-_-||嗯,其實我就是想和大家講下流程硬上 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 代碼 10 。
在插件中,我們監聽 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 Token11 頁面為 skpm 申請新的 Token,記得勾選上 repo 操作的權限。使用 skpm login <token> 進行登錄之后,skpm 就獲得了操作項目的權限。
最后通過 skpm publish <version> 就可以成功發布了。如前文所說,發布后會在項目目錄創建 .appcast.xml 文件,同時會發布一條對應版本的 Release 記錄,提供插件的 zip 包下載地址。執行完 publish 操作后,如果發現你的插件還沒有在 插件中心倉庫12 中列出來,還會詢問你是否提交個 PR 把自己的插件增加上。
當然如果你的插件不方便發布到 Github 上,也可以使用前文所說的手工發布,執行 skpm build 后對生成的 <name>.sketchplugin 目錄進行打包即可。
插件調試
上文的示例插件比較簡單,所以沒有使用特別多的調試手段。在官方教程《Debug a plugin》13 中描述了多種可以進行調試的方式。用的比較多的還是日志調試方式,可以使用系統的 Console.app 查看日志,也可以使用 skpm log -f 插件日志。
文檔里說的大部分是插件的調試,WebView 內的前端代碼調試會更簡單一點。WebView 窗體右鍵審查元素即可使用 Safari 的開發者工具進行調試了。
注: 插件本身的代碼本質是客戶端代碼,WebView 本質是前端代碼,所以兩者的調試和日志輸出位置都是有區別的,這里要注意區分。
后記
以上就是開發 Sketch 的一些基礎知識和簡單流程,其它的就是多去看一下Sketch API14 文檔了。不過在實際的使用中 Sketch 的這套 JavaScript API 并不是非常完美,部分功能可能還暫時需要使用原生 API 區別。這時候可以多 Google 一下,能找到很多前人的實現,節省自己的工作量。
本文主要是介紹了一套 JavaScript API + WebView 的偏前端的開發方式,代碼我都已經放到 Github 上 https://github.com/lizheming/sketch-placeimg,大家可以自行查閱和下載。除了這種方式之外,我們也可以使用 OC + WebView 甚至是純 OC 客戶端的方式去開發插件。使用純客戶端開發的話性能會比 JavaScript API 的形式好一點,但是對于不了解 OC 開發的前端同學來說上手難度還是比較高的。
除了 Sketch 之外,Figma 也是一款非常棒的 UI 設計軟件。它基于 Web 開發,天生跨平臺,更提供了更加易用的協作模式,解決 UI 開發中的多人協作問題。感興趣的同學也可以去了解一下。
文內鏈接:
https://github.com/ccgus/CocoaScript
https://developer.sketch.com/plugins/javascript-api
https://developer.sketch.com/plugins/create-a-plugin
https://github.com/skpm/skpm#create-a-new-plugin
https://github.com/skpm/skpm/issues/266
https://developer.sketch.com/plugins/update-a-plugin
http://reactdesktop.js.org
https://github.com/skpm/skpm/blob/master/packages/builder/src/utils/resourceLoader.js#L7-L21
https://github.com/skpm/skpm/blob/master/packages/builder/src/utils/webpackConfig.js#L163-L169
https://github.com/skpm/sketch-module-web-view/blob/master/lib/inject-client-messaging.js
https://github.com/settings/tokens
https://github.com/sketchplugins/plugin-directory
https://developer.sketch.com/plugins/debugging
https://developer.sketch.com/reference/api
參考資料:
《Sketch插件開發總結》:https://zhuanlan.zhihu.com/p/28325423