如何制做 Sketch 插件

Sketch 是近些年比较流行的 UI 设计软件,它比起以前经常使用的 Illustrator 或者 Photoshop 比较好的地方在于小巧功能简单但足够,同时对 Mac 的触摸板支持更加友好。另外它的插件系统也要比 Adobe 更加友好,大量的插件帮助咱们解决协同和效率上的问题。javascript

Sketch 插件最大的好处在于能够直接使用 JavaScript 进行开发,并提供了许多配套的开发工具。下面我就以帮助设计师同窗快速插入占位图的插件 Placeholder 为例,带你们一步一步的了解如何进行 Sketch 插件开发。html

在进行插件开发以前,咱们须要了解一些基础的知识。Sketch 是一套原生 Objective-C 开发的软件,它之因此能支持使用 JS 开发,是由于它使用 CocoaScript 做为插件的开发语言。它就像是一座桥(Bridge),能让咱们在插件中写 OC 和 JS,而后 Sketch 将基础方法进行了封装,实现了一套 JavaScript API,这样咱们就能使用 JS 开发 Sketch 插件了。前端

注: 关于如何开发插件,官方提供了一份入门教程 《Create a plugin》,在阅读下文以前,也能够花 2~3min 先看看这篇官方教程,内容比较简短。

需求整理

在进行插件开发以前,咱们捋一捋咱们须要实现的功能。http://placeimg.com/ 是一个专门用来生成占位图的网站,咱们将利用该网站提供的服务制做一个生成指定大小的占位图并插入到 Sketch 画板中的功能。插件会提供一个面板,可让使用者输入尺寸、分类等可选项,同时提供插入按钮,点击后会在画板插入一张图片图层。java

使用 skpm 初始化项目

skpm 是 Sketch 官方提供的插件管理工具,类比于 Node.js 中的 npm。它集插件的建立、开发、构建、发布等多项功能于一体,咱们在不少场景都须要使用它。安装的话比较简单,直接使用 npm 全局安装便可。node

npm install -g skpm

按照官方教程,安装完毕以后咱们就可使用 skpm create 命令来初始化项目目录了。固然 skpm 是支持基于模板初始化的,官方仓库也列举了一些模板,咱们可使用 --temlate 来指定模板进行初始化。不过处于教学的目的,我这里就仍是使用官方默认的模板建立了。react

➜  ~ 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 的话对监听文件变化实时编译,在开发中很是有帮助。webpack

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

项目结构入门

建立好的模板目录结构以下,为了帮助你们理解,咱们来简单的介绍下这些目录和文件。git

.
├── 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 进行配置,默认的值以下。github

{
  ...
  "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 目录下。web

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插件开发总结》