Sketch 插件开发随手记

Sketch 是一套原生 Objective-C 开发的设计软件,它的作用和特点就不在此描述。由于最近简单研究并开发了几个 Sketch 插件,借本文来记录开发过程中的一些重要知识点。

结构

api layers

插件包的目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
my-plugin.sketchplugin/
├── Contents/ # 包含manifest(manifest.json)文件和所有的javascript(.js)/cocoascript(.cocoascript)文件
│ ├── Sketch/ # 子目录是插件的核心部分,包含了插件的各种功能,如命令、面板、库等
│ │ ├── Commands/
│ │ │ ├── My Command/
│ │ │ │ ├── manifest.json
│ │ │ │ └── My Command.sketchplugin
│ │ │ └── ...
│ │ ├── Libraries/
│ │ ├── Panels/
│ │ ├── Settings/
│ │ ├── Data/
│ │ └── Resources/
│ └── Resources/ # 包含插件所有的资源文件,包含了插件所需的各种文件,如图标、脚本、样式等。
│ ├── icon.png
│ ├── my-script.js
│ ├── my-style.css
│ ├── manifest.json
│ └── ...
├── README.md
└── LICENSE

开发

Manifest

manifest.json用来描述主要插件配置,具体字段说明如下:

  • appcast{String},该字段指向的 appcast 文件 URL 包含可用的版本信息,包括下载特定版本的 URL。Sketch 会自动检查该文件更新,如果有更新可用时会通知用户。
  • author{String},插件作者。
  • authorEmail{String},插件作者的 Email 地址,可选。
  • bundleVersion{Number},指定版本的插件包的元数据结构和文件布局。这是可选的,默认为1。目前没有其他版本的支持。
  • compatibleVersion{String},定义了 Sketch 运行插件所需的最低版本。这个字符串必须提供使用语义版本控制
  • commands{Object[]},定义插件提供的所有命令。其中对象字段如下:

    • identifier{String},定义了一个惟一的标识符内的命令插件包。
    • name{String},定义 Sketch 软件“插件”目录下的菜单名称。
    • shortcut{String},命令快捷键,如ctrl shift t。具体按键对应申明可见https://developer.sketch.com/plugins/plugin-manifest
    • script{String},指定执行命令对应脚本的路径文件。
    • handler{String},声明命令的指定函数名称。函数的声明应该在脚本作用域顶层,并接受一个context上下文参数包含信息,如当前文档和选择内容。如果省略该字段插件将默认使用一个名为onRun的处理程序。(如果使用skpm的话,可以通过export default导出函数而不用申明。)
    • handlers{Object},更精细的处理控制。定义setup › run › tearDown生命周期的控制。对象字段:
      • run{String},命令运行时的执行声明,同handler字段。
      • setUp{String},命令被调用前的执行声明(命名为 setUp 是为了避免与 StartUp 动作重名或混淆)。
      • tearDown{String},命令函数调用完成的执行声明(命名为 tearDown 是为了避免与 Shutdown 动作重名)。
      • onDocumentChanged{String},Sketch 设计稿 Document 变更时的执行声明。
      • actions{String},Sketch 应用事件,更多可见https://developer.sketch.com/plugins/actions
  • description{String},插件描述文案。

  • disableCocoaScriptPreprocessor{Boolean},CocaScript 支持开关,默认为false。CocoaScript 默认支持@import和中括号语法,如[obj hello: world]注意,如果使用 ES6 语法或者使用 skpm 等构建工具,该项必须选择true

  • homepage{String},官网。

  • icon{String},插件图标,128*128的 PNG 图片,并且在插件包的Contents/Resources目录下。

  • identifier{String},插件的唯一命名符,使用反向域名格式,如"com.example.sketch.plugin.select-shapes"

  • maxCompatibleVersion{String},最高兼容版本。

  • name{String},暴露给用户的插件名称。

  • scope{String},申明插件的作用域。"document"(仅在打开设计文档时插件菜单可见)或"applicaton"(打开Sketch时插件菜单就可见)

  • suppliesData{Boolean},申明是否为一个数据插件。如果设置为true则该插件是一个可视窗口,显示所有已安装的插件的列表。

  • version{String},版本号。

  • menu{Object},菜单栏控制。字段说明:

    • isRoot{Boolean},菜单是否在“插件”根目录下展示,默认在插件的二次菜单。注意子菜单不支持。

    • items{String[]},数组的菜单项,支持值命令标识符,"-"分隔符和对象定义子菜单。

    • title{String},菜单名称声明,当isRoottrue时被忽略。

demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{
"name": "Select Shapes",
"description": "Quickly select all or just specific shape types",
"author": "Bob Ross",
"homepage": "https://github.com/example/select-shapes",
"version": "1.0",
"identifier": "com.example.sketch.plugin.select-shapes",
"appcast": "https://example.com/select-shapes-plugin-appcast.xml",
"compatibleVersion": "52.1",
"commands": [
{
"name": "All",
"identifier": "all",
"shortcut": "ctrl shift a",
"script": "shared.js",
"handler": "selectAll"
},
{
"name": "Circles",
"identifier": "circles",
"script": "circles.js"
},
{
"name": "Selection changed",
"identifier": "selection-changed",
"script": "./selection-changed.js",
"handlers": {
"actions": {
"SelectionChanged.finish": "onSelectionChanged"
}
}
}
],
"menu": {
"items": ["all", "circles", "rectangles"]
}
}

开发模式

笼统来说,Sketch Plugin 作用到 Sketch.app 本身,有两种主要方式:

它之所以能支持使用 JS 开发,是因为它使用 CocoaScript 作为插件的开发语言。它就像是一座桥(Bridge),类似于 js-bridge,能让我们在插件中写 OC 和 JS,然后 Sketch 将基础方法进行了封装,实现了一套 JavaScript API,这样我们就能使用 JS 开发 Sketch 插件了。

使用 JS API 的最大弊端是 API 的功能覆盖太少(截止到 Sketch 49 版本前),而我们期望实现的功能大都没有对应的接口来完成。因此在开发时,我们选择使用 CocoaScript + Objective-C Framework 的组合方式,这样的好处是能够平衡开发的敏捷性与插件的私密性

之前对于 CocoaScript 的了解并不多,但如官方介绍的那样作用是作为通信桥梁:CocoaScript is a bridge providing access to the internal Sketch APIs and macOS frameworks in JavaScript.

JavaScript 封装包

Sketch 根据作用领域对 JavaScript API 做了封装库:

  • sketch/ui:界面相关,包括 toast、alert 等;
  • sketch/dom:访问、修改和创建文档,从颜色到层和插件;
  • sketch/settings:保存图层/文档,或者插件的用户设置(类似于 localStorage);
  • sketch/data-supplier:提供图片或文字数据,直接输出设计相关的用户界面;
  • sketch/async:默认插件命令的 JavaScript 上下文会在成功执行后销毁。通过这个包的异步操作方法可以延长作用生命周期。

选型

像我这样对于 OC 语法不熟悉的 web 开发,目前 Sketch 开发有两大开发方案:

  • 1.React 组件渲染成 sketch:react-sketchapp ,由 airbnb 团队发起
  • 2.使用 skpm 构建开发 Sketch 插件。

本人当前主要使用 skpm,所以就主要记录 skpm 的使用功能点。

skpm

安装

1
npm i -g skpm

创建项目

1
2
3
4
5
6
7
8
skpm create <plugin-name>

--name The plugin display name.
--cwd A directory to use instead of $PWD.
--force Force option to create the directory for the new app. [boolean] [default: false]
--template The repository hosting the template to start from. [string] [default: skpm/skpm]
--git Initialize version control using git. [boolean] [default: true]
--install Installs dependencies. [boolean] [default: true]

当前主要的模版:

1
skpm create testPlugin --template=skpm/with-webview

构建:

1
npm run build

如果观察数据:

1
npm run watch

如果你想每次构建时运行:

1
npm run start

注意:尽可能不要使用npm start 进行开发,它携带的 --run 命令会使得构建速度特别慢。虽然它带 Live Reload 功能会很方便,但在官方未修复该问题前还是不建议大家使用。

skpm/with-webview 使用记录

由于目前开发的插件主要是 Webview 界面的形式,那么先记录一下开发工程中遇到的坑与解决方案。

1.文档地址

2.webview 页面与插件运行环境的通信

Sketch 显示型插件肯定需要调用或者触发 sketch plugin\/sketch api 功能,那么首先就涉及到 webview 通知插件运行环境,姑且称 webview 环境为 frontend,插件运行环境为 backend。

fontend -> backend:window.postMessage()

webview 自带window.postMessage方法,与 backend 的通信方式就是通过此方法。

首先 backend 注册事件监听,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Content/script.js
*/
import BrowserWindow from 'sketch-module-web-view';
import UI from 'sketch/ui'; // sketch UI插件库

const browserWindow = new BrowserWindow();
const webContents = browserWindow.webContents;

webContents.on('nativeLog', message => {
// 注册nativeLog事件,当frontend通知触发nativeLog消息时触发
UI.message(message);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Resources/webview.js
*/

window.onload = function () {
window.postMessage('nativeLog', 'page is onloaded.'); // 调用通知nativeLog
};

document.querySelector('#btn').addEventListener(
'click',
function () {
window.postMessage('nativeLog', 'btn is clicked.'); // 调用通知nativeLog
},
false
);

注意:webContents 本身存在一些如生命周期的默认事件,要避免与这些事件命名冲突。关于 webContents 及默认事件见后文。

backend -> frontend:browserWindow.webContents .executeJavaScript

通过 browserWindow 实例中 webContents 上下文的 executeJavaScript 方法,可以调用 webview 中的全局方法,调用形式类似于eval()browserWindow.webContents .executeJavaScript()方法返回一个 Promise 实例,以获取和处理调用的响应结果。如:

1
2
3
4
5
6
7
/**
* Resources/webview.js
*/
window.webviewLog = message => {
console.log(message);
return true;
};
1
2
3
4
5
6
7
8
9
10
11
12
/**
* Content/script.js
*/

import BrowserWindow from 'sketch-module-web-view';

const browserWindow = new BrowserWindow();
const webContents = browserWindow.webContents;

webContents.executeJavaScript('webviewLog("hello")').then(res => {
// do something with the result
});

小技巧:如果你不想装 sketch-dev-tools,但想看到 console.log 的信息,可以通过Safari > Develop > “你的电脑” > “你的插件”

BrowserWindow

构造函数,用于创建和控制浏览器窗口。

js 与 OC(Objective-C)

OC 信息

SKetch 运行的插件环境可以获取很多 OC 结构体信息,这些结构体提供设备信息等原生开发才有的应用权限。比如 NSScreen 提供屏幕信息。

OC 属性代表:NSScreen

文档:http://www.gnustep.org/resources/OpenStepSpec/ApplicationKit/Classes/NSScreen.html

如:

1
console.log(NSScreen.mainScreen().frame());

返回画布及坐标信息:

p-2.png

OC 方法代表:NSPasteboard

如清理剪贴板:

1
NSPasteboard.generalPasteboard().clearContents();

添加剪贴板:

1
NSPasteboard.generalPasteboard().writeObjects();

Sketch 环境下 OC 方法与 JS 方法的语法关系

OC 的方括号语法在 js 中转换为点语法。具体来说的话,CocoaScript 创建了一个对 OC 的 js 代理对象。

  • OC 在 js 中的引用方法:

    • Getter: object.name()
    • Setter: object.name = 'Sketch'
    • 方法调用: object.setName()
  • OC 方法选择器转换为了 js 方法:

    • :(参数与函数名分割符) 转换为 _,最后一个下划线可省略;
    • 下划线从一杠变成两杠 sketch_method -> sketch__method
    • 选择器的每个组件都连接成一个没有分隔的字符串。

如 OC 的对象操作方法

1
[executeOperation:withObject:error:]

在 js 中转变为

1
executeOperation_withObject_error();

实现插件的自动更新

Sketch 会定期检查 appcast 更新,并提示用户安装新的插件版本。

如何在使用 skpm 的情况下,实现闭源且用 github 提供下载更新的地址?

  • 1.准备两个仓库。比如 xxx 和 xxx-release,xxx 存放源码,xxx-release 发布更新。

  • 2.在 package.json 文件中加入 repository 字段,指向 xxx-release。

1
"repository": "https://github.com/xxx/xxx-release",
  • 3.使用 skpm 发布新版本

  • 4.在 xxx-release 仓库更新.appcast.xml 文件,加入新的地址。


相关链接