轉:[譯]利用js構建osx應用
英文原文地址javascript
OSX Yosemite
引入了js
來建立Automation
,這使得javascript
能夠訪問native
OSX類庫,我已經深刻研究這塊而且編寫了一些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
注意:你的操做系統要在
yosemite
開發版7+,下面的例子才能夠正常運行github
瞭解它的最好方法就是實踐,下面咱們將要實現一個簡單的app(從你電腦上選取一個圖片並顯示它),效果如圖web
制定一個app.js
,包括一個窗口,一個文本標籤,一個輸入框和一個按鈕,對應的class
名稱就是NSWindow
,NSTextField
,NSTextField
和NSButton
.objective-c
單擊選擇圖片文件按鈕,將會顯示一個NSOpenPanel
,它將會顯示一個文件選擇窗口,咱們還須要配置這個窗口的文件過濾屬性,只能選擇.jpg
,.png
或者.gif
chrome
當選擇一個圖片以後,將會在窗口中顯示出來,而且自適應圖片大小,窗口設置了一個最小的寬高以避免圖片被裁剪
打開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
,確認保存以前,有兩個選項須要注意下,看下圖:
文件格式選擇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
命令,控制檯信息以下圖
你是否是看到在控制檯中顯示hello feenan!
了,若是沒有的話,退出應用程序再次運行看看,有時候咱們忘記退出應用程序,代碼並無再次運行.
$
讓你可以訪問Objective-C bridge
.任什麼時候候你須要訪問Objective-C
其中某個類或者常量,你均可能使用$.foo
或者ObjC.foo
,後面還有講到關於使用$
的其它一些方法.
Console app
和NSLog
是必不可少的工具,你將會不停的使用它們來調試你的應用程序,想要了解更多的信息,能夠點擊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
而且打開一個窗口,並且咱們能夠移動,最小化和關閉它.
若是你跟我同樣沒有用Objective-C
或者Cocoa
來建立一個app的話,這些能夠看起來有點難以理解,對我來講,這些方法名稱的長度有點難以接受,雖然我喜歡描述性的方法名稱,可是像Cocoa
這樣的仍是太極端了.
看上面的代碼其實就是javascript
,就跟你編寫網站代碼同樣.
第一行中的styleMask
是幹麼的呢?它提供了一些對窗口屬性設置的功能,有標題
,關閉按鈕
,最小化按鈕
,這些選項都是常量,經過|
符號來添加多個,|
符號是C
裏的or
操做符,不用去理解它的原理,只要知道它是用來並列多個選項的就足夠了.
這裏有不少種樣式信息,想了解詳情的能夠點擊the docs,NSResizableWindowMask
將是接下來要使用的一個樣式,能夠添加到上面的代碼中看看效果
這裏還有一些有趣的語法須要你記住,$.NSWindow.alloc
調用NSWindow
裏的alloc
方法,可是並無在後面插入()
,這種調用方式就跟js
裏獲取屬性同樣,可是js
裏調用方法不是這樣的,原來,在OSX JS
中,假如方法沒有參數的時候,是不能在後面加入()
的,不然它會出現運行時錯誤.因此之後當你發現有些事件並無像你指望中發生時就要檢查下console
裏的輸出內容了.
下一個要關注的事情是這個超長的方法:
initWithContentRectStyleMaskBackingDefer
讓咱們來看看NSWindow
的doc上講解這個方法的,你會發現有一點不一樣:
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
表示調用NSWindow
的alloc
方法,換成js
的話,須要在二者以前添加一個.
,像NSWindow.alloc
這樣
我認爲剩下的代碼足夠用來描述建立一個窗口並顯示它,我跳過了不少關於這方面的細節說明,這個須要不少時間用來閱讀相關文檔,不過你能夠這些.當你作到顯示出一個窗口來已經不錯了,讓咱們作更多的事情吧
窗口裏還須要一個標籤,一個輸入框,一個按鈕,我使用NSTextField
和NSButton
來建立它,輸入下面的代碼而後運行你的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);
若是應用跑起來沒問題的話,將會看到下面的效果,你能夠在輸入框中輸入內容,按鈕點擊沒有任何效果,不過咱們後面會加些東西上去
看到上面的代碼是否是對添加有些疑惑,到底怎麼實現的呢?textFieldLabel
和textField
很是類似,它們都是NSTextField
的實例,在這裏都是經過類似的方法實現的,當你看到initWithFrame
和NSMakeRect
時,它們是建立ui
元素的好方法,NSMakeRect
就跟它的名字同樣,用來建立有必定大小的方形用給定的位置信息(x, y, width, height)
.這就至關於建立了Objective-c
中的結構體信息,在js
中咱們引用它爲一個對象或者hash
,或者一個dict
,擁有本身的鍵值對.
建立完輸入框這後,給它賦一些屬性,Cocoa
並無單首創建標籤的方法,因此這裏咱們利用NSTextField
,禁用它的編輯屬性而且設置的背景樣式來模擬標籤效果.
假如是正常的輸入框的話,其實只須要設置一行代碼就能夠了
對於按鈕咱們使用NSButton
類,跟輸入框同樣,建立它也須要先畫一個矩形,不過這裏有兩個屬性須要強調下:bezelStyle
和buttonType
.它們的值就是常量,主要用來控制按鈕的渲染以及有什麼樣式信息,想了解更多的信息,能夠點擊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"; ...
appDelegate
和btnClickHandler
還沒存在,因此要先建立它們,在下面的代碼有提供,而後代碼中還增長了註釋用來告訴你把這些新的代碼添加到何處
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
中的錯誤信息.
ObjC.registerSubclass
是幹什麼的呢?Subclassing
是建立一個子類的方法,它能夠繼承一個父類.也許這個名稱叫的不專業,還請多多包含.registerSubclass
須要一個參數,它是一個對象,成員能夠包含name
,superclass
,protocols
,properties
,methods
,我不敢保證這裏列出了全部的成員,不過你能夠看release notes.
一切看起來挺不錯的,可是上面的代碼作了什麼呢?由於並無寫superclass
屬性,因此默認繼承NSObject
,它是全部Objective-c
裏的基類, 設置name
屬性方便後面咱們用$
或者Objc
來引用它.
$.AppDelegate.alloc.init
會建立一個AppDelegate
類實例,須要再次注意的是,alloc
和init
後面並無插入()
,由於咱們並無傳任何參數.
能夠經過一個字符串內容來建立一個方法,好比上面的btnClickHandler
,而後給它提供一個對象參數,成員包括types
和implementation
,官方文檔上並無說明types
數組應該包括什麼,可是通過我不斷嘗試感受它的參數說明應該是這樣的:
["return type", ["arg 1 type", "arg 2 type",...]]
btnClickHandler
沒返回任務東西因此設置return type
爲void
,它須要一個參數,就是發送的對象,因此這裏設置參數名爲id
,它能夠表示任何對象.
想查看整個類型的列表信息,能夠點擊release notes
implementation
是一個普通的函數,在這裏面能夠寫javascript
,同時能夠訪問$
符號以及外面定義的變量.
在子類中能夠實現Cocoa protocols
,可是我發如今你腳本中使用protocols
數組,應用程序會中止並且沒有任何錯誤,我寫了一些example and explanation來講明這些問題,有興趣的能夠看一看.
咱們準備打開一個面板,選擇一個圖片,而後顯示它,更新btnClickHandler
的implementation
函數,代碼以下
... 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
橋的另一種用法,能夠經過這個方法在js
和Objective-c
之間進行類型轉換,想轉換Objective-c
類型爲js
中對應的類型的話,使用$(ObjCThing).js
方法.
打開面板經過panel.runModal
方法,這個代碼會立刻執行,你能夠點擊取消
和肯定
,而後咱們能夠經過返回值來判斷你點擊是哪一個,當點擊肯定返回的是$.NSOKButton
.
另外一個須要注意的是panel.URLs
,一般咱們訪問js
中的數組元素是經過[]
,由於URLS
是NSArray
類型,因此不能經過[]
來訪問,這裏提供了objectAtIndex
方法來訪問,它跟[]
的效果是同樣的.
只要咱們獲取到圖片的URL
,那麼咱們就能夠建立一個NSImage
實例,這裏有一個專門的方法
initByReferencingFile
跟建立其它ui
元素同樣, 這裏咱們建立一個NSImageView
實例來顯示圖片
咱們想窗口的大小可以自適應圖片的大小,可是不能低於最小寬高,爲了設置窗口的大小,這裏調用setFrameDisplay
方法
咱們經過圖像視圖來包裝圖片,而後把它添加到window
視圖中,由於窗口大小改變了,因此須要從新計算居中顯示.
迄今爲止,咱們已經在Script Editor
中建立了一個app並用命令opt + cmd + d
運行它,也能夠像其它app同樣,雙擊它的應用圖標來運行.
你也能夠修改app圖標的路徑,替換/Contents/Resources/applet.icns
就行,想訪問應用的資源文件,能夠右鍵app選擇Show Package Contents
就行.
我對它的潛能感到很是激動,下面是我已經想到的一點觀點.當Yosemite
正式發佈以後,不少人能夠坐下來開發原生app了,使用最普通的編程語言,並且不用下載或者安裝別的東西,假如你想的話連xcode
也能夠不用安裝,徹底下降了進入app
開發的門檻,這簡直太瘋狂
了.
我知道有不少大型應用程序經過腳本
是辦不到的,我也沒有說腳本
是建立app的惟一方式,可是咱們可讓一些人建立一些小的app爲他本身或者別人.當一個團隊成天在命令行下工做的不舒服時,咱們能夠爲它們建立一個gui
程序;當須要快速,可視化的建立或者修改配置文件時,也能夠爲它建立一個小的app.
固然也有其它編程語言能夠辦到,像python
和ruby
也能夠訪問到低層api,而後建立app.只是利用javascript
來建立app顯的更不同凡響,這簡直有點顛覆咱們的思想.這感受就像一些網站敲響了桌面上的門,apple
讓這個門解鎖了,我徹底被它吸引了.