浅谈Sketch插件开发

2,073 阅读11分钟

在这里插入图片描述

sketch是什么?

sketch是一款用来制作矢量绘图的软件,矢量绘图也是进行网页,图标以及界面设计的最好方式。但除了矢量编辑的功能之外,sketch同样添加了一些基本的位图工具,比如模糊和色彩校正。是Mac系统才有的软件,可以理解为精简版的PS ,比PS更适合UI设计。该软件的特点是容易理解,上手简单,对于有设计经验的设计师来说,入门门槛很低。

sketch是用Objective-C构建的,是一套原生Objective-C开发的软件。Objective-C类通过Bridge (CocoaScript/mocha) 提供Javascript API调用。

sketch基本结构

sketch只有文字和图形的概念。MSTextLayer文字控件,MSShapePathLayer图形控件。

sketch插件

介绍

  1. sketch插件是按照特定方式管理的一个文件夹,是一个或多个scripts的集合,每个script定义一个或多个commands。sketch插件是以 .sketchplugin 扩展名的文件夹,包含文件和子文件夹。

  2. sketch插件主要使用Javascript 语言编写,支持ES6语法,但运行环境既不是浏览器也不是Nodejs,而是Hybrid SketchAPI for macOS Native运行环境。

  3. 用JavaScript 编写一个sketch插件。利用ES6,访问macOS框架并使用sketch API,无需学习Objective-C或Swift。所有macOS 框架和内部sketch API都由CocoaScript提供给 JavaScript。

可以做什么?

  • 根据复杂规则选择文档中的图层
  • 操纵图层属性
  • 创建新图层
  • 以所有支持的格式导出资源
  • 与用户交互(询问输入,显示输出)
  • 从外部文件和Web服务获取数据
  • 与剪贴板交互
  • 操纵sketch的环境(编辑指南,缩放等…)
  • 通过调用插件中的菜单选项自动执行现有功能
  • 设计规范
  • 内容生成
  • 透视变化

设计师常用的sketch插件

Fusion Cool

蓝湖

Kitchen(语雀)

Dapollo

中文版sketch插件库

功能完整的sketch插件包括哪些部分

一个功能完善成熟的sketch插件包括三个部分:

  • 工具栏
  • webview容器
  • 业务数据

通过下面这张图看下:

在这里插入图片描述

工具栏

开发工具栏主要使用NSStackView、NSButton、NSImage以及NSFont这几个类,可以类比iOS开发中以UI作为前缀的控件类,NS前缀主要是AppKit以及Foundation的相关类,MS前缀则是skecth的相关类,CA、CF前缀为核心动画库和核心基础类。

webview容器

sketch插件使用webview创建复杂的UI。不用于一般的插件页面,可以使用webview模块加载一个复杂的Web应用,使其与sketch进行交互。通过webview的Bridge桥接传递用户操作到插件侧代码,之后调用sketch API对图层进行操作。

快速创建webview容器有两种:

  • 通过CocoaScript创建原生NSPanel

    // 原生方式加入webview
    const panel = NSPanel.alloc().init();
    panel.setFrame_display(NSMakeRect(0, 0, panelWidth, panelHeight), true);
    const wkwebviewConfig = WKWebViewConfiguration.alloc().init()
    const webView = WKWebView.alloc().initWithFrame_configuration(
      CGRectMake(0, 0, panelWidth, panelWidth),
      wkwebviewConfig
    )
    panel.contentView().addSubview(webView);
    webview.loadFileURL_allowingReadAccessToURL(
      NSURL.URLWithString(url),
      NSURL.URLWithString('file:///')
    )
    
  • 官方的sketch-module-web-view快速创建WebView容器,它提供了丰富的API对窗口的展示样式和行为进行定制,包括Frameless Window、Drag等,同时还封装了WebView与插件层的通信的Bridge,可以轻松在"frontend" (the WebView)和"backend" (the plugin running in Sketch)之间发送消息。

    // 使用官方的BrowserWindow
    import BrowserWindow from "sketch-module-web-view";
    const browserWindow = new BrowserWindow(options);
    const webViewContents = browserWindow.webContents;
    webViewContents
    .executeJavaScript(`someGlobalFunctionDefinedInTheWebview(${JSON.stringify(someObject)})`)
    .then(res => {
    // do something
    })
     browserWindow.loadURL(require('./index.html'))
    

这里建议通过官方的方式创建webview,后面会对webview做具体说明,此处不做过多阐述。

业务数据

结合具体业务通过接口获取数据。

插件开发技术方案

sketch插件开发大概有如下三种方式:

  1. 纯使用CocoaScript脚本进行开发
  2. 通过Javascript + CocoaScript的混合开发模式
  3. 通过AppKit + Objective-C进行开发

我们看一张图:

官方推荐方案对于前端会更友好,原生开发方案插件性能会更好,两种都不是很简单,有一定的学习成本,前端同学开发的话建议使用官方推荐方案。即混合开发模式进行sketch插件开发,具体流程参考下图:

在这里插入图片描述

插件技术分析

sketch插件系统可以完全访问应用程序的内部结构和macOS中的核心框架。

sketch官方针对sketch Native API封装了一套JS API,目前还未涵盖所有场景,比如UI界面、组件拖放等,若需要更丰富的底层 API需结合CocoaScript进行实现。

sketch插件为什么支持使用JS开发?

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

Objective-C是什么?

在iOS的开发中使用的是Objective C语言,它是一种通用、高级、面向对象的语言,是C语言的严格超集,广泛用于IOS开发。Objective-C,通常写作ObjC或OC和较少用的Objective C或Obj-C,是扩充C的面向对象编程语言。具体可参考官方文档。

CocoaScript是什么?

是一种bridge,实现JavaScript运行环境到Objective-C运行时的桥接功能,可通过桥接器编写JavaScript外部脚本访问内部sketch API和macOS 框架(AppKit)底层丰富的API功能。可以从CocoaScript访问所有Cocoa和sketch API。

Mocha 实现提供JavaScript运行环境到Objective-C运行时的桥接功能已包含在CocoaScript中。

CocoaScript建立在Apple的JavaScriptCore之上,而JavaScriptCore是为Safari提供支持的JavaScript引擎,使用CocoaScript编写代码实际上就是在编写JavaScript。CocoaScript包括桥接器,可以从 JavaScript访问Apple的Cocoa框架。

CocoaScript中的Mocha实现JS到Objective-C的Bridge,虽然Mocha包含在CocoaScript中,但文档仍保留在原始Github中。因此,在CocoaScript的Readme中看不到任何语法教程。这里一个诀窍是,如果你想了解Mocha将原生的Sketch Objects通过bridge,从Objective-C传递到JavaScript层的属性、类或者实例方法的信息,可以将其打印出来。

CocoaScript github地址

CocoaScript官方文档

Mocha

语法

借助CocoaScript使用JavaScript调Objective-C语法

  • 方法调用用 '.'语法
  • Objective-C 属性设置:Getter: object.name();Setter: object.setName('Sketch'),object.name='sketch'
  • 参数都放在 ‘()’里
  • Objective-C 中 ' : '(参数与函数名分割符) 转换为' _ ',最后一个下划线是可选的
  • 返回值,JavaScript 统一用var/const/let设置类型
// Objective-C
[executeOperation:withObject:error:]

// CocoaScript
executeOperation_withObject_error()

AppKit是什么?

构建sketch的一个主要Apple框架,是为macOS应用程序构建和管理事件驱动的图形用户界面的。

技术实现

通过UI层和逻辑层两部分实现。

  • UI层:可以通过webview内嵌实现,可以使用各种前端开发框架,比如React或者Vue等。UI层将用户的操作反馈传递给逻辑层,使其调用sketch API更新Layers。
  • 逻辑层:负责调用sketch API,显然不在WebView中,因此需要通过CocoaScript Bridge进行通信,逻辑层将从服务器获取到的数据传递给UI层展示。

具体sketch通信原理可参考下图:

在这里插入图片描述

技术难点

  • sketch更新速度很快,官方文档简单陈旧,底层API功能薄弱,更深入的话需要了解掌握Objective-C、CocoaScript、Appkit、Sketch-Headers
  • 官方提供的API能实现的功能有限,需要结合Appkit、Objective-C IOS开发技术以及桌面开发相关技术,开发技术栈混乱
  • 成熟项目一般还未开源,而开源的项目基本上没有特别大的参考价值,需要深入文档结合具体业务项目学习。

sketch插件开发

插件术语

  • Plugin(插件): 一组 Scripts、Commands和其他资源组合在一起作为一个独立单元

  • Plugin Bundle(插件包): 磁盘上的文件夹,其中包含组成 Plugin的文件

  • Action(行为): 用户所做的事情(选择菜单或更改文档)触发Command

  • Command(命令): 一个插件可以定义多个命令; 通常每一个都与不同的菜单或键盘快捷键相关联,并导致执行不同的Handler程序

  • Handler(操作): 执行一些代码来实现Command的函数

  • Script(脚本): 一个 JavaScript文件, 包含一个或多个用来实现一个或多个Commands的Handlers

插件位置

sketch中插件的位置如下图所示:

安装的sketch插件位于下面目录:

/Users/用户名/Library/Application Support/com.bohemiancoding.sketch3/Plugins

我们可以通过sketch内置脚本编辑器编写一个简单的脚本,脚本编辑器提供对 JavaScript API 和内部 API 的完整访问。

开发环境

skpm

skpm(Sketch Plugin Manager)是Sketch提供的用于Plugin创建、Build以及发布的官方工具是一个打包工具,用于快速上手插件开发。

官方提供的打包工具skpm, 它集插件的创建、开发、构建、发布等多项功能于一体,用于快速上手sketch插件开发。它基于webpack,项目根目录下存放webpack.skpm.config.js, 用于工程配置修改。skpm默认采用Webpack作为打包工具。终端运行npm run build会生成xxx.sketchplugin目录,该目录就是最终的插件目录。双击该目录,或者将该目录拖拽到Sketch界面上就成功安装插件了。运行 npm run watch对监听文件变化实时编译,在开发中非常有帮助。

注: 不要使用npm start进行开发,它携带的 --run 命令会使得构建速度特别慢。在官方未修复该问题前还是不建议大家使用。

创建插件模版

1. cnpm install -g skpm // 全局安装skpm脚手架
2. skpm --help // 查看使用说明以查看所有可用的命令
3. skpm create my-plugin
4. npm run build/yarn build

插件结构

  • sketch插件是按照特定方式管理的一个文件夹,包含一个或多个scripts,每个script含有若干扩展sketch用途的命令。
  • 插件主要使用Javascript编写,支持ES6语法,但运行环境既不是浏览器也不是Nodejs,而是Hybrid SketchAPI for macOS Native运行环境。

插件目录

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

// resources文件夹
其中还有一个resources,用于存放资源文件,包括css、js、html.

在这里插入图片描述

目录说明:

文件夹描述
*.sketchpluginskpm生成过程生成skpm插件捆绑包。不要编辑此文件夹中的任何文件,任何更改都将用下一个版本覆盖
assets与插件捆绑的任何资源文件的文件夹。要使用不同的路径或添加文件夹,请修改package.json skpm.assets设置
src编写程序目录
resources用于存放资源文件,包括css、js、html.
.appcast.xmlmanifest.json有两个比较重要的字段,就是versionappcastversion就是用来表示当前插件的版本的。而appcast它的值是一个XML的URL地址,该XML里面包含了该插件所有的版本以及该版本对应的下载地址。sketch会将version对应的版本和appcast对应的XML进行对比,如果发现有新的版本了,会使用该版本对应的下载地址下载插件,执行在线更新插件。通过skpm publish命令去发布插件的话,会自动在根目录生成一个.appcast.xml文件
mainifest.json

该文件提供有关插件的信息,例如作者,描述,图标、从何处获取最新更新、定义的命令commands、调用菜单menu项等等。

{
  "name": "Mock",
  "description": "Mock some content",
  "author": "QCM",
  "$schema": "https://raw.githubusercontent.com/sketch-hq/SketchAPI/develop/docs/sketch-plugin-manifest-schema.json",
  "icon": "icon.png", // 插件图标
  "commands": [ // 可以有多个执行命令
    {
      "name": "Name", // sketch插件命令名称,显示在Sketch Plugin菜单中
      "identifier": "my-plugin.my-command-identifier", // 唯一标识,建议用 com.xxxx.xxx 格式,不要过长
      "script": "./name.js" // 插件执行脚本,实现命令功能的函数所在的脚本
      "shortcut": "", // 命令的快捷键
      "handler": "", // 函数名,该函数实现命令的功能。Sketch 在调用该函数时,会传入 context 上下文参数。若未指定 handler,Sketch 会默认调用对应 script 中 onRun 函数
    }
  ],
  "menu": {
    "title": "Name", // sketch插件名称
    "items": [
      "my-plugin.my-command-identifier"
    ]
  }
}
commands

表示命令具体的执行操作

  • script:实现具体命令功能的函数所在的文件
  • handler : 函数名,该函数实现命令的功能。sketch在调用该函数时,会传入context上下文参数。若未指定handler,sketch会默认调用对应script中onRun函数
  • shortcut:命令的快捷键
  • name:显示在sketch插件菜单中
  • identifier : 唯一标识,一般建议用com.xxxx.xxx格式,不要过长
总结
  • 需要参与webpack打包的脚本文件必须在resources目录下声明,否则不会参与编译
  • assets目录需要配置在skpm.assets下,是资源文件夹,如需更改需在package.json中的skpm.assets中设置
  • 常用的命令可以定义在scripts中方便直接调用
  • src下是需要被webpack打包的脚本文件以及manifest清单文件
  • my-plugin.sketchplugin是skpm构建过程生成的插件包
  • resource配置中配置的文件会走webpack的编译打包,并输出到xxx.sketchplugiin/Contents/Resources 目录中
  • 点击插件菜单后什么都没有发生,此时还需要更改一下配置,即需要安装html-loader@skpm/extract-loader两款Loader,前者是用来解析处理html代码中可能存在的资源关联情况,后者是将html文件拷贝到xxx.sketchplugin/Contents/Resources目录并返回对应的file:///格式的文件路径URL,用来在插件中进行关联。然后在webpack.skpm.config.js中进行配置

插件开发前设置

崩溃保护
// 当 Sketch 运行发生崩溃,它会停用所有插件以避免循环崩溃。对于使用者,每次崩溃重启后手动在菜单栏启用所需插件非常繁琐。因此可以通过如下命令禁用该特性
defaults write com.bohemiancoding.sketch3 disableAutomaticSafeMode true
插件缓存
// 通过配置启用或禁用缓存机制:
defaults write com.bohemiancoding.sketch3 AlwaysReloadScript -bool YES
webview调试
// 如果插件实现方案使用 WebView 做界面,可通过以下配置开启调试功能
defaults write com.bohemiancoding.sketch3 WebKitDeveloperExtras -bool YES

编写一个简单的插件

实现一个demo,将选中图层文本修改为随机数

import sketch from 'sketch'
import Mock from 'mockjs'
const Random = Mock.Random

// 将选中图层文本修改为随机数
export default function(context) {
  const doc = sketch.getSelectedDocument(); // 整个文件就是一个document
  const layers = doc.selectedLayers.layers; // 获取图层
  console.log(layers, layers.length, 111);
  layers.forEach(layer => {
    const randName = Random.cname();
    // if(layer.type === "ShapePath"){ // 图形
    //   layer.style.fills[0].color = Random.hex();  // 随机修改选中图层颜色
    // }
    // layer.text = (Math.round(Math.random()*100)).toString(); // 将选中图层文本修改为随机数
    layer.text = randName; // 将选中图层文本修改为随机姓名
  });
}

看下运行结果:

在这里插入图片描述

插件调试

Sketch DevTools,是包含在Sketch中的CLI工具。

sketch-dev-tools

发布插件

通过skpm发布插件,如果插件托管在GitHub 上skpm允许管理发布,并从命令行自动提交到插件列表,实现以下步骤:

// 在插件文件夹中运行skpm
skpm publish

// 查看使用说明
skpm publish --help

// 如果尚未使用gitHub存储库skpm则需要首先使用个人访问令牌从命令行登录,从而授予对存储库范围的访问权限
skpm login

// 更新插件
skpm publish <version>

sketch的主要对象

所有的关于sketch对象的操作,都是通过context来的。context的document对象,oc对应的是MSDocument对象 。可以通过命令打印出来查看

// commands.js
export function importIcons(context) {
  log(context, 'context')
}

在这里插入图片描述

context上下文包括6个部分,分别是:

  1. command: MSPluginCommand对象,当前执行命令
  2. document: MSDocument对象 ,当前文档
  3. plugin: MSPluginBundle对象,当前的插件bundle,包含当前运行的脚本
  4. scriptPath: NSString 当前执行脚本的绝对路径
  5. scriptURL: 当前执行脚本的绝对路径,跟scriptPath不同的是它是个 NSURL 对象
  6. selection: 一个 NSArray 对象,包含了当前选择的所有图层。数组中的每一个元素都是MSLayer对象

webview

sketch插件使用webview创建复杂的UI。不用于一般的插件页面,可以使用webview模块加载一个复杂的Web应用,使其与Sketch进行交互,想要在插件中加载网页,需要安装sketch封装好的sketch-module-web-view插件。

sketch-module-web-view

// 安装
npm install sketch-module-web-view --save-dev

BrowserWindow

创建和控制浏览器窗口

// In the plugin.
const BrowserWindow = require('sketch-module-web-view')

let win = new BrowserWindow({ width: 800, height: 600 })
win.on('closed', () => {
  win = null
})

// Load a remote URL
win.loadURL('https://github.com')

// Or load a local HTML file
win.loadURL(require('./index.html'))

BrowserWindow文档

webContents

渲染和控制网页。webContents是一个事件。它负责渲染和控制网页,是BrowserWindow对象的属性。访问webContents对象的示例:

const BrowserWindow = require('sketch-module-web-view')

let win = new BrowserWindow({ width: 800, height: 1500 })
win.loadURL('http://github.com')

let contents = win.webContents
console.log(contents)

webContents文档

sketch和webview通信

创建UI时,您可能需要在"前端"(Webview)和"后端"(Sketch 中运行的插件)之间进行通信。

从sketch插件命令向webView发送消息

如果要在sketch中发生某些更改(例如,当选择发生变化)中更新UI,则需要向WebView发送消息。

// webview中
window.someGlobalFunctionDefinedInTheWebview = function (arg) {
  console.log(arg)
}

// 插件中
browserWindow.webContents
  .executeJavaScript('someGlobalFunctionDefinedInTheWebview("hello")')
  .then((res) => {
    // do something with the result
  })
从webView向sketch插件发送消息

当用户与webView交互时,您可能需要与插件进行通信。您可以通过收听webView将发送的事件来做到这一点

// 插件上
var sketch = require('sketch')

browserWindow.webContents.on('nativeLog', function (s) {
  sketch.UI.message(s)
  return 'result'
})

// webview中
window.postMessage('nativeLog', 'Called from the webview')

// 传递单个参数
window.postMessage('nativeLog', {
  a: b,
})

// 传递多个参数
window.postMessage('nativeLog', 1, 2, 3)

// `window.postMessage` returns a Promis with the array of results from plugin listeners
window.postMessage('nativeLog', 'blabla').then((res) => {
  // res === ['result']
})

如果想查看console.log来自webview内部的消息,则可以看到Safari > Develop > {{Your Computer}} > {{Your Plugin}},Safari 浏览器开发工具可用于插件的Javascript代码调试,Developer > name of your machine > Automatically Show Web Inspector for JSContexts,同时启用选项 Automatically Pause Connecting to JSContext。

sketch和webview通信文档

webivew demo

// sketch插件中代码
import BrowserWindow from 'sketch-module-web-view'

/** 生成webview **/
let win = new BrowserWindow({ width: 800, height: 600 });
win.on('closed', () => {
    win = null
});

// Load a localhost URL
const Panel = `http://localhost:8080`;
win.loadURL(Panel);

/** 监听webview的事件:webview->plugin **/
var contents = win.webContents;

//监听webview的事件:webview->plugin
contents.on('fromwebview', function(s) {
    console.log(s, 'fromwebview');
});

/** 主动执行webview代码,发送数据 **/
var data = {
    name: "json",
    type: "come from plugin object data!"
};
//执行webview的代码
const getData = () => {
	contents
		.executeJavaScript(`someGlobalFunctionDefinedInTheWebview(${JSON.stringify(data)})`)
		.then(res => {
			console.log(res, "send data success,from plugin to webview!")
		})
};

//// 从webView向sketch插件发送消息,插件中
//主动执行webview代码,发送数据
contents.on("did-start-loading", () => getData());

// webview,使用vue框架
mounted(){
    window.someGlobalFunctionDefinedInTheWebview = function (arg) {
      console.log(arg, 'arg------')
    }

    // webview中
    window.postMessage('fromwebview', {
      a: 'webview fromwebview---',
    })
}

打印结果如下:

在这里插入图片描述

官方API

Action API

一种让插件对应用程序中的事件做出反应的方法。使用它,插件作者可以编写在触发某些操作时执行的代码,例如“打开文档”、“保存”、“添加画板”……

该API用于监听用户操作行为而触发事件,是应用程序中发生的事件,通常是用户交互的结果,操作的名称类似于OpenDocumen(打开文档)、CloseDocument(关闭文档)、Shutdown(关闭插件)、TextChanged(文本变化)、TogglePresentationMode等。您可以告诉您的插件在触发这些操作时运行一些代码,该API未来会被新的 Events API 替代。它代表了 sketch App 内部触发的事件。

如何注册我的插件来“监听”一个动作?

简单:您只需在manifest.json插件已有的文件中添加一个处理程序,我们将添加一个新的处理程序,用于OpenDocument操作

"commands" : [
  ...
+  {
+    "script" : "my-action-listener.js",
+    "name" : "My Action Listener",
+    "handlers" : {
+      "actions": {
+        "OpenDocument": "onOpenDocument"
+      }
+    },
+    "identifier" : "my-action-listener-identifier"
+  }
  ...
],

告诉我们的插件我们希望onOpenDocument在文档打开时运行该函数,所以让我们将其添加到my-action-listener.js

export function onOpenDocument(context) {
  context.actionContext.document.showMessage('Document Opened')
}

保存所有内容,构建插件,现在每当您在sketch中打开文档时,您都应该看到一个小的Toast 横幅,上面写着“文档已打开”。

文档

Javascript API

是针对Native API的封装,目前还未涵盖所有场景,官方承诺未来将覆盖90%,若需要更丰富的底层API需结合CocoaScript进行实现,JavaScript API涵盖不同的领域并包含不同的包,通过Javascript API可以很方便的对sketch 中 DocumentArtboardGroupLayer 进行相关操作以及导入导出等,不过可能需要考虑兼容性。

  • 文档对象模型sketch/dom:访问、修改和创建文档——从颜色到图层和符号的所有内容
  • 设置和状态保存sketch/settings:保存图层或文档的自定义数据并存储插件的用户设置
  • 用户界面sketch/ui:无需构建即可显示通知并获取用户输入
  • 数据供应商sketch/data-supplier:直接在sketch中提供图像或文本数据。数据供应商直接与sketch用户界面集成,使内容在整个设计过程中随时可用
  • sketch/async:默认情况下,插件命令的JavaScript上下文在成功执行后会被销毁。对于异步操作,JavaScript API提供了异步作为延长JavaScript上下文生命周期的方法
  • export/import从磁盘导入和导出图层将文件作为图层导入并将对象导出为支持的文件格式

文档

sketch中node模块

  • child_process
  • console
  • events
  • fetch
  • fs
  • os
  • process
  • querystring
  • stream
  • String_decoder
  • timers
  • util

插件开发流程总结

  • 首先利用JavaScript或CocoaScript开发操作面板
  • 使用NPM或yarn安装所需依赖
  • 通过CocoaScript Bridge传递用户操作到插件逻辑侧,通过调用skecth API对文档进行处理
  • 使用webpack进行打包
  • 通过测试后发布插件更新

具体可以参考下面这张图:

在这里插入图片描述

插件开发参考文档

sketch插件开发官方文档

sketch插件开发中文文档

cocoascript

cocoascript中文文档

Mocha

Javascript API

action api

webview和sketch插件通信

sketch-module-web-view

美团积木sketch plugin