高見龍

iOS app/Ruby/Rails Developer & Instructor, 喜愛非主流的新玩具 :)

在Titanium Studio使用CoffeeScript來寫app

即然CoffeeScript寫出來的東西就是一般的JavaScript,之前就在猜想是不是也可以把CoffeeScript用在Titanium Studio裡,果然,有善心人士用Python寫了給Titanium Studio的外掛。

安裝

首先,你需要在你電腦裡安裝好CoffeeScript的相關工具,詳情可參考這篇

相關的工具安裝完成之後,接著請到 https://github.com/billdawson/ti_coffee_plugin 下載壓縮檔,解壓縮之後把plugin.py這個檔案放到Titanium Studio的plugin資料夾。以我的電腦來看是放在這個資料夾:

/Library/Application Support/Titanium/plugins/ti_coffee_plugin/1.0/plugin.py

如果資料夾不存在的話,就自己手動建吧,檔案放進去之後就算安裝完成了。

使用

在每個Titanium Studio的project底下,應該都可以找到一個”tiapp.xml“的檔案:

image

找到它之後,請在最下面加上這段:

<plugins>
    <plugin version="1.0">ti_coffee_plugin</plugin>
</plugins>

image

接下來,因為我們要建一個app.coffee來產生app.js,所以原來的app.js就可以先丟掉,然後在Resources資料夾裡建一個叫app.coffee的檔案:

image

接著就是打開app.coffee來寫code啦,如果大家懶得寫,可以直接剪貼這段直接貼上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Titanium.UI.backgroundColor = 'white'

win1 = Titanium.UI.createWindow(
  backgroundColor: 'white'
  title: 'CoffeeScript Demo'
)

label1 = Titanium.UI.createLabel(
  color: '#999'
  text: 'Hello CoffeeScript'
  font:
      fontSize:30
      fontFamily: 'Helvetica Neue'
  textAlign: 'center'
  width:'auto'
)

win1.add label1
win1.open()

打完收工,按下build按鈕,耐心等一下畫面..

image

同時你應該也會發現在Resources資料夾裡自動長出了app.js,這個就是神奇的plugin幫我們在build的時候,自動幫我們完成從編譯js檔的工作。

為什麼好好的JavaScript要改用CoffeeScript來寫?

大家先看看程式碼的畫面:

image

主要是CoffeeScript的語法簡單、清楚很多,例如像是物件可以用YAML的方式來寫。所以像label1裡的參數本來是一層包一層,可以改用更清楚的YAML式的寫法(不過這個多少有個人觀感,說不定有的人覺得原來用大括號的比較清楚)。

好寫是好寫,但也不是完全沒缺點,大家自己看畫面應該會發現到,例如原本Titanium Studio有syntax highlight改用CoffeeScript就沒了,當然code hint就別想了(不知道會不會有善心人士也把這個功能補完),不然就是先到有支援CoffeeScript的編輯器裡先寫好,再回來Titanium Studion裡進行編譯也是一招。

所以各位客倌就自己取捨囉,供大家參考。

CoffeeScript OOP篇

在JavaScript裡寫OOP有好幾種方式,例如你可以使用prototype的方式,也可以使用function或物件的方式來寫,不過其實不管哪一種,類別的程式碼寫起來、看起來都不夠直覺(當然也部份是因為我對JavaScript沒這麼熟)。還好在CoffeeScript寫類別是相當容易,而且程式碼是很清楚、容易閱讀的,來看個例子:

1
2
3
4
5
6
7
class Animal
  constructor: (name, age) ->
      this.name = name
      this.age = age

animal = new Animal("eddie", 18)
console.log animal

編譯結果:

1
2
3
4
5
6
7
8
9
10
var Animal, animal;
Animal = (function() {
  function Animal(name, age) {
      this.name = name;
      this.age = age;
  }
  return Animal;
})();
animal = new Animal("eddie", 18);
console.log(animal);

看到了嗎? 用CoffeeScript來定義類別真的簡單多了。特別注意一下constructor(建構子)的部份,這邊的:

1
this.name = name

因為在CoffeeScript裡@符號代表this,所以可以簡寫成:

1
@name = name

整個很有Ruby味道。又,因為建構子用的this.name = name這樣指定動作太常見,所以:

1
2
3
4
class Animal
  constructor: (name, age) ->
      this.name = name
      this.age = age

可以更簡化成:

1
2
class Animal
  constructor: (@name, @age) ->

方法

那如果要加其它的function到類別裡要怎麼做?

1
2
3
4
5
6
7
8
class Animal
  constructor: (@name, @age) ->

  say_hello: (something) ->
      console.log "Hello, #{something}"

animal = new Animal("eddie", 18)
animal.say_hello("CoffeeScript")

編譯結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
var Animal, animal;
Animal = (function() {
  function Animal(name, age) {
      this.name = name;
      this.age = age;
  }
  Animal.prototype.say_hello = function(something) {
      return console.log("Hello, " + something);
  };
  return Animal;
})();
animal = new Animal("eddie", 18);
animal.say_hello("CoffeeScript");

編譯出來的程式碼已經有點開始長了..

繼承

在CoffeeScript裡,繼承使用extends這個關鍵字:

1
2
3
4
5
6
7
8
9
10
11
12
class Animal
  constructor: (@name, @age) ->
  say_hello: (something) ->
      console.log "Hello, #{something}"

class Human extends Animal
  walk: ->
      console.log "I can walk with my foots!"

eddie = new Human("eddie", 18)
eddie.say_hello "CoffeeScript"
eddie.walk()

編譯結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var Animal, Human, eddie;
var __hasProp = Object.prototype.hasOwnProperty, __extends = function(child, parent) {
  for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; }
  function ctor() { this.constructor = child; }
  ctor.prototype = parent.prototype;
  child.prototype = new ctor;
  child.__super__ = parent.prototype;
  return child;
};
Animal = (function() {
  function Animal(name, age) {
      this.name = name;
      this.age = age;
  }
  Animal.prototype.say_hello = function(something) {
      return console.log("Hello, " + something);
  };
  return Animal;
})();

Human = (function() {
  __extends(Human, Animal);
  function Human() {
      Human.__super__.constructor.apply(this, arguments);
  }
  Human.prototype.walk = function() {
      return console.log("I can walk with my foots!");
  };
  return Human;
})();
eddie = new Human("eddie", 18);
eddie.say_hello("CoffeeScript");
eddie.walk();

看到編譯出來的JavaScript程式碼了嗎? 跟CoffeeScript的程式碼行數以及複雜度比起來,我想誰比較容易寫應該一目了然了。

在OOP的繼承體系裡,子類別會自動繼承自父類別的方法,如果兒子覺得老爸的方法不好用,兒子也可以自己定義一個來用,以上面的例子來說:

1
2
3
4
class Human extends Animal
  say_hello: (something) ->
      console.log "Hi, I am #{@name}"
      super something

你可以在子類別定義同名的function,以來取代或補充父類別的方法,如果在override的方法裡要存取父類,則是使用關鍵字super

有興趣的話,大家可以自己試著編譯看看這時候的程式碼是不是已經有點看不太懂了,但CoffeeScript的部份應該看起來還算滿清楚的。

CoffeeScript 基本篇(二)

邏輯判斷

其實就是if..else那類的事,沒什麼特別的。不過在CoffeeScript裡有幾個比較有趣的點:

  • 不需要特別寫then:除非是寫成一行,不然不需要特別加上”then”
  • 有unless可以用:unless就是not if,if就是not unless,也許你會覺得有if not了何必需要unless呢? 其實每個人對語意的理解都有些差別,有的人覺得”除非怎樣怎樣”比較通順,有的人覺得”如果不要怎樣怎樣..”比較通順,其實指的都是同一件事,隨個人喜好。
  • if後面的判斷句如果在不影響語意的情況下,可以不需要小括號,例如:
1
2
if age > 18
  console.log "Yes, You are Adult"
  • 如果if / unless接的內容只有一行的話,if / unless 可以像一般英文敘述句一樣,放到最後面,例如:
1
console.log "Yes, You are Adult" if age > 18

為什麼要放到後面? 有什麼好處? 通常把if丟後面去,除了縮短一行的行數、程式碼讀起來更像一般口語之外,以上面這個例子來說,會有特別”強調”或”希望它執行”前面那個動詞的意味(個人觀感)。 * 在比大小的時候,跟Python借來的大於、小於用法,讓語法更清楚,例如:

1
2
body_weight = 70
normal_body_weight = 65 < body_weight < 80

有if..else,當多個if..else判斷串在一起的時候,CoffeeScript也有switch可以用:

1
2
3
4
5
6
switch username
  when "eddie" then console.log "You are  the King"
  when "nayumi"
      console.log "Yes, My Lord.."
      console.log "You are the Queen"
  else console.log "You are nobody"

編譯結果:

1
2
3
4
5
6
7
8
9
10
11
switch (username) {
  case "eddie":
      console.log("You are  the King");
      break;
  case "nayumi":
      console.log("Yes, My Load..");
      console.log("You are the Queen");
      break;
  default:
      console.log("You are nobody");
}

要注意的是,跟if..else一樣,如果是單行敘述,when後面請加上then,否則不需要加。話說還好這點像Ruby不像Python,Python並沒有switch之類的可以用,這並沒不好,只是我還是覺得有switch之類的語法可以讓程式碼變得比較清楚,純個人喜好。

迴圈

CoffeeScript裡的迴圈跟一般的程式語言沒什麼太大的差別,就差不多是那麼一回事。前面提到的Range,常會放在迴圈裡面來使用:

1
2
3
4
console.log i for i in [1..10]                  # 印出1到10
console.log i for i in [1..10] when i % 2 == 0  # 印出2, 4, 6, 8, 10
console.log i for i in [1..10] by 2             # 一次跳2,印出1, 3, 5, 7, 9
console.log i * 2 for i in [1..10]              # 對每個i值 x 2

還有類似Ruby的陣列操作:

1
[1..10].map (i) -> console.log i*2

編譯結果:

1
2
3
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(function(i) {
  return console.log(i * 2);
});

跟if/unless一樣,while也有他的死對頭until (while = not until, until = not while),如果while / until的敘述句只有一行的話,同樣也可以放到句子的最後面。

1
console.log "Keep Walking" while miles < 100

編譯結果:

1
2
3
while (miles < 100) {
  console.log("Keep Walking");
}

語法糖衣

為了讓程式碼看起來更像一般的英文敘述句,在CoffeeScript裡特別做了一些alias,例如幫true值做了兩組alias:on以及yes,相對的,off跟no就是false的同義詞,其它的還有:

is    =>  ===
isnt  =>  !==
and   =>  &&
or    =>  ||
not   =>  !

所以,你可能可以寫出這樣的程式碼:

1
2
3
4
5
6
7
8
if light is off
  console.log "I can't see anything"

if eddie isnt handsome
  console.log "It's impossible!"

if girl is not single
  console.log "Don't Touch! Be Careful!"

編譯結果:

1
2
3
4
5
6
7
8
9
10
11
if (light === false) {
  console.log("I can't see anything");
}

if (eddie !== handsome) {
  console.log("It's impossible!");
}

if (girl === !single) {
  console.log("Don't Touch! Be Careful!");
}

這樣一來,程式碼看起來就更口語化了。

最後來看一下這個好用的問號:

1
age ?= 18

這個的意思是說,如果age這個變數還沒被定義或給值的話,就給這個變數設定為18。簡單的說,就是給預設值啦,所以上面這一行會被編譯成:

1
2
3
4
5
if (typeof age !== "undefined" && age !== null) {
  age;
} else {
  age = 18;
};

真的是相當方便啊!!

更多精彩內容,請參考CoffeeScript官網上文件。

CoffeeScript 基本篇(一)

首先,CoffeeScript的官網的文件寫得相當的詳細,強烈建議各位一定要看,只要看完一輪,大概就知道CoffeeScript是怎麼一回事了。

底下你看到的console.log,通常是拿來debug用的,並不會真的在網頁上產生結果,請搭配Firebug使用。

註解

CoffeeScript跟Ruby一樣,使用#字號來做註解,而且註解的內容在編譯過的js檔裡是看不到的。如果要做多行註解,則是使用3個#字號。

變數宣告

其實變數不用特別宣告,直接抓來用就好了,例如:

1
2
age = 18;
name = "CoffeeScript";

編譯結果:

1
2
3
var age, name;
age = 18;
name = "CoffeeScript";

看到了嗎? 它幫你宣告好了agename兩個變數,而且該加的分號都加上去了。

如果你要組合字串,除了用加號來串接外,CoffeeScript也從Ruby那邊借來了string interpolation:

1
greeting = "hello, #{name}"

編譯結果:

1
greeting = "hello, " + name;

要注意的是,如果你用的是單引號的話,string interpolation不會發生作用。

跟Ruby一樣,string interpolation可以放一個完整的expression,例如:

1
2
3
4
5
6
greeting = "hello, #{
  if name
      name
  else
      "CoffeeScript"
}"

編譯結果:

1
2
var greeting;
greeting = "hello, " + (name ? name : "CoffeeScript");

Function

1
2
3
4
say_hello = ->
  "Hello CoffeeScript"

say_hello()

編譯結果:

1
2
3
4
5
var say_hello;
say_hello = function() {
  return "Hello CoffeeScript";
};
say_hello();

像Ruby一樣,function的最後一個執行結果自動就是整個function的回傳值,所以這邊即使沒有特別寫return,該加上的return、大括號以及分號,都在編譯成js檔之後加上去了。(你要特別加上return也ok,不會變兩個return的)

再來,如果你要給function傳進參數的話:

1
2
3
4
5
6
7
8
9
10
11
12
age = 18;
name = "CoffeeScript";
greeting = "hello, #{name}"
say_hello = (name) ->
if name
  "hello, #{name}"
else
  greeting

console.log say_hello()
console.log say_hello("eddie")
console.log say_hello "eddie"

編譯結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var age, greeting, name, say_hello;
age = 18;
name = "CoffeeScript";
greeting = "hello, " + name;
say_hello = function(name) {
  if (name) {
      return "hello, " + name;
  } else {
      return greeting;
  }
};
console.log(say_hello());
console.log(say_hello("eddie"));
console.log(say_hello("eddie"));

這樣就可以傳參數進去了。

上面這段範例有兩件事要特別留意,首先,CoffeeScript像Python一樣使用縮排(identation)做為程式碼區塊的判斷,所以上面if..else..的縮排是有意義的。是的,if後面不用加then

第二,注意到在上面傳參數的時候,像Ruby一樣的省略掉了小括號嗎? 編譯出來的結果跟有小括號的是一樣的。那為什麼say_hello不傳參數的時候不能像Ruby一樣省略參數? 我想是因為CoffeeScript沒辦法真的像Ruby一樣辨識say_hello到底是一般的變數,還是沒有參數的方法呼叫,所以請記得,如果沒傳參數給function,請記得加上小括號

如果你要讓參數有預設值:

1
2
say_hello = (name = "CoffeeScript") ->
  console.log "Hello, #{name}"

編譯結果:

1
2
3
4
5
6
7
var say_hello;
say_hello = function(name) {
  if (name == null) {
      name = "CoffeeScript";
  }
  return console.log("Hello, " + name);
};

在Ruby裡傳參數,有叫做Splat的概念,可以一次傳進多個選擇性的參數,在CoffeeScript也有借來用,只是寫法有點不太一樣:

1
2
3
4
5
6
7
8
9
say_hello = (name, others...) ->
  if others.length > 0
      "Hello, #{name} and #{others}"
  else
      "Hello, #{name}"

console.log say_hello "eddie"
console.log say_hello "eddie", "kao"
console.log say_hello "eddie", "kao", "joanne"

編譯結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var say_hello;
var __slice = Array.prototype.slice;
say_hello = function() {
  var name, others;
  name = arguments[0], others = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
  if (others.length > 0) {
      return "Hello, " + name + " and " + others;
  } else {
      return "Hello, " + name;
  }
};

console.log(say_hello("eddie"));
console.log(say_hello("eddie", "kao"));
console.log(say_hello("eddie", "kao", "joanne"));

第二個參數others後面接三個點,表示它是一個「可有可無而且可以不定數量」的參數,而傳進來的參數變成一個陣列,看到編譯出來的程式碼就覺得如果要自己手工寫出這段程式碼要想好一下子的..

像Ruby一樣,CoffeeScript支援變數多重指定,但寫法有些不一樣,需要特別加上中括號,所以可以這樣玩:

1
2
3
4
[first, others..., last] = [1..10]
console.log first
console.log last
console.log others

編譯結果:

1
2
3
4
5
6
var first, last, others, _i, _ref;
var __slice = Array.prototype.slice;
_ref = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], first = _ref[0], others = 3 <= _ref.length ? __slice.call(_ref, 1, _i = _ref.length - 1) : (_i = 1, []), last = _ref[_i++];
console.log(first);     # 1
console.log(last);      # 10
console.log(others);    # [ 2, 3, 4, 5, 6, 7, 8, 9 ]

變數可以多重指派,當然也可以交換:

1
2
3
4
5
6
x = 1
y = 10
[x, y] = [y, x]

console.log x
console.log y

編譯結果:

1
2
3
4
5
6
var x, y, _ref;
x = 1;
y = 10;
_ref = [y, x], x = _ref[0], y = _ref[1];
console.log(x);
console.log(y);

可以用原來的方式寫function嗎?

你不一定要照著CoffeeScript的方式寫,如果CoffeeScript的語法讓你覺得不舒服,你希望可以用你習慣的JavaScript語法的話也ok,你可以這樣做:

1
2
3
say_hello = `function(name){
  return "Hello, " + name
}`

在反單引號(backtick)包起來的內容,在編譯的時候會照你寫的內容輸出,只是這樣一來用CoffeeSCript就沒意義啦!

陣列

1
heroes = ['Spider Man', 'Captain America', 'X-men', 'Iron Man']

在CoffeeScript裡,你可以用多行表示:

1
2
3
4
5
6
heroes = [
  'Spider Man',
  'Captain America',
  'X-men',
  'Iron Man'
]

編譯結果:

1
2
var heroes;
heroes = ['Spider Man', 'Captain America', 'X-men', 'Iron Man'];

甚至連每行後面的逗點也都可以省下來。

CoffeeScript從Ruby借來了Range

1
students = [1..10]

編譯結果:

1
2
var students;
students = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

如果是多一個點:

1
students = [1...10]

編譯結果:

1
2
var students;
students = [1, 2, 3, 4, 5, 6, 7, 8, 9];

如果你把Range反過來寫,例如[10..1],它就會變成從10到1的數字陣列。但如果把Range的數字放大一點:

1
students = [1..100]

編譯結果:

1
2
3
4
5
6
var students, _i, _results;
students = (function() {
  _results = [];
  for (_i = 1; _i <= 100; _i++){ _results.push(_i); }
  return _results;
}).apply(this, arguments);

只要Range大於20就會改用迴圈的方式來建構這個陣列。

陣列的操作也是很容易的

1
heroes[0..2]

編譯結果:

1
heroes.slice(0, 3);

如果利用Range來替換或新增陣列的內容:

1
heroes[1..2] = ["Batman", "ThunderCat"]

編譯結果:

1
2
3
var _ref;
heroes = ['Spider Man', 'Captain America', 'X-men', 'Iron Man'];
[].splice.apply(heroes, [1, 2].concat(_ref = ["Batman", "ThunderCat"])), _ref;

物件

你可以用這種JSON的寫法:

1
eddie = { name: "Eddie Kao", age: 18, speciality: "eat" }

或是YAML式的寫法:

1
2
3
4
eddie =
  name: "Eddie Kao"
  age: 18
  speciality: "eat"

編譯結果:

1
2
3
4
5
6
var eddie;
eddie = {
  name: "Eddie Kao",
  age: 18,
  speciality: "eat"
};

特別是如果你要做巢狀的物件結構,用YAML的寫法會更清楚:

1
2
3
4
5
6
7
8
9
10
11
eddie =
  name: "Eddie Kao"
  age: 18
  speciality: "eat"
  lovers:
  nayumi:
      name: "Nayumi Hung"
      age: 18
  mary:
      name: "Mary Bloody"
      age: 20