使用Javascript开发OS X应用程序

译者按:Javascript快要统一所有端了呢,骚年们快去创造奇迹吧~

本文译自tylergaw.com

OS X Yosemite为Automation引入了Javascript的支持。这让在原生OS X框架中使用Javascript成为可能。过去一段时间我一直在探索这个新领域,并收集了一些示例。在这篇文章里我想解释一下基本用法并且搭建一个小的示例应用

WWDC 2014包含一个Javascript for Automation的 session。这个session讲明了在automate应用里可以用Javascript代替AppleScript来进行开发。这本身就是一个激 动人心的消息。使用AppleScript来自动化重复性任务已经存在很长时间了。AppleScript写起来并不是特别舒服,所以能够使用类似的语法 来代替还是很受欢迎的。

在session里演讲者还解释了Objective-C bridge,这让整个事情开始变得特别酷。bridge允许你导入任何Objective-C框架到JS应用里。比如,如果你想搭建一个使用标准OS X控件的GUI时,你可以导入Cocoa:

ObjC.import("Cocoa");

Foundation框架则和它的名字一样,给OS X应用提供基础构件。它包含大量的类和接口,比如NSArrayNSURLNSUserNotification等等。可能你对这些类还不是太熟悉, 但从名字你可以猜出它们代表的意思。由于它是如此重要,你无需在开发中导入它,因为它已经默认包含在里面了。

从我目前了解的情况看,任何能用Objective-C和Swift开发的程序,你都能用Javascript来开发。

开发一个示例程序

注意:你需要Yosemite Developer Preview 7+来保证示例能够正常运行。

了解Javascript for Automation的最好办法是投入进去,并开始用它来开发一些东西。这里我将演示如何创建一个小程序,能让你从电脑中选择一张图片并显示。

可以从我的 示例程序仓库里下载完整的代码。

post-image-jsosx-example-01

我们创建的应用的一张截图

这个程序包括一个窗口,文字标签,文字输入框,以及按钮。对应的类名则是:NSWindowNSTextFieldNSTextField以及NSButton

点击Choose an Image按钮将显示一个NSOpenPanel来让你选择文件。我们将设置选择面板限制它只能从.jpg/.png/.gif里挑选文件。

在选中一张图片后,我们将在窗口显示它。窗口会自动调整大小,以匹配图片的尺寸以及空间的高度。我们还将设置窗口的最小尺寸以确保控件能够完整显示。

项目设置

打开位于Application > Utilities中的Apple Script Editor程序。它不是我用过的最好的编辑器,但对于现在是必要的,搭建JS OS X程序需要使用它的一些特性。我不太清楚它背后的原理,但它可以像程序一样编译和运行你的脚本。它还会创建一些我们需要的额外的文件比如 Info.plist,我估计其他一些编辑器也有这些功能,不过我还没来得及测试它们。

打开菜单的File > New或者快捷键cmd + n来创建新文档。我们需要做的第一件事就是将文档以程序的格式保存,打开菜单File > Save或快捷键cmd + s来打开保存对话框。不要立即确认保存,这里有两个选项是我们需要设置的。

post-image-jsosx-save

Script Editor保存对话框,重要的地方已用红色标出

将文件格式改为“应用程序”,然后选中“Stay open after run handler”选项。你也可以在之后来更新这些保存选项:打开File菜单并按住Option键,它会出现“Save as…”选项,这里你可以进行上面的操作。

如果你不选中“Stay open after run handler”,你的程序会打开,但会一闪而过,然后关掉。

让我们开始coding

在你的脚本里添加下面两行代码,然后打开菜单Script > Run Application或快捷键opt + cmd + r来运行程序。

ObjC.import("Cocoa");
$.NSLog("Hi everybody!");

可以看到菜单栏和dock上的图标,说明我们的程序是在运行的,但除了这个,没有其他变化。

代码中的“Hi everybody!”哪去了?还有那个“$”符号,这是jQuery吗?打开菜单File > Quit或快捷键cmd + q关闭程序,我们来看看NSLog跑哪去了。

通 过系统菜单Applications > Utilities > Console打开Console程序。所有程序能够在控制台里的输出信息。这个控制台和Chrome、Firefox以及Safari里的开发者工具差不多。主要区别是这个是用来调试程序而不是网站的。

控制台充斥着许多信息,你可以在搜索框里输入applet来过滤你所需要的。现在你需要回到Script Editor重新运行程序,推荐使用快捷键opt + cmd +r

post-image-jsosx-console

看到没有?我们的“Hi, everybody!”已经显示在控制台里了。

“$”代表什么意思?

“$”表示的是连接到Objective-C bridge。需要导入Objective-C类或者常量,就用$.fooObjc.foo。后面我还会讲到很多其他使用到$的地方。

Console程序和NSLog是调试程序过程中不可或缺的工具。如果你需要输出字符串以外的东西,这里有我的NSLog示例

创建窗口

让我们来创造一些我们能看见并能与之交互的东西。将下面的代码替换之前脚本中的代码:

ObjC.import("Cocoa");

var styleMask = $.NSTitledWindowMask | $.NSClosableWindowMask | $.NSMiniaturizableWindowMask;
var windowHeight = 85;
var windowWidth = 600;
var ctrlsHeight = 80;
var minWidth = 400;
var minHeight = 340;
var window = $.NSWindow.alloc.initWithContentRectStyleMaskBackingDefer(
  $.NSMakeRect(0, 0, windowWidth, windowHeight),
  styleMask,
  $.NSBackingStoreBuffered,
  false
);

window.center;
window.title = "Choose and Display Image";
window.makeKeyAndOrderFront(window);

保存代码后,使用快捷键opt + cmd + r运行程序。看看我们的成果!仅仅几行代码,我们就可以创建一个程序,拥有打开一个窗口并移动、最小化和关闭它的功能。

post-image-jsosx-basic-window

使用JS创建的一个基本的NSWindow

如果你之前从未使用过Objective-C或Cocoa来写程序,上面的代码可能看起来有点混乱。对我来说,代码里的方法名简直太长了,我喜欢描述性的命名,但Cocoa把它推向了一个极端。

忽略这一段,你会发现它仅仅是Javascript,它和你搭建网站时用的代码没有任何不同。

第一行代码里的styleMask表示,你使用它们来设置窗口样式,每一个样式选项代表这它所添加的:标题、关闭按钮、最小化按钮,这些选项是不变的。然后使用“|”将它们隔开。

你可以在苹果官方文档里获取更多的样式选择。NSRsizebleWindowMask是其中一个你会用到的。试试将它添加到style mask里看看会它有什么用处。

关 于语法有一些东西是你必须记住的。$.NSWindow.alloc调用NSWindowalloc方法,注意在alloc的后面没有“()”。在JS里面这种方法是用来调用属性而不是方法的。在JS for OS X里圆括号只有在你向方法传入参数才使用。如果没有带参数,你会获得一个运行时错误。如果事情和你预期不一致,注意检查Console里的错误。

接下来就是超级长的方法名了。

initWithContentRectStyleMaskBackingDefer

查看NSWindow文档里关于这个方法的部分你会发现它有些不同。

initWithContentRect:styleMask:backing:defer:

在Objective-C里你需要用下面的代码来创建同样的窗口。

NSWindow* window [[NSWindow alloc]
  initWithContentRect: NSMakeRect(0, 0, windowWidth, windowHeight)
  styleMask: styleMask,
  backing: NSBackingStoreBuffered
  defer: NO];

当你将Objective-C方法转换到JS里的时候,你需要去掉冒号并将其后的字母改成大写。而被“[]”包围的则叫做类或者对象的方法。[NSWindow alloc]调用NSWindowalloc方法。转换到JS的时候去掉方括号并且用“.”将它们连接起来。

我认为代码的其他部分应该够直观的,这里就不细讲了。要弄清楚每一行代码的意义,你可以去看官方文档。只要你成功显示了窗口说明目前为止你做的还不错,让我们更进一步。

添加控件

在窗口里我们还需要一个标签、文本域,以及一个按钮。我们将使用NSTextFieldNSButton来显示它们。将你的代码更新成下面那样,然后运行程序。

ObjC.import("Cocoa");

var styleMask = $.NSTitledWindowMask | $.NSClosableWindowMask | $.NSMiniaturizableWindowMask;
var windowHeight = 85;
var windowWidth = 600;
var ctrlsHeight = 80;
var minWidth = 400;
var minHeight = 340;
var window = $.NSWindow.alloc.initWithContentRectStyleMaskBackingDefer(
  $.NSMakeRect(0, 0, windowWidth, windowHeight),
  styleMask,
  $.NSBackingStoreBuffered,
  false
);

var textFieldLabel = $.NSTextField.alloc.initWithFrame($.NSMakeRect(25, (windowHeight - 40), 200, 24));
textFieldLabel.stringValue = "Image: (jpg, png, or gif)";
textFieldLabel.drawsBackground = false;
textFieldLabel.editable = false;
textFieldLabel.bezeled = false;
textFieldLabel.selectable = true;

var textField = $.NSTextField.alloc.initWithFrame($.NSMakeRect(25, (windowHeight - 60), 205, 24));
textField.editable = false;

var btn = $.NSButton.alloc.initWithFrame($.NSMakeRect(230, (windowHeight - 62), 150, 25));
btn.title = "Choose an Image...";
btn.bezelStyle = $.NSRoundedBezelStyle;
btn.buttonType = $.NSMomentaryLightButton;

window.contentView.addSubview(textFieldLabel);
window.contentView.addSubview(textField);
window.contentView.addSubview(btn);

window.center;
window.title = "Choose and Display Image";
window.makeKeyAndOrderFront(window);

如果一切正确的话你现在会得到有控件的窗口。你不能在文本域里输入,点击按钮也不会发生任何事情。不过我们接下来会讲到那里。

post-image-jsosx-controls

带有标签、域和按钮元素的窗口

新添加的代码里有什么呢?textFieldLabeltextField类似,都是NSTextField的实例。它们的使用方法和创建窗口的代码差不多。initWithFrameNSMakeRect则是脚本创建UI元素的方法,它们的作用正如字面意思。

在创建文本域之后我们还需要给它们设置一些属性。Cocoa没有和html标签元素一样的东西,但我们可以通过设置属性来达到同一效果。

我们需要让程序自动写入到文本域,所以禁止了在里面输入文字。如果不需要这么做,你可以用一行代码来创建一个标准的文本域。

对于按钮我们则使用NSButton,和文本域一样,创建它需要绘制一个长方形,需要的属性是bezelStylebuttonType,两个的值都是常量。这两个属性控制如何渲染按钮以及它的样式。查看NSButton文档来看你能用它做什么。我同样创建了一个示例程序来显示不同类型和样式的按钮。

代 码里我们做的最后一件事就是,使用addSubview将元素添加到窗口。当我第一次添加的时候我使用 window.addSubview(theView),这对于其它使用NSView创建的标准view是适用的,但不适合NSWindow的实例。我不太清楚为什么,但对于窗口你需要添加子视图到contentView,文档里描述contentView说“窗口的视图层级里最高可访问性的NSView对象”,测试可行。

让按钮响应点击

当点击按钮的时候,我希望得到一个选择文件的面板。在在此之前,让我们先热下身,尝试当点击按钮时输出一条信息到Console里。

在Javascript里你可以绑定事件监听器到元素上以处理点击。Objective-C没有类似的概念。它使用消息传递的方法,你发送包含方法的信息到一个对象,对象需要知道接收信息后如何处理。

我们需要做的第一件事是给按钮添加targetactiontarget是我们想将action发送到的对象名称。在代码的按钮设置区域添加下面的属性:

...
btn.target = appDelegate;
btn.action = "btnClickHandler";
...

appDelegatebtnClickHandler现在还并不存在,我们需要来创建它们。下面的代码需要顺序。我在代码里标记了应该在哪里添加新代码。

ObjC.import("Cocoa");

// New stuff
ObjC.registerSubclass({
  name: "AppDelegate",
  methods: {
    "btnClickHandler": {
      types: ["void", ["id"]],
      implementation: function (sender) {
        $.NSLog("Clicked!");
      }
    }
  }
});

var appDelegate = $.AppDelegate.alloc.init;
// end of new stuff

// Below here is in place already
var textFieldLabel = $.NSTextField.alloc.initWithFrame($.NSMakeRect(25, (windowHeight - 40), 200, 24));
textFieldLabel.stringValue = "Image: (jpg, png, or gif)";
...

运行应用,点击Choose Image按钮,然后观察Console的输出。如果看到了“Clicked!”,说明添加成功了。如果不是的话,仔细检查你的脚本是否和上面一样,然后观察Console里输出的错误。

Subclassing

这个Objc.registerSubclass是什么呢?Subclassing是创建继承Objective-C中类的子类的方法。注意:这里的术语是我自创的,不要在意这些细节。registerSubclass接收一个参数:包含新对象成员的JS对象。这些成员包括:namesuperclassprotocolspropertiesmethods

这些都是很好的,但我们需要怎么做呢?鉴于我们没有提供superclass,我们继承了NSObject。它是大多数Objective-C类的根类。然后设置一个名称以便待会我们引用它。

$.AppDelegate.alloc.init:创建一个AppDelete类的实例。注意我们没有使用圆括号,因为这里它们不需要接受参数。

Subclass方法

你可以给方法定义任何可接受的名称。在这里我们用btnClickHandler。给它一个带有typesimplementation成员的对象。我没有在官方文档里找到types数组需要包含的元素,通过尝试我发现它是这样的:

["return type", ["arg 1 type", "arg 2 type",...]]

btnClickHandler不会返回任何值,所以我们将type设为void,它接收一个参数,发送对象。在这里将是被命名为btnNSButton。我们使用“id”类型来指代任何对象。

type全部的类型列表可以查看release notes

implementation是一个普通的函数。在里面你可以直接写Javascript。和$ bridge一样,类的内部代码对外部有同样的可访问性,比如你在类外部设置的变量。

使用接口

你能用subclass来实现已有的Cocoa接口,但这里有一个陷阱。我发现如果你使用protocols数组你的脚本将会停止响应并且没有错误日志。如果你陷入了同样的状况,我写了一个示例和解释以供查看。

选择并且显示图片

现在我们准备好打开面板,选择图片,并且在窗口显示它了。使用下面的代码替换btnClickHandlerimplementation函数:

...implementation: function (sender) {
  var panel = $.NSOpenPanel.openPanel;
  panel.title = "Choose an Image";

  var allowedTypes = ["jpg", "png", "gif"];
  // NOTE: We bridge the JS array to an NSArray here.  panel.allowedFileTypes = $(allowedTypes);

  if (panel.runModal == $.NSOKButton) {
    // NOTE: panel.URLs is an NSArray not a JS array    var imagePath = panel.URLs.objectAtIndex(0).path;
    textField.stringValue = imagePath;

    var img = $.NSImage.alloc.initByReferencingFile(imagePath);
    var imgView = $.NSImageView.alloc.initWithFrame(
    $.NSMakeRect(0, windowHeight, img.size.width, img.size.height));

    window.setFrameDisplay(
      $.NSMakeRect(
        0, 0,
        (img.size.width > minWidth) ? img.size.width : minWidth,
        ((img.size.height > minHeight) ? img.size.height : minHeight) + ctrlsHeight      ),
      true
    );

    imgView.setImage(img);
    window.contentView.addSubview(imgView);
    window.center;
  }}

我们做的第一件事是创建了一个NSOpenPanel的示例,这里的panel面板就是选择文件和保存文件时打开的窗口。

我 们只想让程序打开图片。设置allowedFileTypes可以让我们限定文件类型。它需要接受一个NSArray数组。我们创建了一个包含 allowedTypes的JS数组,但还需要将它转换到NSArray。使用$(allowedTypes)可以完成转换。这是bridge的另一种用法。通过这种方法我们可以将JS传到Objective-C里,如果要传送Objective-C到JS你可以用$(ObjcThing).js

打开面板使用的是panel.runModal。这将暂停代码的执行。当你点击CancelOpen,面板会返回一个值。如果点击的是Open,将会返回$.NSOKButton的值。

另一个关键是panel.URLs。在JS里,我们读取数组的第一个值的方法是array[0],因为URLs是一个NSArray,所以不能使用方括号,而用objectAtIndex方法来代替。它们的结果是一样的。

当我们获取到一个图片的URL的时候我们可以创建一个新的NSImage对象。由于从文件URL获取图片非常普遍,所以为此了定义了一个方便的方法:

initByReferencingFile

我们创建了一个NSImageView,使用的方法和创建其他UI元素一样。imgView负责显示图片。

我们还想让窗口的尺寸匹配图片的尺寸,同时我们还需要确定一个最小的尺寸来显示控件。我们使用setFrameDisplay来改变窗口的大小。

然后我们通过设置Image view图片并将其添加到窗口,因为它的尺寸已经变了我们需要重新定位窗口。

到这里程序已经完成了。你可以粗暴的测试它,打开一堆图片,gif动图也是支持的,所以别忘了打开一些。

花絮

目前为止,我们一直从Script Editor来运行程序,但双击打开程序是同样的支持的。

post-image-jsosx-appicon

双击图标以打开程序

你可以通过替换/Contents/Resources/applet.icns来更新程序图标,右击程序图标并选择“Show Package Contents”来查看程序资源。

为什么我感到激动

我 很激动是因为我认为它有很大的潜力。这里是我的一些想法:一旦Yosemite正式发布,任何安装了这个系统的人都将能使用最流行的程序语言来编写原生的程序。他们不需要下载或安装任何其他东西(比如J*va的运行时)就能运行这些程序。你甚至没有必要下载Xcode。这将大大降低参与的门槛,未来将是难以置信的。

我知道与运行一段脚本相比,还有很多种创建OS X程序的方法。我也不会幻想Javascript编写的程序成为Mac上的首选。但我认为这将允许程序员开发一些让自己和其他人工作更轻松的小程序。如果一个团队成员不习惯使用命令行,可以快速为他创建一个GUI界面;需要快速的、可视化的方法来创建并更新巨大的配置文件?来写一个小程序吧。

其实其他的语言也有这样的能力,Python和Ruby同样也能读取相同的API,以及编写小程序。但能够使用Javascript让人感觉到事情不一样了。它是颠覆性的,它就像是充满创造力的Web敲响了桌面的大门。苹果敞开了大门,无需邀请,我将自己进去。

EOF
  • 文章信息
  • 评论交流
2014年09月30日 4:16 发布。字数:9954
本文被收录于下面的话题: ,