Dec
3
什么是Sketch插件,从sketch的plugins菜单点开你就可以看到了。进入manage plugins...点击get plugins...,你就可以看到官网上数以吨计的解决各种各样问题的sketch插件。理论上到这一步我们就没问题了,不过正好,当前我手下的case,由于ui和项目架构的规范要求,还真没插件可以帮我做这事儿,所以我们开细看sketch插件。
它是怎么实现的。sketch插件开发主要使用的是一门冷门的脚本语言cocoaScript(https://github.com/ccgus/CocoaScript),这是某知名开源作者搞的让js和oc混写来编写cocoa应用程序的脚本语言,不过看git上的记录的话,34年前就已经凉了,而且从搜索结果上看,基本上除了sketch插件就没有大型软件或者框架在使用这门脚本语言了。因此,相对的参考资料比较少,这是接下来有各自坑的主要原因。
那么它能做到什么?
首先我们要问cocoaScript能做到什么。其实cocoaScript的核心代码(https://github.com/ccgus/CocoaScript/blob/master/src/framework/mocha/MochaRuntime.m)很短,简短浏览一下就知道,它本质上是一个魔改过的JSCore和一个同步的JSBridge,让JS和OCRuntime交互,换一个角度理解,它就是js,并且具有在程序中操作ocruntime的能力。分开看的话:
- js部分:拥有基础的js,无法使用es5/6的特性。不过通过补充其他js文件,可以获得更多能力
- runtime部分:除了增删改类/属性/方法相关的方法没暴露,其他东西都可以直接操作
那么回到sketch中,我们能做到什么呢?相对的,sketch没有对cocoaScript运行做任何限制,并且sketch绝大部分功能都是oc实现。因此,我们可以得到结论,插件在sketch,可以为所欲为。
为此我们开发sketch插件,需要会什么?
- 基本的js
- oc和osx开发(osx开发主要是插件界面部分,和对sketch界面进行拓展和修改)
- 习惯cocoaScript语法
- 了解sketch的oc类/属性/方法。这个有人dump出来了(https://github.com/abynim/Sketch-Headers)
前两个问题我觉得随便百度一下就可以解决,我们从第三个看起。
CocoaScript踩坑
CocoaScript编写体验最大的问题,下面这一段代码就可以提现:
这段代码中,我们新建了两个字符串,分别是oc风格的NSString和js风格的string,然后,分别做了:a.打印它们。b. typeof它们。c.获取他们的length。d.它们之间作比较。e.把它们放到对象中转换为json。最后得到的结果分别如下:
fuck,现在的感觉就不对了:
1. 两个字符串输出为原始定义的123,看起来还算正常的,也是一样的
2. 但是用js的typeof关键字打出来,NSString那个在js中被看作了object,和一般的字符串不太一样了
3. 调用.length属性,明明NSString也有length属性,得到的却是MOMethod的log,即告诉我们在js中那是一个oc方法
4. 它们两通过计算后是相等的
5. 在转换为json后,果不其然,NSString是一个对象,而里面的内容完全无法解释出来
那么为什么呢,我们来回归js本身,js是一门弱类型语言,而不同类型的对象,只有通过做运算,才会转换成相同类型的到结果。原本js的type很简单,number,string,array,object,而现在oc的对象混进来了,而且明显完全不一样,因此我们的cs中多了至少四种类型,即NSString,NSNumber,NSArray,NSDictionary,而cs中,对这四种对象,也做了强转规则。
因此首先就可以理解结果4了,通过==运算,它们被转换为了相同的string并运算,自然就是相等的了。而由于OC对象不是js的typeof能识别的,它被当作object在各种地方被处理了也是可以理解的。
唯一的问题是NSString的length属性为啥取不出来,分析过后就可以看到,cs的特性估计就是oc对象在cs中直接点属性即getter方法名字,获得是方法而不是运行getter方法,应该是为了兼容括号运行oc方法的缘故,既然设定如此,不方便也只能接受了。
因此,如果oc对象是oc对象,js对象是js对象,开发cs程序时,只要开发者能区分出到底哪些是oc对象哪些是js对象,然后用不同的语法和规则去编写对应的代码,并且在两种数据转换之间做好强转(比如各种 + '' 和 * 1 的操作),就可以相对舒服的编写cocoaScript脚本了。
Sketch插件
会这些我们就开始进入Sketch进行插件开发了,然而我们首先需要知道以下三件事:
插件入口manifest.json
首先是sketch插件的必要格式:
- 放在一个.sketchplugin后缀的文件夹中作为程序包
- 里面有Sketch和Resources两个文件夹储存程序脚本和资源文件(其实是随意的)
- Sketch中有一个manifest.json。因此,其实最核心的插件入口就在这里了
那么,我们来看一个典型的manifest.json:
除了各种基本信息,最重要的部分应该是commands,也就是命令。应该是,sketch插件上的各种功能是以命令为单位进行聚合的,每一个命令是一段脚本的入口,指向对应的脚本文件(script)和函数(handler)。相反menu倒是可有可无的东西,反正可以有快捷键(shortcut)。
整个配置和每一个命令都有一个id,虽然没发现有什么用反正不要重复最好。
因此,我们以命令为单位,就可以进行实际功能开发了。
程序上下文context与Sketch环境
实际开始编写代码,虽然上文中我们得出的结论是我们可以为所欲为,不过我们还是来看看Sketch提供了什么。当执行一条插件的代码时,Sketch会传入一个叫context的js对象,它在整个插件编写的过程中发挥着很大的作用。因此我们log出context看看:
其中<>的都是oc对象,并且MO是cocoascript的对象(我们可以看到源码),MS是sketch的对象(我们可以查到头文件)
因此我们挨个看看:
- api是一个MOJavaScriptObject,实际log里面的东西之后发现现在什么也没有,可能是一个api预留吧。
- command是当前运行这个command(下文讲)的上下文,也就是当前运行脚本的信息。
- document很重要,是当前运行的文档。这是一个功能聚合的对象,我们发掘sketch很多功能都得从它展开。(https://github.com/abynim/Sketch-Headers/blob/master/Headers/MSDocument.h)顺带一提,[MSDocument currentDocument]应该也是这个对象;MSDocument是继承于NSDocument,它的功能聚合性非常强。
- plugin可以取得当前插件的信息,就是manifest.json那些和其他的。比如它有一个方法urlForResourceNamed:可以获得资源文件地址,是非常方便的。
- selection,是sketch中当前选中的对象,和document.selectedLayers是同一回事儿,返回的是一个MSLayerArray
当然,以上信息解读,我们已经非常依赖倒出来的Sketch的头文件了。如果我们想寻找一个具体功能做插件或者hook的突破口,就从以上对象为起点(主要是document),继续阅读就是。
开发工具与调试方法
由于是冷门脚本语言,除了语言资料很难找到,开发工具链也很难处理。
开发过程的话,ide肯定是没有,就算要语法高亮,也只能用js将就一下问题不大。不过程序中纯oc部分,比如AppKit写界面的时候,可以到xcode中随便建一个osx项目先写好,再移植到cocoascript中,这样的开发难度应该会小很多。
调试的话,主要依赖的是sketch提供的log或者就用NSLog。其中sketch的log可以在sketch的run script中看到,这个可以作为很方便的playground做功能测试。而log也会像NSLog把log打到应用级log中,通过控制台(Console.app)筛选出sketch即可看到了。(js的console.log反而没有)
其他
实际开发过程中,还有一些周边议题。
开发方式
除了这些,实际功能开发起来,就是千人千面了。总的从网上看到的插件构成来说,开发方式还不少。
1. 按照上文的思路,直接编写cocoaScript脚本,混写js和AppKit功能。
2. 如果对于功能极为复杂的插件,这样开发确实不方便开发调试,另外其实这样是完全暴露源码的。因此可以看到部分插件的主要功能是编译了一个framework这样的东西,通过cocoaScript加载并作为bridge传递信息,实现功能。
3. 还有一种插件,可能是web前端哥写的,对AppKit不熟悉也不想学,因此直接整了一个webview,在里面嵌入网页用web实现界面和功能,cocoaScript加载并作为bridge传递信息。这样的好处是可以做一些在线功能的插件并且基本不需要native开发支持。
4. 最后,最暴力的,是直接在插件包中携带完全独立的程序实现功能,cocoaScript还是做bridge当老好人了。
根据不同的人,程序规模,也许不同的开发方式会更适合。
另外虽然可以,但是还是不建议入侵sketch的界面,最好还是自己启动windows。由于sketch插件的运行和管理方式,深入侵到sketch的对象中可能会导致较为严重的内存泄漏等问题,吃力不讨好。
版本兼容
由于我们很多功能都是从抓的sketch头文件里扒出来的,而sketch更新其实挺快的,所以很有可能我们当前版本可用的插件,下一个版本就gg了。
还好,我们怎么说是可以通过[NSBundle mainBundle]取到sketch版本号的,所以我们需要评估代码中的风险点并做好版本判断。并且对各个最近版本做好回归测试。
当然最重要的还是要保持更新,sketch更了自己就要更新并测试修改发布,真是辛苦啊。
另外manifest.json中好像可以设置compatibleVersion,不过不知道有没有卵用。
发布
绝大部份插件其实是通过github直接发布的,毕竟download下来双击一下就可以用了。
另外通过讨论社区http://sketchplugins.com/和官网https://sketchapp.com/extensions/plugins/也是可以把插件发布和推广出来的。
教程Demo
先看看这次写的https://github.com/Lrdcq/xgfe-app-sketch-exporter吧,这篇博客的插件编写思路就是写这玩意儿的思路了。至于这个插件到底怎么回事儿,下一篇博客会作详细说明的。
它是怎么实现的。sketch插件开发主要使用的是一门冷门的脚本语言cocoaScript(https://github.com/ccgus/CocoaScript),这是某知名开源作者搞的让js和oc混写来编写cocoa应用程序的脚本语言,不过看git上的记录的话,34年前就已经凉了,而且从搜索结果上看,基本上除了sketch插件就没有大型软件或者框架在使用这门脚本语言了。因此,相对的参考资料比较少,这是接下来有各自坑的主要原因。
那么它能做到什么?
首先我们要问cocoaScript能做到什么。其实cocoaScript的核心代码(https://github.com/ccgus/CocoaScript/blob/master/src/framework/mocha/MochaRuntime.m)很短,简短浏览一下就知道,它本质上是一个魔改过的JSCore和一个同步的JSBridge,让JS和OCRuntime交互,换一个角度理解,它就是js,并且具有在程序中操作ocruntime的能力。分开看的话:
- js部分:拥有基础的js,无法使用es5/6的特性。不过通过补充其他js文件,可以获得更多能力
- runtime部分:除了增删改类/属性/方法相关的方法没暴露,其他东西都可以直接操作
那么回到sketch中,我们能做到什么呢?相对的,sketch没有对cocoaScript运行做任何限制,并且sketch绝大部分功能都是oc实现。因此,我们可以得到结论,插件在sketch,可以为所欲为。
为此我们开发sketch插件,需要会什么?
- 基本的js
- oc和osx开发(osx开发主要是插件界面部分,和对sketch界面进行拓展和修改)
- 习惯cocoaScript语法
- 了解sketch的oc类/属性/方法。这个有人dump出来了(https://github.com/abynim/Sketch-Headers)
前两个问题我觉得随便百度一下就可以解决,我们从第三个看起。
CocoaScript踩坑
CocoaScript编写体验最大的问题,下面这一段代码就可以提现:
var str = [NSString stringWithString:"123"];
log(str)
var str2 = "123";
log(str2)
log(typeof str)
log(typeof str2)
log(str.length)
log(str2.length)
log(str == str2)
var json = {str:str,str2:str2}
log(JSON.stringify(json))
这段代码中,我们新建了两个字符串,分别是oc风格的NSString和js风格的string,然后,分别做了:a.打印它们。b. typeof它们。c.获取他们的length。d.它们之间作比较。e.把它们放到对象中转换为json。最后得到的结果分别如下:
123
123
object
string
<MOMethod: 0x608001030060 : target=0x600003c4d530123, selector=length>
3
1
{"str":{},"str2":"123"}
fuck,现在的感觉就不对了:
1. 两个字符串输出为原始定义的123,看起来还算正常的,也是一样的
2. 但是用js的typeof关键字打出来,NSString那个在js中被看作了object,和一般的字符串不太一样了
3. 调用.length属性,明明NSString也有length属性,得到的却是MOMethod的log,即告诉我们在js中那是一个oc方法
4. 它们两通过计算后是相等的
5. 在转换为json后,果不其然,NSString是一个对象,而里面的内容完全无法解释出来
那么为什么呢,我们来回归js本身,js是一门弱类型语言,而不同类型的对象,只有通过做运算,才会转换成相同类型的到结果。原本js的type很简单,number,string,array,object,而现在oc的对象混进来了,而且明显完全不一样,因此我们的cs中多了至少四种类型,即NSString,NSNumber,NSArray,NSDictionary,而cs中,对这四种对象,也做了强转规则。
因此首先就可以理解结果4了,通过==运算,它们被转换为了相同的string并运算,自然就是相等的了。而由于OC对象不是js的typeof能识别的,它被当作object在各种地方被处理了也是可以理解的。
唯一的问题是NSString的length属性为啥取不出来,分析过后就可以看到,cs的特性估计就是oc对象在cs中直接点属性即getter方法名字,获得是方法而不是运行getter方法,应该是为了兼容括号运行oc方法的缘故,既然设定如此,不方便也只能接受了。
因此,如果oc对象是oc对象,js对象是js对象,开发cs程序时,只要开发者能区分出到底哪些是oc对象哪些是js对象,然后用不同的语法和规则去编写对应的代码,并且在两种数据转换之间做好强转(比如各种 + '' 和 * 1 的操作),就可以相对舒服的编写cocoaScript脚本了。
Sketch插件
会这些我们就开始进入Sketch进行插件开发了,然而我们首先需要知道以下三件事:
插件入口manifest.json
首先是sketch插件的必要格式:
- 放在一个.sketchplugin后缀的文件夹中作为程序包
- 里面有Sketch和Resources两个文件夹储存程序脚本和资源文件(其实是随意的)
- Sketch中有一个manifest.json。因此,其实最核心的插件入口就在这里了
那么,我们来看一个典型的manifest.json:
{
"author": "lrdcq",
"commands" : [
{
"name": "Export",
"identifier": "com.lrdcq.sketch-commandExport",
"shortcut": "ctrl option e",
"handler" : "commandExport",
"script": "main.js"
},
{
"name": "Setting",
"identifier": "com.lrdcq.sketch-commandSetting",
"handler" : "commandSetting",
"script": "main.js"
}
],
"menu" : {
"items" : [
"com.lrdcq.sketch-commandExport",
"com.lrdcq.sketch-commandSetting"
],
"title" : "XGAPP Resources Exporter"
},
"identifier": "com.lrdcq.sketch",
"homepage": "http://lrdcq.com",
"version": "0.1.0",
"description" : "Just try.",
"authorEmail" : "luoruidong@163.com",
"name" : "XGFE APP Sketch Resources Exporter"
}
除了各种基本信息,最重要的部分应该是commands,也就是命令。应该是,sketch插件上的各种功能是以命令为单位进行聚合的,每一个命令是一段脚本的入口,指向对应的脚本文件(script)和函数(handler)。相反menu倒是可有可无的东西,反正可以有快捷键(shortcut)。
整个配置和每一个命令都有一个id,虽然没发现有什么用反正不要重复最好。
因此,我们以命令为单位,就可以进行实际功能开发了。
程序上下文context与Sketch环境
实际开始编写代码,虽然上文中我们得出的结论是我们可以为所欲为,不过我们还是来看看Sketch提供了什么。当执行一条插件的代码时,Sketch会传入一个叫context的js对象,它在整个插件编写的过程中发挥着很大的作用。因此我们log出context看看:
{
api = "<MOJavaScriptObject: 0x600000e25420>";
command = "<MSPluginCommand: 0x6080000f0d80>";
document = "<MSDocument: 0x7fc278f5b440>";
plugin = "<MSPluginBundle: 0x6000002e0d80>";
scriptPath = "/Users/**/Library/Application Support/com.bohemiancoding.sketch3/Plugins/XGAPPSketchExporter.sketchplugin/Contents/Sketch/main.js";
scriptURL = "file:///Users/**/Library/Application%20Support/com.bohemiancoding.sketch3/Plugins/XGAPPSketchExporter.sketchplugin/Contents/Sketch/main.js";
selection = (
);
}
其中<>的都是oc对象,并且MO是cocoascript的对象(我们可以看到源码),MS是sketch的对象(我们可以查到头文件)
因此我们挨个看看:
- api是一个MOJavaScriptObject,实际log里面的东西之后发现现在什么也没有,可能是一个api预留吧。
- command是当前运行这个command(下文讲)的上下文,也就是当前运行脚本的信息。
- document很重要,是当前运行的文档。这是一个功能聚合的对象,我们发掘sketch很多功能都得从它展开。(https://github.com/abynim/Sketch-Headers/blob/master/Headers/MSDocument.h)顺带一提,[MSDocument currentDocument]应该也是这个对象;MSDocument是继承于NSDocument,它的功能聚合性非常强。
- plugin可以取得当前插件的信息,就是manifest.json那些和其他的。比如它有一个方法urlForResourceNamed:可以获得资源文件地址,是非常方便的。
- selection,是sketch中当前选中的对象,和document.selectedLayers是同一回事儿,返回的是一个MSLayerArray
当然,以上信息解读,我们已经非常依赖倒出来的Sketch的头文件了。如果我们想寻找一个具体功能做插件或者hook的突破口,就从以上对象为起点(主要是document),继续阅读就是。
开发工具与调试方法
由于是冷门脚本语言,除了语言资料很难找到,开发工具链也很难处理。
开发过程的话,ide肯定是没有,就算要语法高亮,也只能用js将就一下问题不大。不过程序中纯oc部分,比如AppKit写界面的时候,可以到xcode中随便建一个osx项目先写好,再移植到cocoascript中,这样的开发难度应该会小很多。
调试的话,主要依赖的是sketch提供的log或者就用NSLog。其中sketch的log可以在sketch的run script中看到,这个可以作为很方便的playground做功能测试。而log也会像NSLog把log打到应用级log中,通过控制台(Console.app)筛选出sketch即可看到了。(js的console.log反而没有)
其他
实际开发过程中,还有一些周边议题。
开发方式
除了这些,实际功能开发起来,就是千人千面了。总的从网上看到的插件构成来说,开发方式还不少。
1. 按照上文的思路,直接编写cocoaScript脚本,混写js和AppKit功能。
2. 如果对于功能极为复杂的插件,这样开发确实不方便开发调试,另外其实这样是完全暴露源码的。因此可以看到部分插件的主要功能是编译了一个framework这样的东西,通过cocoaScript加载并作为bridge传递信息,实现功能。
3. 还有一种插件,可能是web前端哥写的,对AppKit不熟悉也不想学,因此直接整了一个webview,在里面嵌入网页用web实现界面和功能,cocoaScript加载并作为bridge传递信息。这样的好处是可以做一些在线功能的插件并且基本不需要native开发支持。
4. 最后,最暴力的,是直接在插件包中携带完全独立的程序实现功能,cocoaScript还是做bridge当老好人了。
根据不同的人,程序规模,也许不同的开发方式会更适合。
另外虽然可以,但是还是不建议入侵sketch的界面,最好还是自己启动windows。由于sketch插件的运行和管理方式,深入侵到sketch的对象中可能会导致较为严重的内存泄漏等问题,吃力不讨好。
版本兼容
由于我们很多功能都是从抓的sketch头文件里扒出来的,而sketch更新其实挺快的,所以很有可能我们当前版本可用的插件,下一个版本就gg了。
还好,我们怎么说是可以通过[NSBundle mainBundle]取到sketch版本号的,所以我们需要评估代码中的风险点并做好版本判断。并且对各个最近版本做好回归测试。
当然最重要的还是要保持更新,sketch更了自己就要更新并测试修改发布,真是辛苦啊。
另外manifest.json中好像可以设置compatibleVersion,不过不知道有没有卵用。
发布
绝大部份插件其实是通过github直接发布的,毕竟download下来双击一下就可以用了。
另外通过讨论社区http://sketchplugins.com/和官网https://sketchapp.com/extensions/plugins/也是可以把插件发布和推广出来的。
教程Demo
先看看这次写的https://github.com/Lrdcq/xgfe-app-sketch-exporter吧,这篇博客的插件编写思路就是写这玩意儿的思路了。至于这个插件到底怎么回事儿,下一篇博客会作详细说明的。