标签plugin下的文章

lizheming 发布于 06月18, 2022

如何制作 Figma 插件

Figma 是一款专业的在线 UI 设计工具,它因为个人使用免费、在线跨平台、多人协作、蓬勃的 Figma Community 社区组织而广受欢迎。目前设计团队都在使用 Figma 进行 UI 设计交付。

能被 SaaS 化的终将被 SaaS 化

Figma 本身是 Web 服务,其客户端也是使用 Electron 进行的封装。所以它的插件系统是前端友好型,和日常前端开发没有什么太大的区别。

插件原理

Figma 的插件也采用了双线程的架构。UI 线程能获得完整 Web 的能力,但是无法直接操作 Figma;主线程则相反,可以通过 Figma API 对数据进行操作,但无完整的 Web 能力,仅有 JS 执行以及 Figma API 支持。两者通过 postMessage 进行通信。

根据官方文章描述,通过 WebAssembly 版的 QuickJS 来实现主线程的沙箱执行,UI 线程则是通过 iframe 执行。

采用这种方案的原因主要是既想保证代码隔离,但是又希望能方便的操作 Figma 数据。

根据官博文章描述,之前也曾有尝试使用 Web 原生的沙盒 API Realms 来实现主线程的沙盒执行,但因为该 API 的一些安全漏洞还是回退回了 QuickJS 的实现。

了解了插件的原理之后,下面我就以帮助设计师同学快速插入占位图的插件 Placeholder 为例,带大家一步一步的了解如何进行 Figma 插件开发。

需求整理

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

项目结构

在 Figma 客户端中按照如上操作即可完成插件的初始化。除了默认的三个例子之外,官方也有一个示例插件的仓库,也可以参考。

https://github.com/figma/plugin-samples

Figma 插件默认推荐使用 TypeScript 开发,官方提供了完善的 TypeScript 类型支持。以默认的带 UI 的模板为例,初始化后进入文件夹 npm i 安装依赖后执行 npm run build 编译完成后点击插件即可看到效果。

.
├── README.md
├── code.js
├── code.ts
├── manifest.json
├── package-lock.json
├── package.json
├── tsconfig.json
└── ui.html

manifest.json

可以看到整体的接口和大多数 JS 项目一样,其中 manifest.json 用来记录插件的信息。manifest.json 这个文件大家可以理解为是 Figma 插件的 package.json 文件。我们来看看默认生成的 manifest.json。

{
  "name": "figma-placeimg",
  "id": "1117699210834344763",
  "api": "1.0.0",
  "main": "code.js",
  "editorType": [
    "figma"
  ],
  "ui": "ui.html"
}

其中重点的是 main 和 ui 两个字段:

  • main:指定插件的入口文件,该文件中的代码会运行在主进程中的沙箱里。
  • ui: 指定插件的 UI 代码文件,该文件中的代码会运行在 iframe 中。实际上,UI 代码文件的内容会作为字符串传递给 figma 内置变量 __html__,在沙箱内可以通过 figma.showUI(__html__) 创建 iframe。

这里注意到是将UI代码文件中的内容作为字符串注入到主线程中,类似 <iframe srcdoc="__html__" />。这就导致了我们无法直接引用插件中的其他资源,所有插件内依赖的资源都需要内嵌到最终的字符串中。

ui 字段也支持指定多个文件,当指定多个文件的时候会注入 __uiFiles__ 对象来映射文件。 manifest.json 中还支持通过 menu 字段定义插件的菜单。如果不想写 UI 也可以通过parameters指定支持的指令,直接通过输入指令来操作也是可以的。更多的配置可以查看官方文档 Plugin Manifest。

插件开发

一些基本原理了解清楚之后我们就可以进行插件的开发了。首先我们需要用户点击插件菜单之后打开一个面板,该面板可以配置尺寸、分类等基础信息。

<link rel="stylesheet" href="https://unpkg.com/figma-plugin-ds@1.0.1/dist/figma-plugin-ds.css">
<style>
.content { display: flex; }
.icon--swap { animation: rotate 1s linear infinite; }
.hide { display: none; }
@keyframes rotate {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}
</style>
<div id="app">
  <div class="field">
    <label for="" class="label">请输入图片尺寸:</label>
    <div class="content" style="padding-left: 10px;">
      <div class="input">
        <input type="input" class="input__field" placeholder="宽" name="width">
      </div>
      <div class="label" style="flex:0;">×</div>
      <div class="input">
        <input type="input" class="input__field" placeholder="高" name="height">
      </div>
    </div>
  </div>
  <div class="field">
    <label for="" class="label">请选择图片分类:</label>
    <div class="content">
      <div class="radio">
        <input id="radioButton1" type="radio" class="radio__button" value="any" name="category" checked>
        <label for="radioButton1" class="radio__label">全部</label>
      </div>
      <div class="radio">
        <input id="radioButton2" type="radio" class="radio__button" value="animals" name="category" >
        <label for="radioButton2" class="radio__label">动物</label>
      </div>
      <div class="radio">
        <input id="radioButton3" type="radio" class="radio__button" value="arch" name="category" >
        <label for="radioButton3" class="radio__label">建筑</label>
      </div>
      <div class="radio">
        <input id="radioButton4" type="radio" class="radio__button" value="nature" name="category" >
        <label for="radioButton4" class="radio__label">自然</label>
      </div>
      <div class="radio">
        <input id="radioButton5" type="radio" class="radio__button" value="people" name="category" >
        <label for="radioButton5" class="radio__label">人物</label>
      </div>
      <div class="radio">
        <input id="radioButton6" type="radio" class="radio__button" value="tech" name="category" >
        <label for="radioButton6" class="radio__label">科技</label>
      </div>
    </div>
  </div>
  <div class="field">
    <label for="" class="label">请选择图片滤镜:</label>
    <div class="content">
      <div class="radio">
        <input id="radioButton7" type="radio" class="radio__button" value="none" name="filter" checked>
        <label for="radioButton7" class="radio__label">正常</label>
      </div>
      <div class="radio">
        <input id="radioButton8" type="radio" class="radio__button" value="grayscale" name="filter" >
        <label for="radioButton8" class="radio__label">黑白照</label>
      </div>
      <div class="radio">
        <input id="radioButton9" type="radio" class="radio__button" value="sepia" name="filter" >
        <label for="radioButton9" class="radio__label">老照片</label>
      </div>
    </div>
  </div>
  <div class="field" style="padding:0 10px;">
    <div id="create" class="icon-button" style="width: 100%;">
      <div class="icon icon--image"></div>
      <div class="type type--small type--medium type--inverse">插入</div>
    </div>
    <div class="icon-button loading hide" style="width: 100%;">
      <div class="icon icon--swap"></div>
    </div>
  </div>
</div>

官方文档里有推荐 https://github.com/thomas-lowry/figma-plugin-ds 这个仓库,提供 Figma 的 UI 库组件,会让你的插件显的更加“原生”。

由于之前说过所有的资源都需要内嵌到 html 中,所以我使用了 CDN 地址的形式引入了样式文件。另外由于功能比较简单,这里也没有使用 React 等框架去进行开发。官方模板中有 React 模板可以参考 https://github.com/figma/plugin-samples/tree/master/webpack-react

获取图片

UI 完成之后接下来我们需要实现功能。我们需要将图片下载下来插入到 Figma 图层中。由于主线程没有网络能力,所以这部分工作需要在 UI 线程中完成,再通过 postMessage 传递回主线程中完成后续操作。具体的代码如下:

<script>
async function loadImage(url) {
  const resp = await fetch('http://localhost:3000/' + url);
  const buffer = await resp.arrayBuffer();
  return new Uint8Array(buffer);
}

document.getElementById('create').onclick = async (e) => {
  const width = parseInt(document.querySelector('input[name="width"]').value);
  const height = parseInt(document.querySelector('input[name="height"]').value);
  const category = document.querySelector('input[name="category"]:checked').value;
  const filter = document.querySelector('input[name="filter"]:checked').value;
  const loading = document.querySelector('.icon-button.loading');

  e.target.classList.add('hide');
  loading.classList.remove('hide');
  
  const imgBytes = await loadImage(`https://placeimg.com/${width}/${height}/${category}/${filter}`);
  parent.postMessage({ pluginMessage: { type: 'insert', bytes: imgBytes, width: width, height: height } }, '*');

  loading.classList.add('hide');
  e.target.classList.remove('hide');
}
</script>

由于 UI 线程是一个纯 Web 环境,当我们使用 XMLHttpRequest 或者 fetch 发送请求的时候,肯定会碰到跨域的问题。按照文档 https://www.figma.com/plugin-docs/making-network-requests/ 提供的解决办法,我们只能依靠服务端加层代理来解决。

当你的插件没有 UI 面板的时候,如何进行网络请求?按照文档所说,我们是可以设置 figma.ui.show() 的第二个参数,将其设置成 visible: false 的形式创建 iframe 获取数据。

// code.ts
function fetch(url, options) {
  const html = `<script>
    fetch(${url}, ${JSON.stringify(options)}).then(resp => resp.json()).then(resp => parent.sendMessage({
      pluginMessage: { type: 'networkRequest', data: resp }
    });
  </script>`;
  
  return new Promise(resolve => {
    figma.ui.on('message', msg => 
      msg.type === 'networkRequest' && resolve(msg.data)
    );
    figma.ui.show(html, { visible: false });
  });
}

插入图片

由于只有主线程才能操作 Figma 数据,所以需要在 UI 线程 postMessage 传递数据到主线程中继续进行操作。

主线程中的步骤就比较简单了,使用 Figma API 创建好矩形并将图片填充即可完成图片的插入。

我们可以通过设置 figma.currentPage.selection 设置选中项,并使用 figma.viewport.scrollAndZoomIntoView 将刚插入的数据滚动到视野中。

figma.ui.onmessage = msg => {
  if (msg.type === 'insert') {
    const rectNode = figma.createRectangle();
    const image = figma.createImage(msg.bytes);

    rectNode.name = 'Image';
    rectNode.resize(msg.width, msg.height);
    rectNode.fills = [{
      imageHash: image.hash,
      scaleMode: 'FILL',
      scalingFactor: 0.5,
      type: 'IMAGE'
    }];
    
    figma.currentPage.appendChild(rectNode);
    figma.currentPage.selection = [rectNode];
    figma.viewport.scrollAndZoomIntoView([rectNode]);
  }

  figma.closePlugin();
};

除了需要显示的调用 figma.ui.show 来展示 UI 之外,在执行完插件后需要显示的调用 figma.closePlugin() 告知 Figma 进行关闭插件操作。

优化插件

上面我们实现了配置宽高然后插入一张图片。但有时候我们会先插入一个矩形占位,之后才会将其替换成图片。所以我们可以优化下操作步骤,当选中到一个矩形的时候,自动获取到它的尺寸,然后点击插入后会直接插入到该矩形中。

// code.ts
function initSelectionState() {
  if (figma.currentPage.selection.length === 1 && figma.currentPage.selection[0].type === 'RECTANGLE') {
    const rectNode = figma.currentPage.selection[0];
    figma.ui.postMessage({ type: 'update', width: rectNode.width, height: rectNode.height });
  }
}
figma.on('selectionchange', initSelectionState);
initSelectionState();

通过在主线程中监听 selectionchange 事件,我们能实时获取到当前选中的元素。我们将尺寸信息发送到 UI 线程后让其填充到输入框中称为默认值。

window.onmessage = function(e) {
  if (e.data.pluginMessage.type === 'update') {
    document.querySelector('input[name="width"]').value = e.data.pluginMessage.width;
    document.querySelector('input[name="height"]').value = e.data.pluginMessage.height;
  }
}

最后再插入的时候,我们也需要判断如果有选中矩形的话则优先使用选中的矩形,而不是新增矩形。

let rectNode: RectangleNode;
if (figma.currentPage.selection.length === 1 && figma.currentPage.selection[0].type === 'RECTANGLE') {
  rectNode = figma.currentPage.selection[0];
} else {
  rectNode = figma.createRectangle();
}
// const rectNode = figma.createRectangle();

插件发布

最终我们的插件的主体功能就开发完毕了。下面我们就可以进行插件的发布了。我们可以直接通过插件管理中 Publish 操作进行发布。

和 Chrome 插件有点类似,Figma 插件支持发布到社区,也支持发布到组织。支持发布到多个组织中。发布到组织不需要审核,但只有该组织的同学和文件可使用。发布到社区的需要由 Figma 官方审核。

插件调试

由于是 Web 技术向,所以 Figma 的插件调试非常简单。直接 Command + Shift + I 打开控制台即可。

不过比较麻烦的是热更新的支持不太好。之前页面资源需要编译到 html 问价中的方式也不太友好。所以有人就想到了** iframe 套娃**来解决 UI 的更新问题。

简单来说就是通过在 UI 线程中再嵌套一个在线页面,UI 线程作为主线程和新的 iframe 的消息中转。这样相当于将插件在线化,回到了纯 Web 开发模式了,热更新自然就没有什么问题了。

不过这仅能解决 UI 线程的热更新问题,主线程如果有变化还需要重新更新插件解决。基于上面的方案,其实我们能做的更“绝”一点。我们可以将主线程变成一个壳,具体的业务代码由 iframe 下发,通过这种方式来解决主线程的更新问题。

// ui.html
parent.postMessage({ pluginMessage: { type: 'MAIN_CODE', code: 'console.log(figma)' } });
// code.ts
figma.ui.onmessage = (msg) => {
  msg.type === 'MAIN_CODE' && eval(msg.code);
}

后记

通过示例讲述了如何开发一个 Figma 插件,包含了获取 Figma 数据信息,操作 Figma 文件等双向操作。基于以上简单操作我们可以完成更多有意义的事情帮助我们更好的开发。比如快速导出多尺寸图片、导出图标自动发布到 npm 等…

以上示例代码已发布到 GitHub 中,欢迎参考。

https://github.com/lizheming/figma-placeimg

阅读全文 »

lizheming 发布于 10月01, 2020

如何制作 Sketch 插件

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.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》 所说,你也可以手动生成。

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

阅读全文 »

lizheming 发布于 10月17, 2014

Typecho在线升级插件 - Update 0.0.2

是不是已经厌烦了Typecho每次升级还得开FTP覆盖文件了呢?好消息来了!为了解决这无比蛋疼的问题,Update插件正式上线了!没有复杂的配置没有复杂的选项,只要开启插件就行了!只要开启插件就行了!(等下,一股浓浓的电视购物风是什么情况Σ(っ °Д °;)っ)
当程序检测到新版的时候会在控制台首页增加一个链接,点击更新就可以了!一击解决Typecho多年蛋疼问题!

DEMO

另外比较温馨的是程序会备份Typecho程序目录下的所有文件(备份文件夹位于插件目录下的backup文件夹),所以你也不必担心升级扑街神马的,如果失败了再把备份文件还原一下就好啦!yeah,就是这么简单!

p.s. 我是不会和大家说为了攒这个插件忍着一个月没升级我的Typecho的→_→...


鉴于有一些人因为服务器文件夹权限等问题导致更新失败,本插件更新至 0.0.2 版本,为每一个过程增加了较为详细的错误信息提示。

下载地址:Box | 百度云

阅读全文 »

lizheming 发布于 12月14, 2013

评论@插件 - At 0.1.1 For Typecho 0.9

在各大微博网站中习惯了@ 这个功能之后,发现它真是越来越方便了呢。连QQ都开始支持@ 功能的今天,Typecho居然不支持这么快捷的功能也太不潮啦。不过装上At插件之后一切就没有问题啦,立马就Fashion起来了有木有。

这款软件的基本作用是能让你:

  • 在评论中方便的@ 和链接页面中的其它评论
  • 同时支持点击回复按钮评论框增加@被回复评论的用户名
  • 被@的人会收到邮件通知(传说中的究极暗黑召唤术,慎用!)
  • 更多功能还在开发中...

具体效果见下图:

请输入图片描述


插件更新至0.1.0版,增加了对@人邮件通知的功能(需要在后台填写相关设置)。因为功能涵盖关系,最好不要和CommentToMail插件同时使用,会导致发送多次邮件的情况出现。另外,邮件模板中可以使用的变量见下表:

变量名 代表内容 说明
{site} 博客站点名称 在博客后台设置中设定的
{title} 评论文章标题
{author} 作者名称
{mail} 评论者的邮箱
{permalink} 评论链接
{text} 发评论的正文
{time} 发布评论时间
{author_m} 被提及的用户名称
{text_m} 被提及的用户评论

插件更新至0.1.1版,修复插件因为评论特殊字符造成的BUG。

下载地址:BOX | 百度云

阅读全文 »

lizheming 发布于 12月06, 2013

代码高亮插件 - GoogleCodePrettify 2.0.0 for typecho 0.9

代码高亮里面我个人比较偏爱Google Code Prettify这款,主要是比较喜欢它的样式,而且加载也非常的简单。由于更新了新版本,70大大写的Google Code Pretty插件使用起来就好像有点不太正常了。所以我根据自己的习惯重新写了这款插件,也叫Google Code Pretty,但是版本号升级成了2.0.0,希望不跟大大的重了吧。

如何使用?

Google Code Prettify是JavaScript前台渲染高亮插件,基本不占用后台资源。
启用插件之后选择一款你们喜欢的样式就算启用成功了。
插件自带5款样式,虽然不像某插件自带三十多款样式,不过也基本算满足一些人的需求了。

后台设置同时非常温馨的自带了自定义CSS设置,如果对当前高亮样式不满意,可以在自定义CSS内DIY自己想要的样式。

效果预览

后台设置

前台渲染效果

下载地址:BOX | 百度云 | 微盘 | OneDrive

阅读全文 »

lizheming 发布于 11月16, 2013

OriginalEditor 1.0.1 for Typecho 0.9 - 让Typecho回归HTML模式

Typecho升级到0.9改用Markdown作为编辑器是一件令人高兴的事情。但是有些事情总是不能两全其美,一部分人正在高兴的同时,也有一小撮同志在埋怨Markdown编辑器怎么怎么不好用,怎么怎么难用,怎么怎么多的BUG。介于官方众口难调的情况,我就答应论坛的@sujianlin 做了这款插件。

插件的作用插件名字已经表达的非常明白了:让编辑器回归原始。说白了就是去除了0.9版的Markdown编辑器(包括后台的编辑器和前台网页的解析),回到0.8以前的HTML MODEL。插件没有多做测试,如果有问题的话请及时反馈。

最后说一句,我爱Markdown,你们根本不知道Markdown配上快捷键是有多么的好用!以上!

toc: true authors: ['lizheming']

本插件紧急升级至1.0.1版,修复插件无法安装的问题。实践告诉我们,千万别手贱啊!

下载地址:BOX | 百度云

阅读全文 »

lizheming 发布于 11月15, 2013

Editor 1.0.2 for Typecho 0.9 - 非官方Markdown编辑器

简单的来说这就是一款Markdown编辑器,和官方自带默认的不同的是有些功能上的优化,我总结为以下几个:

  • 自带类可视化编辑(输入标记符后能实时渲染)
  • 标记符输入半自动化(列表标记符输入一个之后下面的会自动生成,代码块下一行会在上一行的Tab处起始)
  • 支持多行选中后按Tab(对于粘贴代码的人来说真是非常需要)

基于以上原因,我就顺手把它移植了过来,感觉非常不错的说。当然优点还有很多,我只是列举了几个我比较喜欢的。如果有喜欢的同学可以到这里在线测试一下:Editor在线编辑器


插件紧急更新至1.0.1版,修复插件页面无法找到插件的问题。


插件更新至1.0.2版,修复编辑器在Firefox浏览器下图标显示不正常的问题。

下载地址:BOX | 百度云(提取码: 2awd)

阅读全文 »

lizheming 发布于 11月08, 2013

Markdown编辑器左右编辑插件 - LREditor 0.0.4 For Typecho 0.9

Typecho默认的预览是在编辑器文本框的下面,这对实时预览来说是比较糟糕的一件事,我想应该没人写一句就往下拉过去看下吧。以这个为需求,LREditor插件就这么诞生了。

插件的作用是将原生的上编辑下预览结构改成左编辑右预览,同时插件只对原生的Markdown编辑器有效,如果装了其它编辑器的话不保证不会出问题。如果有什么问题可以在下面留言


插件更新至0.0.2版,增加预览框代码高亮功能


插件更新至0.0.3版,修改代码高亮样式,使得书写文章时更协调。


插件更新至0.0.4版,修复无法启用插件的问题,同时修改样式以适应Typecho的新版本。

下面放一张预览图

请输入图片描述

下载地址:Box | 百度云

阅读全文 »

lizheming 发布于 11月07, 2013

XiaMiPlayer插件 for typecho 0.9(更新至3.1.2)

曾经大家以为Typecho还会蓬勃发展的时候,它停下了脚步;现在大家以为Typecho必然行之将死的时候,它居然更新了!是的,在半个月前,历时几年后Typecho终于发布了新版。虽然只是小小的0.1,但是这是一个契机,之后Typecho开始了疯狂的更新,Github上也开始活跃了起来。不管怎么说,这是一件值得令人高兴的事情。

新版最重要的一个功能就是把Markdown编辑器给加上去了,而且样式上都有比较大的变化,所以我的XiaMiPlayer插件对于新版也出现了一点问题。在前几个版本Typecho终于恢复了文章/页面编辑页面上的插件接口之后,我就立马做了一下优化。目前这个版本应该能比较完美的支持Typecho 0.9了。

目前的版本号是3.0.1,算是Alpha版吧。如果大家有什么意见建议的话请留言。以后的打算是这样的,基数版本号(3.x)对应新版,偶数版本号(2.x)对应老版本,想要下载老版本的可以在这里下载:XiaMiPlayer插件 for typecho 0.8。


本插件更新至3.0.2版,增强了插件与Typecho后台的兼容性(推荐使用默认Markdown编辑器效果会更好),并增加了多个快捷键。

快捷键一览:

  • Ctrl + Shift + M :进入插件
  • Esc :退出插件
  • Ctrl + ← :虾米搜索
  • Ctrl + → :输入链接
  • Ctrl + ↑ :上一页
  • Ctrl + ↓ :下一页
  • Ctrl + Enter : 搜索
  • Ctrl + [1-8] :插入相应行的歌曲

本插件紧急更新至3.0.3版,修复了搜索结果页码不重置的问题,同时为搜索结果增加数字提示以方便使用快捷键。


本插件更新至3.0.4版,增加了对非默认Markdown编辑器的支持,更新播放器代码,设置为默认不自动加载歌曲文件,减少页面加载时间。


本插件更新至3.0.5版,修复了自定义歌曲地址不支持自定义配色的BUG。


本插件更新至3.0.6版,修复了之前版本播放器的一些样式问题。增加了添加列表播放器的功能(暂时还没有简洁好看的播放器样式,所以还是先用了官方的Flash播放器,所以只支持虾米搜索添加,直链添加会在之后的版本中支持)。选择了列表模式之后添加的歌曲会进入列表中,上下拖拽列表歌曲可以对歌曲进行排序,左右拖拽至外侧可删除歌曲。新功能写的比较仓促没做过多的测试,欢迎反馈意见。


本插件更新至3.0.7版,更改音乐的插入方式(由iframe替换为script)。列表播放支持自定义地址。代码改动较大,为了兼容老版本没有将一些废弃文件去除掉,想要研究代码的可以进插件内查看。


本插件更新至3.0.8版,修复了音乐搜索不可用的问题


本插件更新至3.0.9,修复了一些小BUG,增加直接解析文章内以http://开头的虾米链接插入歌曲、专辑、精选集功能。例如在文章中直接插入“http:// www.xiami.com/album/435293“(请自行去除空格)即可插入该专辑的所有音乐,同时支持Markdown模式和非Markdown模式。

http://www.xiami.com/album/435293


本插件更新至3.1.0,修改插件内以相对协议加载相关文件,防止https阻止非https的资源加载。同时修改播放器的class名称防止与主题自带样式冲突。


插件更新至3.1.1,主要是修改了接口地址,另外修改了一个多歌曲时无法切换的 BUG。之前的服务放在 SAE 上由于 SAE 开始收费遂切换到另外一个免费服务提供商。由于该服务商未提供 SSL 服务所以对于 https 用户只能抱歉。如果需要替换之前已经发布的歌曲链接可以在数据库中执行以下命令进行批量替换:

UPDATE `typecho_contents` SET `text` = REPLACE(`text`, 'songs.sinaapp.com', 'xiamiplayer.songs.ali-sh.goodrain.net:10080')

插件更新至 3.1.2,还是修改接口地址,求个国内主机啊卧槽 T_T

下载地址:百度云 | 微盘

阅读全文 »

lizheming 发布于 08月16, 2013

FtpUpload插件

由于我本人比较喜欢将日志中的图片上传到我的一个FTP服务器中,但是一边写一遍开着FlashFXP上传实在是蛋疼了,所以萌生了写这个插件的想法。总的来说这个插件能让你远程传送文件(不限于图片,其它文件也可以)到FTP服务器中,并及时返回文件的URL地址插入到日志当中。

启用插件的时候,Typecho会提示"Server Error"错误的信息,虽然不知道为什么(有知道的同学请一定要告诉我!),但是并不影响正常的使用。

启用插件后记得在设置中填写自己的FTP服务器信息,以及希望将文件上传到哪个文件夹中。请一定要记得这个设置的文件夹要是存在的,否则会产生错误。

插件的基体是我拿着之前写过的XiaMiPlayer插件修改的,所以样式也是从那里继承过来的。

由于本插件写的非常潦草,纯粹为了实现功能而实现功能,所以肯定有许多不足的地方,希望大家多多反馈!

以上。

下载地址:BOX | 百度云

阅读全文 »