如何制作 Sketch 插件

公子   10月11, 2020

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/sketch-placeimg,大家可以自行查阅和下载。除了这种方式之外,我们也可以使用 OC + WebView 甚至是纯 OC 客户端的方式去开发插件。使用纯客户端开发的话性能会比 JavaScript API 的形式好一点,但是对于不了解 OC 开发的前端同学来说上手难度还是比较高的。

除了 Sketch 之外,Figma 也是一款非常棒的 UI 设计软件。它基于 Web 开发,天生跨平台,更提供了更加易用的协作模式,解决 UI 开发中的多人协作问题。感兴趣的同学也可以去了解一下。

参考资料:

  1. 《Sketch插件开发总结》