使用JavaScript开发OS X应用

4 年前

此译文由@张小俊128同学首次发表在前端乱炖。在我准备翻译时,偶然发现了它,也就没有必要重复的造轮子了。谢谢译者和他的分享精神,授权我转发到专栏里。

原译注:随着JavaScript在各个领域的大放异彩,人们越来越多的从JavaScript这门语言身上期待更多的可能性。虽然我们有node-webkit这样的项目可以让我们使用JavaScript语言跨品台开发应用,但是这些Webview套壳的应用并不是”真正的”本地应用。

在不久之前的WWDC 2014中的JavaScript for Automation环节中,JavaScript第一次可以取代Applescript,在最新版本的OS X Yosemite系统中进行程序自动化的任务。而更令人兴奋的是,Objective-C bridge可以允许开发者在JS应用中调用任何的Objective-C框架。例如,如果你想要使用标准OS X控件来创建一个GUI,你可以导入Cocoa框架:

objC.import("Cocoa")

Cocoa这个基础框架的作用非常简单,它为你提供了创建一个OS X应用所需要的各种模块。这个框架包含很多的类和协议,其中包括NSArray,NSURL,NSUserNotification等等。作为一个JavaScript开发者,你可能对这个Objective-C中的类不是很熟悉,但是你完全可以从它们的名字中猜到它们的作用是什么。因为这个类非常的重要,你完全不需要显式的导入这个库,就能够直接使用这些类。这都是默认定义的。

在本文中,我们将使用JavaScript来创建一个简单的应用,你将从中看到,JavaScript的作用可不仅仅是进行程序自动化那么简单,你完全可以使用JavaScript来创建一个Native应用,而无需使用Objective-C或者Swift。

注意:下文中的例子需要运行在Yosemite Developer Preview 7+的系统中。

学习一个新东西最好的做法就是动手去试一试。在本文中我们将使用JavaScript来创建一个简单的应用,它的作用是从你的电脑中选择并展示一张图片。

如果你对完整的代码感兴趣,可以从这个地址来下载到完整的代码。 如上图所示,这个简单的应用由几个部分构成:一个窗口,一个文本栏,一个输入框,以及一个按钮。当然,这几个东西都有它们各自对应的类名:NSWindow,NSTextField,NSTextField以及NSbutton。

点击”选择一张图片”按钮,一个NSOpenPanel将会弹出并允许你选择一个文件。再次我们将配置这个仪表盘来将文件类型限制在.jpg,.png或者.gif。

在选择了一张图片之后,我们将在窗口中展示这张图片。此时,窗口将会自动调整尺寸来使用图片的大小。我们将为窗口设置一个最小宽度和最小高度来确保控制条永远保持呈现状态。

开始我们的项目

首先,我们需要在Application > Utilities中打开Apple Script Editor。虽然这个编辑器并不是很好用,但是我们现在需要它。其中包含一系列的特征来帮助我们创建JS OS X应用。

使用File > New 或者Cmd + n来创建一个新文档。我们需要做的第一件事情是将我们的文档保存为一个应用。使用File > Save 或者 Cmd + S来存储文档。不要马上确认保存。我们还需要在两个地方进行一下配置。

首先,将文件格式选择为”Application”。然后,选中”Stay open after run handler”。如果你没有选择”Stay open after run handler”,你的应用将会在打开之后立刻立刻闪退,这并不符合我们对一个应用的期许。

正式开始写程序了

现在我们添加两行代码并使用Script > Run Application或者opt + cmd + r来运行应用。

objC.import("Cocoa")
$.NSLog("Hi everybody!")

运行之后并没有发生什么。但是你可以看到你的应用的icon出现在了dock中。

你可能会问,”Hi everybody!”到哪里去了?这个$符号究竟是什么,难道是jQuery吗?使用File > Quit 或者cmd + q退出应用,然后打开控制台,你会发现这一条信息被输出到了控制台中。任何应用都可以往控制台中输出信息。在这里,控制台的作用和Chrome等浏览器中的控制台并没有什么很大的区别。主要的区别就是你现在使用它来调试一个本地应用而不是一个网站。

控制台中有很多的信息。但是你可以在右上角的搜索框中输入”applte”来过滤出我们想要的信息。让”applet”保持在搜索框中,并回到Script Editor。使用opt + cmd + r再次运行程序。

看到了吗?”Hi everybody!”此时应该已经显示到了控制台中。如果你没看到,退出之后再次运行你的应用即可。

$符号是什么东西?

使用$符号意味着你可以访问Objective-C bridge。如果你想要使用Objective-C中的类或者常量,你可以使用$.foo或者objC.foo。

创建窗口

现在我们来创建一些能看得到的东西。首先,使用下面的代码更新你的脚本:

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);

完成之后,运行你的应用。你可以看到上吗的这一段代码为我们创建了一个可以移动,最小化,以及关闭的窗口。

如果你之前没有使用过Objective-C或者Cocoa,上面代码中的某些部分可能会让你很费解。其中比较让人头疼的是,方法和类的名称实在是太长了!

再回顾一下上面的代码,你会发现,这就是JavaScript。和你编写一个网站并没有本质的区别。

你可能会问在第一行代码中究竟发生了什么?其中,你使用了style mask来配置这个窗口。每一个样式选项都添加了一些东西:一个标题,一个关闭按钮,一个最小化按钮。其中的”|”操作符的作用是将这些分隔的部分合到了一起。

在这里,有几个你需要记住的有趣的语法。‘$.NSWindow.alloc调用了NSWindow的alloc方法。注意到在alloc之后并没有括号”()”。在JavaScript中,调用一个方法必须要使用括号。但是在OS X的JavaScript中,只有在有参数需要进行传递时,才允许使用括号。如果你在没有参数时使用了括号,那么你将会看到一个报错信息。记住,当发生意想不到的情况时,你都可以从控制台中查找到错误信息。

接下来要说的事情是一个非常非常长的类名:

initWithContentRectStyleMaskBackingDefer

如果你查看一下NSWindow的文档,你会发现一点差别:

initWithContentRect:styleMask:backing:defer:

在Objective-C中,你可以使用下面的代码创建创建同样的窗口:

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

需要注意的是在原生方法中包含冒号”:”。而当你在JS中使用Objective-C中的方法时,所有的冒号都需要删除,并将冒号之后的首字母大写。在其中你还会看到两个方括号”[]”,这表示调用一个对象或者类的方法。[NSWindow alloc]表示调用NSWindow中的alloc方法。对于JS来说,我们使用点和括号来调用方法,例如NSWindow.alloc。

添加控制器

有了窗口之后,我们还需要一个标签,一个文本输入框,一个按钮。在这里,我们需要使用NSTextField和NSbutton来创建这些东西。使用下面的代码更新你的脚本,然后运行应用。

    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);

我们在上面的代码中做了些什么事情?textFieldLabel和textField很类似。它们都是NSTextField的实例。我们像创建窗口一样创建这些东西。其中的initWithFrame和NSMakeRect的作用是创建UI元素。NSRect的作用是根据指定的位置和尺寸创建一个矩形,(x,y,width,height)。这创建了一个在Objective-C中叫做结构体的东西。在JavaScript中我们把它叫做一个对象或者哈希或者字典。它其中包含键和值。

在创建完文本区域之后我们还对其属性进行了一系列设置。Cocoa中没有像是html中label元素一样的东西。因此我们需要通过禁止编辑和添加背景色来创建我们自己的label元素。

对于按钮来说我们使用NSButton来创建。和创建文本区域一样,我们首先需要绘制一个矩形。这里有两个属性需要特别的指出:bezeStyle和buttonType。这两个值都是常量。这些属性控制着按钮渲染的方式和样式。

我们在上面的代码中做到最后一件事情是使用addSubview来为窗口添加新元素。

让按钮动起来

当我们点击按钮来选择一张图片时,我们想要打开一个仪表盘来显示我们电脑中的文件。在完成这件事之前,先让我们来做一点简单的工作:在点击按钮时将一条信息输出到控制台中。

在JavaScript中我们会在元素上绑定一个点击事件处理函数。而在Objective-C中则不太一样。在这里我们需要记住一个叫做消息传递的概念。你需要为一个对象传递一个包含方法名的消息。这个对象在接收到一条消息的时候需要知道它自己应该做些什么。

我们首先要做的事情是设置这个按钮的target和action。target是我们想要发送action的对象。如果现在你还是一头雾水,注意看下面的代码。你需要将关于按钮的代码修改为下面的样子:

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

appDelegate和btnClickHandler到目前为止并不存在。我们需要创建它们。代码如下所示:

ObjC.import(”Cocoa”);

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

var appDelegate = $.AppDelegate.alloc.init;
// 新的部分结束

// 下面的代码是已经存在的部分
var textFieldLabel = $.NSTextField.alloc.initWithFrame($.NSMakeRect(25, (windowHeight - 40), 200, 24));
textFieldLabel.stringValue = "Image: (jpg, png, or gif)";
...

运行应用,点击按钮,然后查看控制台中的信息。如果你没有看到”Clicked!”信息,你需要检查一下错误信息并进行相应的修改。

选择并展示图片

接下来,我们需要编写打开仪表盘,选择图片,并展示图片的代码。你需要将btnClickHandler的代码修改为如下所示:

...
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的实例。如果你曾经选择过一个文件,或者存储过一个文件你肯定已经见过一个仪表盘是什么样的了。

因为我们只想让用户选择图片文件。因此我们需要指定allowedFileTypes。我们需要为它传递一个NSArray类型的值。在这里,我们使用allowedTypes创建一个JS数组,但是依然需要将它转换为一个NSArray。我们使用$(allowedTypes)进行转换。这是使用Objective-C bridge的另一种方式。我们使用这种方法将一个JS值转换为一个Objective-C值。如果需要将一个Objective-C转换为JS,你需要使用$(Objective).js。

另一个需要注意的事情是panel.URLs。在JS中,我们通过array[0]获取数组的第一个值。由于URLs是一个NSArray类型的值,因此我们不能使用方括号。在这里我们需要使用objectAtIndex方法。

一旦我们获得了图片的URL,我们就可以创建一个新的NSImage。由于根据一个文件URL创建一张图片是一件常做的事情,这里有一个非常方便的方法:

initByReferencingFile

在这里,我们像创建其他UI元素一样创建了一个NSImageView。并使用imgView来展示图片。

因为我们想要根据图片的高度和宽度来改变窗口的尺寸。同时也需要设置一个最小的高度和最小的宽度。我们使用setFrameDisplay来改变窗口的尺寸。

最后一点工作

到目前为止,我们的应用已经差不多完成了。你完全可以像运行其他应用一样双击应用图标来运用应用。如果你想要修改应用的默认图标,可以修改/Contents/Resources/applet.icns来完成。

原文:BUILDING OS X APPS WITH JAVASCRIPT

原译文:使用JavaScript开发OS X应用

0
推荐阅读