利用JavaScript構建OSX應用

轉:[譯]利用js構建osx應用
英文原文地址javascript

OSX Yosemite引入了js來建立Automation,這使得javascript能夠訪問nativeOSX類庫,我已經深刻研究這塊而且編寫了一些examples,今天這篇文章會講解一些基礎東西而且一步步的來建立一個小的example app.html

WWDC 2014上面有一個JavaScript for Automation主題,專門解釋用javascript來代替applescript建立自動化應用程序,這是個很是激動的事情,使用applescript來構建自動化任務已經存在很長時間了,它的一些語法一直都不是很受歡迎.java

在這個主題上,主持人講解了object-c bridge,這個是很是酷的東西,能夠往javascript中導入任何底層的方法,例如,如用你想用標準的OS X控件來建立gui應用的話,你將要引入cocoa:python

ObjC.import('Cocoa');

Foundation框架就跟它名字同樣,提供一些基礎模塊給osx app,它包含不少類以及接口,好比NSArray,NSURL,NSUserNotification等等,也許你對這些類不熟悉,可是根據字面意思大概就能知道它們的功能,這些類比較經常使用,因此能夠直接在app中使用而不須要單獨導入它.它是默認就會加載的jquery

我能夠這麼跟你說,只要是用objective-c或者swift建立的app,用javascript均可以作出來.git

build and example app

注意:你的操做系統要在yosemite開發版7+,下面的例子才能夠正常運行github

瞭解它的最好方法就是實踐,下面咱們將要實現一個簡單的app(從你電腦上選取一個圖片並顯示它),效果如圖web

clipboard.png

制定一個app.js,包括一個窗口,一個文本標籤,一個輸入框和一個按鈕,對應的class名稱就是NSWindow,NSTextField,NSTextFieldNSButton.objective-c

單擊選擇圖片文件按鈕,將會顯示一個NSOpenPanel,它將會顯示一個文件選擇窗口,咱們還須要配置這個窗口的文件過濾屬性,只能選擇.jpg,.png或者.gifchrome

當選擇一個圖片以後,將會在窗口中顯示出來,而且自適應圖片大小,窗口設置了一個最小的寬高以避免圖片被裁剪

建立一個project

打開Apple Script Editor應用程序,定位到Applications > Utilities,Script Editor並非最好用的編輯器,可是如今有必要用它,它提供了不少的特性用來建立JS OSX APP,而且能夠用來編譯和運行你的js osx app,它也能夠添加些擴展文件像咱們app須要的info.plist文件.我猜也許還有別的編輯器能夠作一樣的事情,不過目前我尚未找到.

建立一個新的文檔,定位到File->New或者使用命令cmd + n,首先要作的就是保存它爲Application,定位到File->Save或者使用命令cmd + s,確認保存以前,有兩個選項須要注意下,看下圖:

clipboard.png

文件格式選擇Application,選中Stay open after run handler

若是沒有選中Stay open after run handler,打開應用以後會一閃而過,而後自動關閉,這些網上都沒有什麼教程,只是本身幾個小時摸索出來的.

如今應該是作些有意義的時候了

添加下面兩行代碼到你的編輯器中,而後運行它,定位到Script->Run Application或者opt + cmd + r.

Objc.import('Cocoa');
$.NSLog('hello feenan!');

這時候運行應用什麼都沒有發生,惟一看到改變的就是全局菜單欄以及dock,由於應用名稱以及file,edit並排在菜單欄上,應用圖片顯示在dock上,這些都表明着應用已經在運行.

那麼hello feenan!跑哪去了呢?$符號是什麼,是jquery?,咱們先把應用程序退出了,定位到File->Quit或者cmd + q,而後咱們來找找NSLog輸出的內容.

打開Console app,定位到Applications > Utilities > Console,任何一個應用程序均可以記錄日誌到console app中,它跟chrome,firefox,safari中的控制檯沒多大區別,主要的區別在於你可使用它來調試應用程序代替website

console app中有不少日誌信息,你能夠在右上角的輸入框中輸入applet來過濾日誌,輸入applet到過濾框中以後,回到Script Editor中,再次運行應用程序,使用opt + cmd + r命令,控制檯信息以下圖

clipboard.png

你是否是看到在控制檯中顯示hello feenan!了,若是沒有的話,退出應用程序再次運行看看,有時候咱們忘記退出應用程序,代碼並無再次運行.

$符號是什麼呢?

$讓你可以訪問Objective-C bridge.任什麼時候候你須要訪問Objective-C其中某個類或者常量,你均可能使用$.foo或者ObjC.foo,後面還有講到關於使用$的其它一些方法.

Console appNSLog是必不可少的工具,你將會不停的使用它們來調試你的應用程序,想要了解更多的信息,能夠點擊NSLog example

建立一個窗口

讓咱們建立一個能夠顯示而且有交互的窗口,代碼看起來像下面這樣

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,而後咱們能夠說,只須要這麼點代碼就能夠啓動一個app而且打開一個窗口,並且咱們能夠移動,最小化和關閉它.

clipboard.png

若是你跟我同樣沒有用Objective-C或者Cocoa來建立一個app的話,這些能夠看起來有點難以理解,對我來講,這些方法名稱的長度有點難以接受,雖然我喜歡描述性的方法名稱,可是像Cocoa這樣的仍是太極端了.

看上面的代碼其實就是javascript,就跟你編寫網站代碼同樣.

第一行中的styleMask是幹麼的呢?它提供了一些對窗口屬性設置的功能,有標題,關閉按鈕,最小化按鈕,這些選項都是常量,經過|符號來添加多個,|符號是C裏的or操做符,不用去理解它的原理,只要知道它是用來並列多個選項的就足夠了.

這裏有不少種樣式信息,想了解詳情的能夠點擊the docs,NSResizableWindowMask將是接下來要使用的一個樣式,能夠添加到上面的代碼中看看效果

這裏還有一些有趣的語法須要你記住,$.NSWindow.alloc調用NSWindow裏的alloc方法,可是並無在後面插入(),這種調用方式就跟js裏獲取屬性同樣,可是js裏調用方法不是這樣的,原來,在OSX JS中,假如方法沒有參數的時候,是不能在後面加入()的,不然它會出現運行時錯誤.因此之後當你發現有些事件並無像你指望中發生時就要檢查下console裏的輸出內容了.

下一個要關注的事情是這個超長的方法:

initWithContentRectStyleMaskBackingDefer

讓咱們來看看NSWindowdoc上講解這個方法的,你會發現有一點不一樣:

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的話,須要在二者以前添加一個.,像NSWindow.alloc這樣

我認爲剩下的代碼足夠用來描述建立一個窗口並顯示它,我跳過了不少關於這方面的細節說明,這個須要不少時間用來閱讀相關文檔,不過你能夠這些.當你作到顯示出一個窗口來已經不錯了,讓咱們作更多的事情吧

添加控件

窗口裏還須要一個標籤,一個輸入框,一個按鈕,我使用NSTextFieldNSButton來建立它,輸入下面的代碼而後運行你的app

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

若是應用跑起來沒問題的話,將會看到下面的效果,你能夠在輸入框中輸入內容,按鈕點擊沒有任何效果,不過咱們後面會加些東西上去

看到上面的代碼是否是對添加有些疑惑,到底怎麼實現的呢?textFieldLabeltextField很是類似,它們都是NSTextField的實例,在這裏都是經過類似的方法實現的,當你看到initWithFrameNSMakeRect時,它們是建立ui元素的好方法,NSMakeRect就跟它的名字同樣,用來建立有必定大小的方形用給定的位置信息(x, y, width, height).這就至關於建立了Objective-c中的結構體信息,在js中咱們引用它爲一個對象或者hash,或者一個dict,擁有本身的鍵值對.

建立完輸入框這後,給它賦一些屬性,Cocoa並無單首創建標籤的方法,因此這裏咱們利用NSTextField,禁用它的編輯屬性而且設置的背景樣式來模擬標籤效果.

假如是正常的輸入框的話,其實只須要設置一行代碼就能夠了

對於按鈕咱們使用NSButton類,跟輸入框同樣,建立它也須要先畫一個矩形,不過這裏有兩個屬性須要強調下:bezelStylebuttonType.它們的值就是常量,主要用來控制按鈕的渲染以及有什麼樣式信息,想了解更多的信息,能夠點擊docs,我也有一些關於不一樣樣式的按鈕例子,example app

最後的事情就是經過addSubview把這些控件添加到咱們的window中去,剛開始我調用window.addSubview(theView),可是並沒添加起來,最後發現只能添加其它用NSView建立的實例,我不知道爲何會這樣,可是想要添加NSWindow建立的實例的話,只能調用window.contentView.addSubview(theView).官方文檔上是這樣描述的,NSView對象是窗口裏最高的訪問層次.

給按鈕添加代碼

當點擊按鈕的時候,我想顯示一個面板出來,上面列出本地的文件信息,作這些以前,讓咱們先添加一個日誌信息熱熱身.

javascript中添加事件通常是給對象添加監聽,可是在Objective-C中沒有這樣的概念,在它這裏叫消息通信,你得發送一個包含方法名的消息給目標對象,目標對象收到這個包含方法名的消息以後才能決定作什麼,也許我說的不是很準確,可是大概就是這個意思

首先咱們要作的就是添加一個target和一個action,target是發送給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)";
...

運行app,而後在console中查看是否有顯示Clicked!,若是顯示了,說明代碼沒問題,不然檢查下代碼跟文中是否有區別,而後仔細看下console中的錯誤信息.

Subclassing(子類)

ObjC.registerSubclass是幹什麼的呢?Subclassing是建立一個子類的方法,它能夠繼承一個父類.也許這個名稱叫的不專業,還請多多包含.registerSubclass須要一個參數,它是一個對象,成員能夠包含name,superclass,protocols,properties,methods,我不敢保證這裏列出了全部的成員,不過你能夠看release notes.

一切看起來挺不錯的,可是上面的代碼作了什麼呢?由於並無寫superclass屬性,因此默認繼承NSObject,它是全部Objective-c裏的基類, 設置name屬性方便後面咱們用$或者Objc來引用它.

$.AppDelegate.alloc.init會建立一個AppDelegate類實例,須要再次注意的是,allocinit後面並無插入(),由於咱們並無傳任何參數.

Subclass methods(子類方法)

能夠經過一個字符串內容來建立一個方法,好比上面的btnClickHandler,而後給它提供一個對象參數,成員包括typesimplementation,官方文檔上並無說明types數組應該包括什麼,可是通過我不斷嘗試感受它的參數說明應該是這樣的:

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

btnClickHandler沒返回任務東西因此設置return typevoid,它須要一個參數,就是發送的對象,因此這裏設置參數名爲id,它能夠表示任何對象.

想查看整個類型的列表信息,能夠點擊release notes

implementation是一個普通的函數,在這裏面能夠寫javascript,同時能夠訪問$符號以及外面定義的變量.

使用protocols的問題

在子類中能夠實現Cocoa protocols,可是我發如今你腳本中使用protocols數組,應用程序會中止並且沒有任何錯誤,我寫了一些example and explanation來講明這些問題,有興趣的能夠看一看.

Choosing and displaying images(選擇並顯示圖片)

咱們準備打開一個面板,選擇一個圖片,而後顯示它,更新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一個實例,若是你歷來沒有打開過文件或者保存操做,那麼默認顯示的面板是活動文件列表.

我只想打開圖片文件,因此須要過濾文件列表,設置allowedFileTypes屬性,它是一個NSArray類型,咱們建立一個js數組allowedTypes來給它賦值,可是咱們須要轉換成NSArray類型,經過$(allowedTypes),這是bridge橋的另一種用法,能夠經過這個方法在jsObjective-c之間進行類型轉換,想轉換Objective-c類型爲js中對應的類型的話,使用$(ObjCThing).js方法.

打開面板經過panel.runModal方法,這個代碼會立刻執行,你能夠點擊取消肯定,而後咱們能夠經過返回值來判斷你點擊是哪一個,當點擊肯定返回的是$.NSOKButton.

另外一個須要注意的是panel.URLs,一般咱們訪問js中的數組元素是經過[],由於URLSNSArray類型,因此不能經過[]來訪問,這裏提供了objectAtIndex方法來訪問,它跟[]的效果是同樣的.

只要咱們獲取到圖片的URL,那麼咱們就能夠建立一個NSImage實例,這裏有一個專門的方法

initByReferencingFile

跟建立其它ui元素同樣, 這裏咱們建立一個NSImageView實例來顯示圖片

咱們想窗口的大小可以自適應圖片的大小,可是不能低於最小寬高,爲了設置窗口的大小,這裏調用setFrameDisplay方法

咱們經過圖像視圖來包裝圖片,而後把它添加到window視圖中,由於窗口大小改變了,因此須要從新計算居中顯示.

Tidbits(花絮)

迄今爲止,咱們已經在Script Editor中建立了一個app並用命令opt + cmd + d運行它,也能夠像其它app同樣,雙擊它的應用圖標來運行.

clipboard.png

你也能夠修改app圖標的路徑,替換/Contents/Resources/applet.icns就行,想訪問應用的資源文件,能夠右鍵app選擇Show Package Contents就行.

WHY I’M EXCITED(爲何我這麼激動?)

我對它的潛能感到很是激動,下面是我已經想到的一點觀點.當Yosemite正式發佈以後,不少人能夠坐下來開發原生app了,使用最普通的編程語言,並且不用下載或者安裝別的東西,假如你想的話連xcode也能夠不用安裝,徹底下降了進入app開發的門檻,這簡直太瘋狂了.

我知道有不少大型應用程序經過腳本是辦不到的,我也沒有說腳本是建立app的惟一方式,可是咱們可讓一些人建立一些小的app爲他本身或者別人.當一個團隊成天在命令行下工做的不舒服時,咱們能夠爲它們建立一個gui程序;當須要快速,可視化的建立或者修改配置文件時,也能夠爲它建立一個小的app.

固然也有其它編程語言能夠辦到,像pythonruby也能夠訪問到低層api,而後建立app.只是利用javascript來建立app顯的更不同凡響,這簡直有點顛覆咱們的思想.這感受就像一些網站敲響了桌面上的門,apple讓這個門解鎖了,我徹底被它吸引了.

相關文章
相關標籤/搜索