sketch 插件开发实战

最近入坑了 sketch, 之前以为 sketch 只是 APP 效果图设计工具,后来发现身边好多研发朋友也在用,向设计师朋友请教了一下,原来 sketch 的修图在一定程度上能替代 PhotoShop , 做高逼格的流程图、效果图也很方便。以前真是 too young、too naive。

工作上设计经常变动,类似于换肤的项目也有很多,每次都是设计狮在 sketch 里设计好效果图,上传到蓝狐给产品审阅,然后在切图,导出,压缩,传给前端,前端在代码里替换,然后在 webpack 打包的的时候上传到 cdn。刚开始觉得流程没有什么问题,后来发现环节一多,越容易出错。研发里面讲高耦合,低内聚,把数据和视图分开,为什么设计和研发要这么纠缠在一起,设计改一个图,也要经过研发这边修改、提交 Git、部署上线这一繁琐的流程。好不科学,其实可以借鉴蓝狐的思路,设计直接在 sketch 里将图上传到 CDN,然后更新一下图片地址,岂不美哉。

这就需要做一个精简版蓝狐插件。好在 sketch 有简易的上手指南

  • 创建新插件
   nmp install -g skpm
   skpm create my-plugin
   cd my-plugin
  • 运行插件
   npm run watch

然后打开sketch,我滴乖乖,居然这么简单,居然在 Plugin 里面直接就有了我们刚刚创建的 my-plugin,点击里面的 My Command,直接弹出了toast 提示。

然后自信心爆棚,javascript 嘛,分分钟搞一个蓝狐插件出来。接着就懵逼了。按照需求无非就是两步,第一获取到图片,第二上传。一清晰思路,这才发现事情并没有这么简单,这里用到的语言根本不是我想象的javascript,只是 JavaScript 标准库,也就是说语法是可以用的,但是 setTimeout、fetch、console 都是不能用的,NodeJS 核心组件也是不能用的,好家伙,阉割版 JavaScript。还好sketch 帮忙填了一些坑,使用 skpm 创建的插件,是可以使用 console、setTimeout、setInterval、fetch 的。各种UI 界面还要用到 Mocha 和 CocoaScript,Mocha 和 CocoaScript 是通过 JavaScript 调用Objective-C/Cocoa 代码的桥梁库。一看 Mocha 和 CocoaScript,文档给的很少,稍微详细一点的是 JavaScript API/Action,唉,Objective-C/Cocoa 完全不会啊,对照着文档开发几乎不可能,要凉。

继续研究,看my-plugin 的代码,经过一番鼓捣,终于靠 JavaScript 搞定了,代码如下

my-command.js

const Settings = require('sketch/settings');  
const UI = require('sketch/ui');

export function uploadSelect(context) {  
  onRun(context, false);
}
export function setting(e) {  
  onRun(context, true);
}

function exportLayerAsBitmap(document, layer, scale) {  
  var slice,
    result = {},
    rect = layer.absoluteRect(),
    path = NSTemporaryDirectory() + layer.objectID() + '.png';
  NSMakeRect(rect.x(), rect.y(), rect.width(), rect.height()),
    (slice = MSExportRequest.exportRequestsFromExportableLayer(
      layer
    ).firstObject()),
    (slice.page = document.currentPage()),
    (slice.format = 'png'),
    (slice.scale = scale),
    document.saveArtboardOrSlice_toFile(slice, path);

  var url = NSURL.fileURLWithPath(path),
    bitmap = NSData.alloc().initWithContentsOfURL(url),
    base64 = bitmap.base64EncodedStringWithOptions(0);

  NSFileManager.defaultManager().removeItemAtURL_error(url, nil);
  var imgRep = NSBitmapImageRep.imageRepWithData(bitmap);
  return (
    (result.bitmap = base64),
    (result.width = imgRep.pixelsWide() / 4),
    (result.height = imgRep.pixelsHigh() / 4),
    result
  );
}

const onRun = function(context, setting) {  
  const url = Settings.settingForKey('url');
  if (setting || !url) {
    UI.getInputFromUser(
      'Input the server address.',
      {
        initialValue: url || 'https://'
      },
      (err, value) => {
        if (err) {
          return;
        }
        return  Settings.setSettingForKey('url', value);
      }
    );
    return ;
  }

  const doc = context.document;
  const selection = context.selection;
  if (selection.count() == 0) {
    doc.showMessage('Please select the layer to upload.');
  } else {
    for (let i = 0; i < selection.count(); i++) {
      const layer = selection[i];
      const layerName = layer.name();
      const { bitmap } = exportLayerAsBitmap(doc, layer, 1);

      fetch(url, {
        method: 'POST',
        headers: {
          Accept: 'application/json',
          'Content-Type': 'application/json'
        },
        body: { image: bitmap, name: layerName }
      })
        .then(response => response.text())
        .then(text => doc.showMessage(text))
        .catch(e => doc.showMessage(e.message));
    }
  }
};

manifest.json

{
  "compatibleVersion": 3,
  "name": "Snatch",
  "description": "Snatch, upload layer to cdn directly, improve work efficiency.",
  "author": "chenzhicheng.com",
  "version": "0.1.0",
  "identifier": "com.chenzhicheng.snatch",
  "bundleVersion": 1,
  "icon": "icon.png",
  "commands": [
    {
      "name": "Upload Select",
      "identifier": "snaptchSelect",
      "shortcut": "ctrl command p",
      "script": "./my-command.js",
      "handler" : "uploadSelect"
    },
    {
      "script" : "./my-command.js",
      "handler" : "setting",
      "name" : "Set Server URL",
      "identifier" : "serverUrl"
    }
  ],
  "menu": {
    "title": "Snatch",
    "items": [
      "snaptchSelect",
      "-",
      "serverUrl"
    ]
  }
}

主要包含两个功能,一个是点击上传选中的 layer,一个是点击设置弹出 prompt,输入服务器地址,可以上传到指定的地址。

其中发现了开发插件的两个好东西,一个就是Sketch-dev-tool,另一个就是Run Script。Sketch Dev Tool 本身就是一个 Sketch 插件,类似于 chrome 的 inspect,可以查看调试信息。Run Script 是一个实时运行 JavaScript 的地方,在 Plugin -> Run Script 里打开。

为了测试上传,还要有服务器端,这个就简单了,用 ExpressJS 新建一个好了。

  • 创建服务文件夹
   mkdir expressjs
   cd expressjs
   npm init

一路默认

  • 安装express 及依赖
   npm install express
   npm install body-parser
   npm install sharp
  • 创建一个 index.js 文件,内容如下
   const express = require('express');
   const app = express();
   const bodyParser = require('body-parser');
   const sharp = require('sharp');

   app.use(bodyParser.urlencoded({extended: false, limit: '50mb'}))
   app.use(bodyParser.json({limit:'50mb'}))

   const boostSize = {
       'pic_wheel': [353, 353, 'png'],
       'pic_arrow': [45, 60, 'png'],
       'btn_luckydraw': [292, 77, 'png'],
       'bg_blue': [360, 640, 'jpg']
   }

   app.post('/', function(req, res) {
       const size = boostSize[req.body.name];
       if (!size) {
           return res.send('Name invalid!');
       }
       let buff = new Buffer(req.body.image, 'base64');
       let resize = sharp(buff)
                       .resize(size[0], size[1]);
       if (size[2]=='jpg') {
           resize = resize.jpeg();
       } else {
           resize = resize.png({adaptiveFiltering: true})
       }
       resize.toFile(req.body.name+'.'+size[2], (err) => { 
           if (err) {
               return res.send(err);
           } else {
               return res.send('Upload Success!');
           }
       });
   });

   app.listen(3000, function(){
       console.log('ok.');
   });
  • 运行
   node index.js

这样,选中图层,点击Plugin -> my-plugin -> my-command,则会上传图片到server 文件夹下。

最后一步需要将插件发布到官方,有两种方法,一种是forkPlugin directory,然后提交 PR,另一种是使用 skpm publish。我使用 skpm publish 失败了。

最后附上 github 链接

https://github.com/ZhichengChen/snatch

https://github.com/ZhichengChen/snatch-server