2012年12月29日 星期六

Git : 動手做看看 - 基本觀念

版本控制在管理源始碼的時候非常重要,可以讓開發者了解目前進度,共同開發時候也可以幫助開發者整合不同的源始碼。
日前因為 GitHub 的火紅帶領了 Git 這個工具的流行,而大部分的開發 IDE 也都支援 Git 的功能,這篇文章透過筆者的觀點來解釋 git 的運作方式,進而利用 Xcode 來說明圖形介面的操作和其相對應底層的機制是如何。

首先是

安裝 Git 


  1. 可以從最新的 Xcode 安裝或
  2. 到這裡下載 http://code.google.com/p/git-osx-installer/ (雖然檔案寫的是 snow leopard 但是 lion, mountain lion 都也可以用。)

1. 用 Xcode 安裝非常簡單,打開 Xcode 的 Preferences。如下

看到視窗後選 Downloads 的 Tab 然後安裝 Command Line Tools 如下。
這樣就好了在終端機輸入
git --version
會看到如下,那就是安裝成功了
git version 1.7.10.2 (Apple Git-33)
可以直接跳過下面,到 Git - 基本操作。

用 2. 的方法安裝的話 Mountain Lion 的朋友要注意一下,不是從 Apple Mac App 下載的安裝程式會被預設的安全機制給拒絕安裝,這時候就要打開,系統偏好設定。從左上方的蘋果圖示按下。

接著選擇安全性與隱私
看到如下畫面如果左下的鎖是鎖起來,點選,然後輸入密碼打開它
然後允許任何來源
這樣就可以安裝 Git。安裝好之後,把終端機(Terminal),打開輸入
git --version
一樣要看到如下
git version 1.7.10.2 (Apple Git-33)
安裝完成後,就來操作一下 git。
在終端機輸入
mkdir gitpractice

Git - 基本操作


來產生一個資料夾,這只是一個資料夾沒有什麼特別的,
再輸入
cd gitpractice
進入剛剛產生的 gitpractice 之內
這個裡面就是空的,沒有任何東西輸入
ls -al
來確認一下,得到

total 0
drwxr-xr-x   2 chronoer  staff    68 12 29 16:30 .
drwxr-xr-x+ 52 chronoer  staff  1768 12 29 16:30 ..
這樣的結果。現在要把這個資料夾轉成 git 可以處理的資料夾,輸入
git init
會得到如下
Initialized empty Git repository in /Users/chronoer/gitpractice/.git/
再用
ls -al
來看看這個資料夾有什麼變化,得到如下
total 0
drwxr-xr-x   3 chronoer  staff   102 12 29 16:31 .
drwxr-xr-x+ 52 chronoer  staff  1768 12 29 16:30 ..
drwxr-xr-x  10 chronoer  staff   340 12 29 16:31 .git
多了一個 .git 這個資料夾,這個 .git 就是存放和 git 的操作相關所有的在這個 gitpractice 底下所有檔案改成的歷史資料都存在這。而這個資料夾的存在,就代表,gitpractice 這個資料夾是受到 git 控管的,任何的檔案結構的改變都可以記錄下來,存放在 .git 這個資料夾裡面。有兩點要注意
Git 控管的資料夾只有根目錄會有 .git 不是每個子目錄都有。這個和 svn 做法不同
比如說 gitpractice 底下還有個 src 和 doc,架構如下
gitpractice/
                src/
                doc/
只有 gitpractice 裡面會有 .git/ 這個資料夾,src 和 doc 裡不會有,雖然 src 和 doc 這兩個也都受 git 控管,裡面檔案的變化也會記錄在其 git 根目錄 gitparctice 的 .git 之下。換言之如果把 .git 整個移除,那麼就無法得知 gitpractice 底下所有檔案變化,gitpractice 就回到一般目錄的身份。
另一個要注意的是
Git 不會自動控管檔案,而要手動加入。用 git add <fileName> 來把 <fileName> 加入 git 管理的隊伍中。
那我們就來動手加入一個檔案試。首先新增一個文字檔在 gitpractice/ 裡。輸入
echo "first line" > sample.txt
然後用
ls -al
看是不是多了一個文字檔如下

total 8
drwxr-xr-x   4 chronoer  staff   136 12 29 16:57 .
drwxr-xr-x+ 52 chronoer  staff  1768 12 29 16:30 ..
drwxr-xr-x  10 chronoer  staff   340 12 29 16:31 .git
-rw-r--r--   1 chronoer  staff    11 12 29 16:57 sample.txt

再用
cat sample.txt 
把這個文字檔的內容呈現在螢幕上
first line
這樣子我們新增一個檔案在 gitpractice/ 裡面,那這個檔案有被 git 所管理了嗎?NO 當然沒有,我們可以下
git status
來看 git 管理的檔案目前的狀態。如下
# On branch master
#
# Initial commit
#
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
# sample.txt
出現 untracked files: 然後有一個檔案就是剛剛新增的叫 sample.txt。
先插開一下話題,設定一下 git,如果要讓 git 的一些常用的輸出有顏色出現比較好看的話,可以做如下設定。可以參考這個網頁
git config --global color.diff auto # git diff 要顯示顏色
git config --global color.status auto # git status 要顯示顏色
git config --global color.branch auto
git config --global color.log auto
OK 回到正題,我們新增一個檔案但是不被 git 所管理,那這種檔案叫 untracked file 。那怎麼變成 tracked 呢?就要如下輸入
git add sample.txt
然後再輸入
git status
就會得到如下
# On branch master
#
# Initial commit
#
# Changes to be committed:
#   (use "git rm --cached <file>..." to unstage)
#
# new file:   sample.txt
#
沒有出現 untracked 字樣,這樣就是表示這個檔案 sample.txt 已為 git 所管理。
接下來我們要把這個檔案目前的樣子,記錄在 git 的歷史裡面,做為一個歷史的截圖,要輸入以下指令
git commit sample.txt -m "add first line"
其中用了 -m 這個 option 指的是對這個歷史節點,一定要留下註解,就是寫在 -m 之後的字串。然後會得到如下回應
[master (root-commit) 56f3cf3] add first line
 1 file changed, 1 insertion(+)
 create mode 100644 sample.txt
然後再用 
git log 
把歷史記錄呈現出來,如下
commit 56f3cf35c041a80f71393649cab87bbcab5d40bb
Author: chronoer <scentsome@gmail.com>
Date:   Sat Dec 29 21:56:25 2012 +0800
    add first line
記得 git log 只會呈現 git commit 之後的狀態,沒有 commit 的檔案是不會出現在 git log 列表裡的。
稍微看一下歷史記錄的內容,第一個 row 看到一堆奇怪的編碼如下
commit 56f3cf35c041a80f71393649cab87bbcab5d40bb
這個代表 git 把 sample.txt 的整份檔案做一個 HASH 的動作,然後存成一個 40 character 的字串,就代表了那個時候的檔案狀態 (讀者產生的 40 字元的字串會因內容不同而產生不同的字串)。接下來看到的是
Author: chronoer <scentsome@gmail.com>
這個就是作者和 e-mail ,可以從這兩個指令設定

git config --global user.name "chronoer"
git config --global user.email "scentsome@gmail.com"
接下來就是記錄的日期
Date:   Sat Dec 29 21:56:25 2012 +0800
和當初 commit 時候的註解

add first line
接著我們再用
git status
來看一下git 的狀態
會得到
# On branch master
nothing to commit (working directory clean) 
表明目前工作的目錄沒什麼改變是一個常態。
那我們就來改變一下 sample.txt 的內容
輸入
echo "second line" >> sample.txt  
注意是兩個">" 意思是把 "second line" 接到 sample.txt 的最後一行後面一行。用
cat sample.txt 
就會看到如下 sample.txt 的內容

first line
second line
此時我們改變了 sample.txt 然後用
git status
來查看目前 git 所管理的檔案的狀態,得到如下反應
# On branch master
# Changes not staged for commit:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
# modified:   sample.txt#
任何被 git add 加入的檔案有改變,而還沒 commit 之前,都會如上表示,為 modified 代表有被修改。
這邊還有一個 Changes not staged for commit 的敘述,是什麼意思呢,之後再解釋,現在我們要把 sample.txt 變成 staged 或是 index 如下輸入
git add sample.txt
再一次把修改中或是 unstaged 的檔案加入到 git 系統
然後我再用
git status
來看檔案的狀態會得到如下的回應

# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
# modified:   sample.txt
#

就不是 unstaged 狀態了。
為了把目前修改的 sample.txt 內容當成一個歷史的節點存下來,要用
git commit sample.txt -m "second line"
就會得到如下的回應

[master c1c905f] second line
 1 file changed, 1 insertion(+)
再次用
git status 
看一下目前的狀態,會得到

# On branch master
nothing to commit (working directory clean)
把剛剛修改的 sample.txt 再一次加入到了歷史的節點,工作目錄底下也就回到常態。到目前為止我們 commit 了兩次,用
git log
就可以看到兩個 commit 的記錄,就有兩個 HASH 之後的 40 個字元的字串。如下

commit c1c905f0d0f19a3ba750c3571d845bcc967c9ae6
Author: chronoer <scentsome@gmail.com>
Date:   Sat Dec 29 23:33:35 2012 +0800
    second line
commit 56f3cf35c041a80f71393649cab87bbcab5d40bb
Author: chronoer <scentsome@gmail.com>
Date:   Sat Dec 29 21:56:25 2012 +0800
    add first line
比較新的放在上方。就有兩筆 commit log。
到目前為止的動作整理一下

  1. 新增資料夾 - mkdir gitpractice
  2. 進入資料夾 - cd gitpractice
  3. 把資料夾轉成 git 可以管理的 - git init 
  4. 新增檔案 - echo "first line" > sample.txt
  5. 加入 git 管理 - git add sample.txt
  6. 記錄歷史節點 - git commit sample.txt -m "first line"
  7. 修改檔案 - echo "second line" >> sample.txt
  8. 變成 staged 檔案 - git add sample.txt 
  9. 記錄歷史節點 - git commit sample.txt -m "second line" (若沒做 8. 會一起執行)
以上就是基本用 git 來做版本控管的方式,每一個 commit log 都可以視為一個版本。
如何檢視各個版本就是用 
git log
會得到如下的結果
commit c1c905f0d0f19a3ba750c3571d845bcc967c9ae6Author: chronoer <scentsome@gmail.com>
Date:   Sat Dec 29 23:33:35 2012 +0800
    second line
commit 56f3cf35c041a80f71393649cab87bbcab5d40bbAuthor: chronoer <scentsome@gmail.com>
Date:   Sat Dec 29 21:56:25 2012 +0800
    add first line
把各個 commit 版本呈現出來。每個版本的代號就是 40 個字的字串。如果想要比較兩個不同版本的差異可以用
git diff <commit id> <commit id>
其中<commit id> 可以用原本的 40 字的前 7 個來代表比如上面的例子,要比較
c1c905f0d0f19a3ba750c3571d845bcc967c9ae656f3cf35c041a80f71393649cab87bbcab5d40bb
可以用  c1c905f  56f3cf3 寫成
git diff c1c905f 56f3cf3
得到如下結果
diff --git a/sample.txt b/sample.txt
index 06fcdd7..08fe272 100644
--- a/sample.txt
+++ b/sample.txt
@@ -1,2 +1 @@
 first line
-second line 
也就是  a/sample.txt, c1c905f 比 b/sample.txt 56f3cf3 少了一行 second line
如果把比較的檔案位子交換成
git diff 56f3cf3 c1c905f
就會得到

diff --git a/sample.txt b/sample.txt
index 08fe272..06fcdd7 100644
--- a/sample.txt
+++ b/sample.txt
@@ -1 +1,2 @@
 first line
+second line
也就是 56f3cf3 比 c1c905f 多了一行 second line
有了版本,就會想到要之前的版本,在 git 可以這麼做
git checkout <commit id> <file name>
<file name> 就會回到 <commit id> 的狀態,比如在這個例子,我們這麼做
git checkout 56f3cf3 sample.txt
然後再用
cat sample.txt
看看 sample.txt 的內容得到
first line
回到了 56f3cf3 的狀態了
很方便的版本管理可以任意回到之前的版本。
最後筆者來整理一下檔案在 git 資料夾裡的狀態。
剛剛我們用 git checkout <commit id> <file name> 之後會回到之前 commit 狀態,此時如果用
git status 
來看狀態會是如下

# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
# modified:   sample.txt
#

是回到一個 staged 的 sample.txt,如果再對這個檔案修改。比如用 vi 存完檔
就會發生一個很有趣的狀況

git status 
來看會看到

# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
# modified:   sample.txt#
# Changes not staged for commit:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
# modified:   sample.txt#
同一個檔案有兩個狀態,一個是 staged 另一個是 unstaged 可以直接 commit 或是先把 unstaged 用 git add 變成 staged
之後如果 commit 就會再多出一個 commit log。
以上是 git 的簡介,下一篇會來探討Xcode 的 GUI 和 git 運作的相關功能。








2012年12月2日 星期日

Objective-C naming convention 的重要性 - ARC class 呼叫 non-ARC Class method

Michael 上課的時候都會和學員提到命名的習慣,每個語言都有其命名的習慣。了解習慣可以幫助初學者很快的進入這個環境想要傳答的意圖是什麼。
命名的範圍很廣,從 Source Code 的檔案名稱到 Class 的名稱,Variable 的命稱,Method 的名稱等等都有其命名的習慣。 打個比方,在寫 Java 時,Java 的 Class 如下定義
class Car {
}
產生一個 Car class 其第一個字大寫是 Java 的命名習慣,而相對應這個 Source code 的檔案,也常命名為 Car.java,習慣就是 Java 的檔案名稱和其內容的第一個 Class 命稱一樣,大寫字開頭,這個是 Java 語言中的習慣。
習慣指的是,即使語法沒有強制說不行,但是大家都這樣做。
而 Objective-C 沒有這樣的習慣。

上述的 class 在 Objective-C 是這樣命名


@interface Car
@end

@implementation Car
@end

Class 的定義在 Objective-C 是分開的分為

@interface Car
@end


@implementation Car
@end
兩個部分。
再來看看另一個語言  C# 是如下定義
 
class Car{
} 
看起來和 Java 有點像,也沒有檔案名稱要和 class 命稱一樣命名的習慣。
我們再來看看 Class 的內部命名習慣。
把剛剛的 class 加上 ivar 和 method.
Java 部分
 
class Car {
  String name;
  String getName(){
    return name;
  }
  void setName(String newName){
    name = newName;
  }
}
新增了 name 這個變數 和 getName 還有 setName 這兩個 method。 getName 稱為 getter 而 setName 則為 setter 。
Java 的命名習慣就是,第一個字小寫,不論 variable 或是 method 的名稱。名詞和名詞之間後面接的名詞第一個字大寫,這種命西習慣稱為 CamelCase 駝峰式大小寫
再來看看 Objective-C 的命名習慣。

@interface Car:NSObject{
  NSString * name;
}
@end

@implementation Car
-(NSString *) name{
  return name;
}

-(void) setName:(NSString *) newName{
  name = newName;
}
@end
 
Objective-C 的命名習慣和 Java 差不多,但是 getter 的地方不一樣,在 Objective-C getter 的名稱和要讀的變數名稱一樣。在這個例子兩者都叫 name。
我們再來看看類似的 class 在 C# 如何表現?
 
class Car{
  string name;
  public string GetName(){
    return name;
  }
  public void SetName(string newName){
    name = newName;
  }
} 
在 C# 中新增了 name 這個變數和 GetName 還有 SetName 這兩個 method。在這邊可以注意到 C# 的變數名稱是小寫開頭而 method 的名稱習慣用大寫字開頭。
三種語言都有其個自的習慣,雖然不按照這個習慣,大部分情況下程式也是可以執行,但是在 Objective-C 如果不照者命名的習慣會造成一些奇怪的問題產生。
就讓我們來進入這篇文章的主題,開啟一個 ARC enable 的 Project 再加入一個 none ARC 的 class 就用上述的 Car 再加上兩個 method,讓我們一步一步來。
首先開啟一個 Single View Application 的專案如下
記得要勾選 Automatic Reference Counting
新增一個 Class 名為 Car 繼承 NSObject 如下是 Car.h
 
@interface Car : NSObject
-(id) newName;
-(id) newname;
@end
Car.m 如下
 
@implementation Car
-(id) newName{
    return [NSString stringWithFormat:@"Honda"];
}

-(id) newname{
    return [NSString stringWithFormat:@"Honda"];
}
@end
新增兩個 method 這兩個 method 的名稱很像,一個叫 newName 一個叫 newname 差別在於一個 name 有大寫的 N 另一個沒有。換句話說,newName 是有依照習慣命名,newname 是沒有的。 接下我們要把這個 Car.m 設定為不使用 ARC。
如下設定。
再新增一個 Button 在 畫面上,然後 Button 按下去 action 如下
 
- (IBAction)testCar:(id)sender {
    Car * car = [Car new];
    NSLog(@"Car name %@", [car newName]);
}
我們先使用這個 newName method。一執行,發現馬上就 crash。 但是如果換成 newname 這個 method 就是正常執行。 為什麼呢? 因為對於 non-ARC 的 method,
ARC 會看其 method 的開頭是不是 alloc, new 或 copy (所謂開頭指的是符合 CamelCase 的第一個詞)
或是
第一個是 mutable 第二個是 copy ,也就是寫成 mutableCopy 。
此時 ARC 會自動認為這個 method 是會產生一個沒有加到 autorelease pool 的物件,然後就自行決定加上 release 在這個例子使用 newName 就 crash 是因為 newName 回傳的是
[NSString stringWithFormat:@"Honda"] 
這個有加到 autorelease pool 的物件。而 ARC 自動多 release 一次了。這個例子是要提示大家,在寫 method 的時候要注意 CamelCase 第一個字,這個就是 ARC 的習慣。

2012年11月19日 星期一

Objective-C 如 printf() 或是 NSLog() 不定參數的寫法


參考
http://www.numbergrinder.com/2008/12/variable-arguments-varargs-in-objective-c/

準備 interface
 
@interface Car : NSObject {
}
-(void)addCars:(NSString * ) title, ... ;

@end
 


addCars: 這個 method 有一個 NSString 的參數,第二個參數是 ...

也就是 variable arguments 重要語法

先要確定 addCars: 怎麼用

我們假設是如下使用

[car addCars:@"Toyota", @"Honda", @"BMW", nil ];


可以輸入多個 NSString 但是要 nil 結束。
再來就是 implementation
 
@implementation Car
-(void) addCars:(NSString *)title, ...{

 va_list args;
 va_start(args, title);
 NSLog(@"%@", title);

 NSString * car;
 while ((car = va_arg(args, NSString *))) {

  NSLog(@"%@",car);
 }
 va_end(args);
}

@end
 

在上方的 addCars: 裡我們用到了

va_list 這個型別,用來代表 ... 的所有內容

用 args 指向 ... 的內容

再來看到 va_start(args, title)

明確告知 args 放的內容是 title 之後的內容

然後把每一個 args 都用 NSString * 來表示, 轉型成 NSString *

用 car = va_arg(args, NSString *) 把位置給 car 變數

到 car 是 nil 為止,所以用 while

最後是用 va_end(args); 結尾。


在 main 就可以如下輸入
 
Car * obj = [Car new];

[obj addCars:@"Toyota",@"Honda",@"Benz",@"BMW",nil];
 
Console 結果是

Toyota
Honda
Benz
BMW

用 nil 結尾主要是可以知道 ... 何時結束

也可以用參考提到的,

第一個參數是數字如下


- (NSNumber *) addValues:(NSNumber *) firstNumber, …


就交給大家試試了

2012年10月27日 星期六

iOS App 開發者交流會 - 2012-10-17 at Apple Inc. Taiwan 會議記錄

致謝

首先當然感謝 Apple 給 Michael 這個機會可以在 Apple 公司裡舉辦 iOS App 開發者的交流會。再來就是感謝極電資訊的幫忙可以促成這次會議的圓滿。

Announcement

一開始由 Apple 的 Kelvin 向大家介紹一下整個 Apple 內的環境,剛搬到這個新的地點,還是第一次舉辦開發者相關的研討會。Kelvin 主要是負責企業和教育市場,有這方面開發需求的開發者或是想要佈署相關設備的企業單位可以寫信給 Michael 幫各位轉介給 Kelvin. 有與會的朋友們己經有 Kelvin 的連絡方式了,就可以自己連絡。

Opening

由小弟我來給大家一個開場,主要是提到 iOS 的現況還有幾個主要的技術分享
  • 使用 Xcode 4.5 在 iOS 5 執行環境,需要注意的地方
  • UIViewController 有關 Rotation 的改變
  • Auto Layout
等等的技術分享,投影片可以在這邊看到。http://slidesha.re/RYkbs5

與創投打交道 - Hoku

(Hoku有交帶,不方便露臉)。
重點節錄
  • 創投不會只想把公司吃掉
  • 不會惡意想要搞垮公司
  • 創投可以在初期就幫助公司,也可以在後期幫助公司上市
  • 誠實的老闆是創投最喜歡投資的對象
  • 請不要太工程師導向或是業務導向
有關更多的資訊,可以參考。http://slidesha.re/RYoq7e

學員成果分享 - Jacky

很高興可以邀請到 Jacky 巨狐資訊科技有限公司 GZFOX, INC. 執行長和大家分享,自行開發 App 的經驗。
其中 Michael 印象比較深的是和公家單位打交道時的過程:
Jacky 有寫一個台鐵的 App,一開始自已去連網站,發現不行,然後寫信給台鐵,一來一往之後,過了很久,才有回覆,才可以拿到比較完整的資料。
還有就是有一個利用 App 可以線上沖洗自己手機裡的照片成真實的像片。
完整的投影片在。http://slidesha.re/RYrpMK

學員成果分享 - Wei Wei

Wei Wei 算是學員中最傳奇的人物了,當過 DJ,出過唱片,書本,會英法德語,還寫了好幾個 App 的 Game。Michael 覺得世上怎麼會有如此厲害的人物。
除了當天就發表一款新遊戲之外,還和大家分享有趣的音樂生成軟體,還有介紹幾本好書。
演講的過程整個充滿熱鬧的氣氛,希望 Wei Wei 不要來搶我的飯碗 orz。
投影片,可以在此下載。 http://slidesha.re/RYsx2V

學員成果分享 - Steven

Steven 寫了二個評價不錯 App 和電子商務有關,也都有廣告商找上門來要配合放廣告,其中比價撿便宜寫下了免費的第二名的佳績,然後呢就決定轉成付費,再來看一下消費者對於定價的觀感,從 90 元 到 30 元的定價比較,來分析如何的定價才是最適當的。
在分享過程中 Kelvin 和 Michael 都各自提過一個 Promote 自己 App 的方法,分享給大家。Promotion 是非常重要的在現在 App 海的社會中。
完整的投影片在此可以下載。http://slidesha.re/RYuEnv

One more thing - Michael


Michael 成立了友教有限公司,專門服務企業內部的訓練和提供專業的 App 開發和整合服務給企業朋友們。有興趣的朋友可以寫信給
Michael - scentsome@gmail.com
除了提供軟體服務外,友教和極電合作,提供好的硬體服務環境給客戶,軟體加硬體的服務希望可以增進企業導入 App 開發和產品合作。
極電資訊 - 20 年的 Apple 授權經銷商 02-2705-6216

ADB - Arthur

除了 App 之外,Michael 向大家推薦 Arthur 最新研發出的產品 ADB 是一個 Module 可以透過耳機孔提供 App 和 UART 的雙向傳輸資料的服務。Square 是一個著名的成功例子,只在信用卡的金流,而 Arthur 的 ADB 可以應用在各式各樣的服務,甚至包含 Android 等有耳機孔的系統,都可以透過 App - ADB - Any Device 溝通。
完整的投影片在- http://slidesha.re/RYvS25

2012年9月11日 星期二

聊天廣播 - iOS 與 HTML 利用 Socket.io

在日前某篇的文章中,筆者教大家如何建立一個 Node.js 寫的 Server 利用 socket.io 這個 module 讓不同的 Browser 可以即時聊天。這篇是介紹大家怎麼在 iOS 的環境下,也利用 socket.io 讓手機可以和 Browser 一起聊天。這篇文章,我們利用到幾個 OpenSource 的套件,分別是 socket.io-objcSocketRocket,這兩個,其中 socket.io-objc 是主要我們直接使用的套件,其利用 SocketRocket 然後包成一個很好用的介面讓我們使用,也是 Socket.IO 在 GitHub Wiki 提到 Objective-C 語言的實作範例。而這次要用 JSON 的格式來傳遞資料,所以我們要來修改一下前篇提到的 chatServer.js 和 chat.html。chatServer.js 更改如下
 
var app = require('http').createServer(handler) 
 , io = require('socket.io').listen(app)
 , fs = require('fs');
app.listen(8124);

function handler (req, res) { 
    fs.readFile(__dirname + '/chat.html', function (err, data) {
 if (err) { res.writeHead(500);
 return res.end('Error loading chat.html'); 
    }
    res.writeHead(200);
    res.end(data); });
}
io.sockets.on('connection', function (socket) {
    socket.on('addme',function(jsonMessage) {
 socket.username = jsonMessage.name;
 var toClient = { sender : "SERVER", message : "Good to see your "+ jsonMessage.name};
 socket.emit('chat', toClient);
 toClient.message =  jsonMessage.name + " is on the Desk";
 socket.broadcast.emit('chat', toClient);
    });
    socket.on('sendchat', function(data) { 
 io.sockets.emit('chat', { sender : socket.username, message : data.message});
    });

    socket.on('disconnect', function() {
 var bye = { sender : "SERVER", message : socket.username+"has left the building"};
 io.sockets.emit('chat', bye);
    });
});
 
所有的訊息資料的傳遞都用 JSON 格式,和之前不一樣的地方特別提出來說明一下。先看 addme 這個事件
socket.on('addme',function(jsonMessage) {
 socket.username = jsonMessage.name;
 var toClient = { sender : "SERVER", message : "Good to see your "+ jsonMessage.name};
 socket.emit('chat', toClient);
 toClient.message =  jsonMessage.name + " is on the Desk";
 socket.broadcast.emit('chat', toClient);
    });
除了 event 的名稱外,只接收一個參數,jsonMessage,因為 JSON 格式本身就可以帶很多資訊,所以 function 不需要多個參數。接著把要傳給 client 的訊息,裡面包含 sender 和 message 的訊息,一樣用 JSON 包起來存在 toClient,然後先 emit 給這個 socket 的 client,再更改 toClient 的 message,然後 broadcast 給所有的 client。
再來看到 sendchat 事件
socket.on('sendchat', function(data) { 
 io.sockets.emit('chat', { sender : socket.username, message : data.message});
    });
聽到 sendchat 事件後,就把 sender 和 message 的訊息用 chat 這個事件傳給所有的 client。 最後看一下 disconnect 事件
socket.on('disconnect', function() {
 var bye = { sender : "SERVER", message : socket.username+"has left the building"};
 io.sockets.emit('chat', bye);
    });
都是包成 JOSN 包含 sender 和 message。如此 Server 就修改完了,接下來看看 client,chat.html

<html lang="en"> 
<head>
<meta charset="utf-8">
<title>Chat Room</title> 
<script src="http://localhost:8124/socket.io/socket.io.js"></script>
<script>
var socket = io.connect('http://localhost:8124'); 
socket.on('connect', function() {
    socket.emit('addme',{name : prompt('Who are you?')});
});
socket.on('chat',function(data) { 
    var p = document.createElement('p');
    p.innerHTML = data.sender + ': ' + data.message;
    document.getElementById('output').appendChild(p); 
});
window.addEventListener('load',function() { 
    document.getElementById('sendtext').addEventListener('click',function() {
        var text = document.getElementById('data').value; 
        socket.emit('sendchat', {message : "say "+text});
    }, false);
}, false);
</script>
</head>
<body>
<div id="output"></div> <div id="send">
<input type="text" id="data" size="100" /><br />
<input type="button" id="sendtext" value="Send Text" /> </div>
</body> </html> 

也都是把交換的訊息用 JSON 格式來傳遞。一個個來看一下
socket.on('connect', function() {
    socket.emit('addme',{name : prompt('Who are you?')});
});
我們可以直接把 prompt() 的結果放在 JSON 物件內,再傳出去
socket.on('chat',function(data) { 
    var p = document.createElement('p');
    p.innerHTML = data.sender + ': ' + data.message;
    document.getElementById('output').appendChild(p); 
});
收到 data 可以直接用 data.message 就把真正的訊息抓出來了。
 document.getElementById('sendtext').addEventListener('click',function() {
        var text = document.getElementById('data').value; 
        socket.emit('sendchat', {message : "say "+text});
    }, false);
當網頁上的 Send Text 按鈕被按下的時候,會把 Text Field 的內容,包成 JSON 格式,在這裡,Text Field 的內容存在 text 它不是 string, 在放到 JSON 之前加上 "say " 就會被轉成 string。
接下來就是 iOS Client 的部分了,首先下載這兩個 Proejct,socket.io-objc SocketRocket 下載完了,先來開啟一個 Single View 的 Project,命名為 ChatClient
並把 Class Prefix 命名為 CC。如下
接著可以把需要的 .h/.m 從,socket.io-objc SocketRocket 把它們拉到 ChatClient 這個 Project 裡如下。
 最左邊的是我們的 Project : ChatClient。右邊兩個分別是 Socket.io-objc 和 SocketRocket 的 source code。
接著我們要為 ChatClient 新增一些 Framework,這些是 SocketRocket 必要的,所以要先新增,如下陳列。
  • libicucore.dylib
  • CFNetwork.framework
  • Security.framework
  • Foundation.framework
其中 Fundation.framework 已經有了,就不用再新增,新增 Framework 可以在下圖的地方新增。
 新增三個,新增完如下。
 這樣一來就準備好了,我們先 run 一下,看看有沒有錯誤?如果成功執行也沒有任何的 Warning 或是 Error 那就可以了。如下的畫面
首先來新增一些 UI 元件,打開 Storyboard,從 Library 放上如下元件
右上是兩個 UIButton,左邊上方是一個 UITextField 下方是一個 UITextView。把 Text Field 和一個 IBOutlet 關連在一起,命名為 inputTextField。如下
接著再連結底下的 Text View。命名為 resultTextView 。如下表示
接著將 Login 的 UIButton 和 IBAction login: 連結在一起,如下

下方的 Send UIButton 則和名為 sendText: 的 UIAction 連結。如下
因為要引用 Socket.IO 我們在 CCViewController.h 加入如下程式碼。
 
#import <UIKit/UIKit.h>
#import "SocketIO.h"

@interface CCViewController : UIViewController<SocketIODelegate>
@property (weak, nonatomic) IBOutlet UITextField *inputTextField;
@property (weak, nonatomic) IBOutlet UITextView *resultTextView;

@end
 
我們 import SockeIO.h 和採用 SocketIODelegate 這個 protocol 接下來準備兩個 ivar,寫在 CCViewController.m 如下
 
#import "CCViewController.h"

@interface CCViewController (){
    NSMutableString * resultString;
    SocketIO *socketIO;
}

@end
 
其中 resultString 是用在保存從 server 來的訊息,而 socketIO 是主要的變數用來和 server 溝通。接下來就是初始化這兩個物件,如下我們寫在 CCViewController.m 的 viewDidLoad 如下
 
- (void)viewDidLoad
{
    [super viewDidLoad];
    socketIO = [[SocketIO alloc] initWithDelegate:self];
    resultString = [[NSMutableString alloc] initWithCapacity:10];
    // Do any additional setup after loading the view, typically from a nib.
}
 
主要就是 socketIO 初始化就把 delegate 物件設定好。socketIO 在這個例子會用到的 delegate method 如下列出。
 
- (void) socketIO:(SocketIO *)socket didReceiveEvent:(SocketIOPacket *)packet;
- (void) socketIODidConnect:(SocketIO *)socket;
- (void) socketIODidDisconnect:(SocketIO *)socket;
 
之後我們會用到。 接著就是第一個動作,筆者寫在 login: 這個 method,按下之後先跳出一個 UIAlertView 讓使用者輸入名稱,然後按下確定後,才真正傳訊息給 server,如下表示
 
- (IBAction)login:(id)sender {
    UIAlertView * nameAlert = [[UIAlertView alloc] initWithTitle:@"Welcome to ChatRoom" 
 message:@"Who are you" delegate:self cancelButtonTitle:nil 
otherButtonTitles:@"OK", nil];

    nameAlert.alertViewStyle = UIAlertViewStylePlainTextInput;
    [nameAlert show];
}
-(void) alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{
    NSString * inputName = [alertView textFieldAtIndex:0].text;
    [socketIO connectToHost:@"localhost" onPort:8124];
    NSDictionary * hello = [NSDictionary dictionaryWithObjectsAndKeys:inputName,@"name", nil];
    [socketIO sendEvent:@"addme" withData:hello ];
}
 
這個 UIAlertView 是一個 UIAlertViewStylePlainTextInput,當使用者按下 Alert View 的確定後,會先 connect 到 server, 再準備一個 hello dictionary 當資料,key 為 name,然後用 sendEvent:withData 把 event 名稱寫成 addme 再把 hello 傳給 server。然後我們準備接收從 server 來的訊息需要 implement delegate method。如下
 
-(void) updateResult{
    resultTextView.text = resultString;
}

#pragma SocketIO Delegate
- (void) socketIO:(SocketIO *)socket didReceiveEvent:(SocketIOPacket *)packet{
    if ([packet.name isEqualToString:@"chat"]) {
        NSDictionary * stringData = (NSDictionary *) [NSJSONSerialization JSONObjectWithData:
[packet.data dataUsingEncoding:NSUTF8StringEncoding] options:
NSJSONWritingPrettyPrinted error:NULL];        
        NSDictionary * messageData = [[stringData objectForKey:@"args"] objectAtIndex:0];
        [resultString appendFormat:@"%@ say %@\n",
 [messageData objectForKey:@"sender"],[messageData objectForKey:@"message" ]] ;
    }
    [self updateResult];
}
 
我們必需要 implement socketIO:didReceiveEvent: 才會收到 server 來的資料,在這個 method 裡,先用 data.name 來得知 event 的名稱,再判斷是不是 chat, 由 packet.data 包起來的訊息有點複雜我們來看其原本的樣子。如下

{"name":"chat","args":[{"sender":"SERVER","message":"Good to see your iOS"}]}
接著來解釋一下 didReceiveEevent中的幾個動作
 NSDictionary * stringData = (NSDictionary *) [NSJSONSerialization JSONObjectWithData:
[packet.data dataUsingEncoding:NSUTF8StringEncoding] options:
NSJSONWritingPrettyPrinted error:NULL];   
上方的程式碼是把 packet.data 轉成 NSData 再轉成 JSONObject 最後轉成 NSDictionary * stringData 的方法,為什麼這麼麻煩是 packet.data 是一個由 JSON 轉成的 string,我們將之轉成 dictionary 會比較好取得其內容。
 NSDictionary * messageData = [[stringData objectForKey:@"args"] objectAtIndex:0];
        [resultString appendFormat:@"%@ say %@\n",
 [messageData objectForKey:@"sender"],[messageData objectForKey:@"message" ]] ;
上方的程式碼是把 packet.data 中 args 這個 key 的值取出來,再把其中的 message key的值取出來接在 resultString 的最後面。最後會用到 [self updateResult]; 這個 method 也只是把 resultString 放到 resultTextView.text 變數裡。
如此一來就可以先打開 server ,用 node chatServer.js 執行後,就可以執行 iOS ,正常的情況下就會出現如下畫面。
先輸入名稱

看到 Server 來的訊息
這樣協定的部分差不多是可以了,再來就是從 iOS 發出訊息的部分,我們寫在 CCViewController.m 如下

- (IBAction)sendText:(id)sender {
    NSDictionary * chat = [NSDictionary dictionaryWithObjectsAndKeys:inputTextField.text,@"message", nil];
    [socketIO sendEvent:@"sendchat" withData:chat];
    [inputTextField resignFirstResponder];
}
單純地把 inputTextField.text 包在 Dictionary 裡面,用 message 當 key 就送給 server。簡單地動作,重新執行後,這樣一來就可以和 Brwoser 溝通了,如下畫面
 當然看著做一定會順暢,筆者還是建議把兩個 delegate method 補上,這樣可以知道那邊出了問題。如下

- (void) socketIODidConnect:(SocketIO *)socket{
    NSLog(@"did connect to %@", socket);
}
- (void) socketIODidDisconnect:(SocketIO *)socket{
    NSLog(@"did disconnect to %@", socket);
}
一樣,程式碼都放在 GitHub,包含了 Server,Client,iOS。Good to see you again, Bye.

2012年9月10日 星期一

聊天廣播 - Socket.io

關於網路的架構,Server-Client 這樣的結合己經延用己久,相信大家會了解 HTTP 的協定是 stateless,一但 client 和 server 通訊完,server 就不知道 client 跑去那了?如果 server 有更新想要 Push 給 client,就要反過來從 Client 定期的去向 server 要求。今天要來使用 socket.io 這個 module 就是可以達到 server 和 client 的雙向溝通,只要一建立連線 server 可以一直傳訊息給 client,反過來也行,直接某一方斷線為止。先姑且不論 socket.io 底層是什麼,我們就用一個聊天廣播室來建立這個服務。client 端我們用 HTML 來實現,建立如下的 HTML,命名為 chat.html。
 
<html lang="en"> 
  <head>
  <meta charset="utf-8">
  <title>Chat Room</title> 
  </head>
  <body>
    <div id="output">
    </div>
    <div id="send">
    <input type="text" id="data" size="100" /><br />
    <input type="button" id="sendtext" value="Send Text" />
    </div>
  </body>
</html>
 
這個 HTML 在 Browser 執行會是如下畫面
有一個 Text Field 可以輸入文字然後有一個按鈕
接著加上一些 javascript,在 <head> </head> 中間。如下
 
<head>
<meta charset="utf-8">
<title>Chat Room</title> 
<script src="http://localhost:8124/socket.io/socket.io.js"></script> 
<script>
var socket = io.connect('http://localhost:8124'); 
socket.on('connect', function() {
    socket.emit('addme', prompt('Who are you?')); 
});
socket.on('chat',function(username, data) { 
    var p = document.createElement('p'); 
    p.innerHTML = username + ': ' + data;
    document.getElementById('output').appendChild(p); 
});
window.addEventListener('load',function() { 
    document.getElementById('sendtext').addEventListener('click',function() {
        var text = document.getElementById('data').value; 
        socket.emit('sendchat', text);
    }, false); 
}, false);

</script>
</head>

一開始這個文件用了 <script src="http://localhost:8124/socket.io/socket.io.js"> 因為要用到 socket.io 的功能,所以要指向 server 的 socket.io.js ,在 chat.html 使用 。
我們一一來看一下。
var socket = io.connect('http://localhost:8124'); 
socket.on('connect', function() {
    socket.emit('addme', prompt('Who are you?')); 
});
這個是要用 io 去連結 server,然後就等待, connect 這個事件,也就是和 server 連上線後,就會觸發。觸發的動作就是發送一個訊息給 server 事件名為 addme,內容為 prompt('Who are you?')。prompt() 的用途是會跳出一個需要輸入的視窗,然後把使用者在這個視窗輸入的值,再經由 socket.emit 傳給 server。接著再來看一下,下一個 socket 事件
socket.on('chat',function(username, data) { 
    var p = document.createElement('p'); 
    p.innerHTML = username + ': ' + data;
    document.getElementById('output').appendChild(p); 
});
在 socket.on 等待 chat 事件,然後 server 會傳兩個值過來分別存在 username 和 data 這兩個變數中,接著更新網頁內容在 <output> 裡多加一個 <p> 內容就是某個 user 打了什麼字,這樣。
在 Client 端最後,我們看到
window.addEventListener('load',function() { 
    document.getElementById('sendtext').addEventListener('click',function() {
        var text = document.getElementById('data').value; 
        socket.emit('sendchat', text);
    }, false); 
}, false);
這個是新增按鈕按下去的觸發動作,主要就是把 TextField 中使用者輸入的文字,抓出來
var text = document.getElementById('data').value; 
然後再利用 socekt.emit 送給 server。這樣 Client 端就完成了。 Server 端也非常之簡單,如下就是全部了,存成 chatServer.js。

var app = require('http').createServer(handler) 
 , io = require('socket.io').listen(app)
 , fs = require('fs');
app.listen(8124);

function handler (req, res) { 
 fs.readFile(__dirname + '/chat.html', function (err, data) {
     if (err) { 
                res.writeHead(500);
         return res.end('Error loading chat.html'); 
            }
     res.writeHead(200);
     res.end(data); 
        });
}
io.sockets.on('connection', function (socket) {
 socket.on('addme',function(username) {
  socket.username = username;
  socket.emit('chat', 'SERVER', 'You have connected'); 
  socket.broadcast.emit('chat', 'SERVER', username + ' is on deck');
 });
 socket.on('sendchat', function(data) { 
  io.sockets.emit('chat', socket.username, data);
 });

 socket.on('disconnect', function() {
  io.sockets.emit('chat', 'SERVER', socket.username + ' has left the building');
 });
});
一開始需要用到 http,socket.io,fs 等 module 就引用就好了。然後來看一下 handler 這個 function 被用在一開始的 require('http').createServer(handler) 。
function handler (req, res) { 
 fs.readFile(__dirname + '/chat.html', function (err, data) {
     if (err) { 
                res.writeHead(500);
         return res.end('Error loading chat.html'); 
            }
     res.writeHead(200);
     res.end(data); 
        });
}
這個 funciton 主要是說不管 client 端在 Browser 輸入什麼路徑,都是執行這個 function ,無論是輸入
http://127.0.0.1:8124/ashdf;asdlf/has;dfasdf
或是
http://127.0.0.1:8124/
結果都是一樣的,就是把同一個目錄下的 chat.html 讀出來然後送給 Browser, 也就是說,現在 Browser 看到的就是一開始我們寫的 chat.html ,一個 Text Field 和一個按鈕的樣式。
io.sockets.on('connection', function (socket) {
 socket.on('addme',function(username) {
  socket.username = username;
  socket.emit('chat', 'SERVER', 'You have connected'); 
  socket.broadcast.emit('chat', 'SERVER', username + ' is on deck');
 });
 socket.on('sendchat', function(data) { 
  io.sockets.emit('chat', socket.username, data);
 });

 socket.on('disconnect', function() {
  io.sockets.emit('chat', 'SERVER', socket.username + ' has left the building');
 });
});
接著看到一滿大的 function,就是 io.socket.on 等待 connection 事件, 然後會得到一個 socket 也就是和一個 client 連上線。要注意的是這個 socket 會因為不同的 client 而有不同的記憶體 接著看裡面一點
socket.on('addme',function(username) {
  socket.username = username;
  socket.emit('chat', 'SERVER', 'You have connected'); 
  socket.broadcast.emit('chat', 'SERVER', username + ' is on deck');
 }); 
然後這個 socket 會等待 addme 這個事件,會得到一個 username 再存到 socket.name 保存下來。 然後用 socket.emit 回給連上來的 client,再用 socket.broadcast.emit 廣播給所有在線上其他的client。接著再往下看。
socket.on('sendchat', function(data) { 
  io.sockets.emit('chat', socket.username, data);
 });
這個 socket 等待 sendchat 的事件,然後收到資料後用 io.sockets.emit 廣播給所有線上的 client。說到這裡,我們來看這三個相似的 function
  • socket.emit - 對於一個特定的 socket 傳訊息
  • socket.broadcast.emit - 對於除了目前這個 socket 之外所有線上的 socket 傳訊息
  • io.sockets.emit - 對於所有線上 socket 傳訊息
然後就是最後一個 function 了
socket.on('disconnect', function() {
  io.sockets.emit('chat', 'SERVER', socket.username + ' has left the building');
 });
也就是 socket 等待 disconnect 之後,就廣播給其他 client 說某 socket 離線了。 先執行 node chatServer.js
測試的方法就是用兩個不同的 Browser 試試連看看,這樣就可以互相聊天了。
 原始碼都放在 GitHub
補充說明一下,在這個例子我們都使用 socket.emit() 來傳送訊息,也許讀者有參考到其他教學有用到 socket.send() 這個 function,我們用一個例子來解釋
socket.send("Hi") - 相當於 socket.emit("message", "Hi");
也就是說,socket.send("Hi") 省略了 socket.emit("message", "Hi"); 前面的第一個 "message",而接受的一端就是用

socket.on('message', function (message) {
  console.log(message);
});
來接收 socket.send("Hi") 的訊息。

2012年9月8日 星期六

NoSQL Database - MongoDB & Node.js

這篇文章主要是介紹 MongoDB 這個資料庫,也會用 Node.js 與其相連,不過在這之前,先來解說一下一些名詞,首先就是 NoSQL,從 Wikipedia 看來就是不用 SQL 當做查詢的語言,換句話說,如果用 MongoDB 就不用學 SQL 了,呵。看來不錯,再來看看 MongoDB 有什麼特色,MongoDB 是一種文件導向的資料庫,什麼文件?長什麼樣子?舉一個例子來說
{
    name : "Michael"
    gender : "Male"
}
長得和 JSON 好像啊,嗯啊,可以直接把 JSON 丟到 MongoDB 裡,這個就是文件,還可以很複雜如下
 {
     "query": "Pizza",
     "locations": [
         1210054,
         1201167
     ]
 }
就是一個 JSON 所以用 Javascript 存取 MongoDB 非常直覺。OK 讓我們來安裝一下 MongoDB
安裝有幾種方式比如有 Homebrew 直接用
brew install mongodb
或者到這安裝執行檔,有幾個作業系統可以選如下
 安裝完之後在 Terminal 可以輸入以下指令,來確定安裝成功。
mongod --version
可以看到如下畫面
 這樣就是成功安裝了。好吧,我們開啟這個 MongoDB 服務,很簡單,直接輸入 mongod 這樣就可以了
mongod
不過,讀者有可能會看到以下的畫面,代表沒有開啟服務成功
 紅色框起來的部分,寫著
MongoDB starting : pid=4116 port=27017 dbpath=/data/db/ 64-bit host=chronoers-MacBook-Air.local
MongoDB 想要在 port 27017 開啟,而且把資料放在 /data/db/ 這個資料夾下,然後就是shutdown 了,很有可能就是 port 27017 被別人用了,或是 dbpath 這個目錄不存在,一開始沒有別的服務的話,dbpath 的這個目錄不存在的可能性很高,這篇文章的做法就是先在當下目錄下建立一個新的目錄名為 dbs,如下指令
mkdir dbs
然後再啟動 MongoDB ,這次要改變一些參數,如下
mongod --dbpath=dbs
這樣一來 mongod 就會用當前的目錄下的 dbs (剛剛我們建立的),來存放資料,執行成功後會看到如下畫面
最後寫 waiting for connections on port 27017 然後整個游標停在那,這樣就表示正常啟動了。
我們先用 Terminal 來新增一些資料到 dbs 裡面,在先前有提過 document 的觀念,基本上可以視為一個 JSON 物件。接者要來說另一個名詞叫 collection,很多筆 document 就組合成一個 collection ,所以我們要新增一筆 document 之前要先建立一個  document ,這是 MongoDB 的架構,OK。
怎麼新增呢?我們在安裝完成後,除了有 mongod 這個指令外,還有另一個叫做 mongo 要注意,只差一個 d,符號,mongo 就是要和 mongod 來溝通的指令,首先先來看一下版本
直接執行
mongo
會看到如下畫面
 游標停在 > 之後,代表可以直接輸入指令,控制 mongod,如下輸入
show dbs
代表把所有的 database 呈現在 Terminal 上,應該會看到如下


local (empty) 就是有一個資料庫名為 local 但是沒有資料在裡面。我們就來新增一個資料庫名為 mydb,如下輸入
use mydb
會看到
雖然沒有明確看到建立的動作,  因為 mydb 裡面如果沒有 document 或 collection 是沒有意義的,暫時這樣,先來看一下這個 mydb 一些基本內容
db.stats()
這樣會看到
還記得 collection 和 document 嗎?我們就來建立一下。輸入
db.users.insert({name : "Michael", gender : "Male"})
如果沒有發生錯誤,就會看到如下
users 這個就是 collection  而 {name : "Michael", gender : "Male"} 就是 document ,insert 就是寫入的動作,就這樣很直接的寫就可以新增資料進入 mydb 了,我們用
show collectons 就可以看到如下,有出現  users

users 真得有寫入 mydb 接下來看 users 這個 collection 裡面的內容,使用如下指令
db.users.find()
會看到如下
真得有一筆資料,接下來我們做個實驗
再輸入一次
db.users.insert({name : "Michael", gender : "Male"})
再用
db.users.find()
會發生什麼事?

兩筆我們認為一樣的資料寫入了,MongoDB 不會因為 name, gender 一樣的值就自行判斷不加入哦,對 MongoDB 而言就是兩筆資料,而兩筆的 _id 是不一樣的,那接下來把其中一個刪除吧。用下列指令
db.users.remove({"_id" : ObjectId("5048d6b704f6c8a66750e12b")})
因為 _id 才是可以辦識不一樣的key 所以要輸入 _id 才可以明確指出要 remove 那一筆 document 。接下來我們來更改一下內容,用 update,如下
db.users.update({gender : "Male"}, { $set :{ name : "James" }})
再用
db.users.find()
把結束呈現出來會看到下方結果

 name 改成了 James 了,我們來看一下 update 這個 function 有兩個參數,第一個參數是要尋找的條件,我們設定為 {gender : "Male"} 目前也只有一個 document 符合,接下來第二個參數是
{ $set : { name : "James"}}
 前面要加上 $set 會依照原本的值 {name : "Michael", gender : "Male"} 做為改變的依據,把 name 這個值改成 James,如果沒有加上 $set 比如說
db.users.update({gender : "Male"}, { name : "James" })
則會把原本的 {name : "Michael", gender : "Male"} 用 { name : "James" } 取代,也就是說  { gender : "Male "} 並不會被保留。
好我們介紹了 insert, find, update, remove 這幾個操作就是有名的 CRUD - (Create,Read,Update,Delete),接下來我們來看一下怎麼從 Node.js 來存取 MongoDB 的資料。
首先把上面的 MongoDB 停止,需要輸入  Ctrl + c。然後再另一個資料夾再開啟一個新的 Server。我們就建立一個名為  mongoDemo 的資料夾,然後在 mongoDemo 之下再建立 dbs 。然後開啟 MongoDB 如下輸入
mongod --dbpath dbs
我們必需要為 Node.js 準備一個 MongoDB 的 Driver,名為 mongojs 如下安裝
npm install -g mongojs
也許有些網友比較熟 mongodb 這個官方網站介紹的 module ,不過這篇文章開頭是用 mongo 這個 interactive shell 來介紹存取 MongoDB 的而 mongojs 的寫法和 mongo 比較像,所以就先採用這個,mongojs 的官方 GItHub 在。而對官方的 mongodb 比較有興趣的讀者可以看這篇解釋
接著準備一個名為 accessMongo.js 的檔案, 先輸入如下內容。

var express = require("express");
var app = express();
app.listen(8800);
var config = {
    "hostname":"localhost",
    "port":27017,
    "db":"mydb"
}
var dbURL = "mongodb://" + config.hostname + ":" + config.port + "/" + config.db;
var collections = ["users"];
var db = require("mongojs").connect(dbURL, collections);
 一開始筆者還是使用了 express,為了方便可以讓使用者看到結果。預計用
http://127.0.0.1:8800/create
http://127.0.0.1:8800/read
http://127.0.0.1:8800/update
http://127.0.0.1:8800/remove
這四個 GET 連結來來代表對 Database 執行 CRUD 四個動作。
接著我們會看到 config 這個 JSON Object 就是關於要存取 MongoDB Database 和  Collection 的設定。最後利用 mongojs 的 connect 和 MongoDB 連上線並設定要存取的 collection 是 users。接下來寫 read 的部分。


app.get("/create",function  (req, res) {
            db.users.save({name: "Michael", gender: "Male"}, function(err, saved) {
             if( err || !saved ) console.log("User not saved");
                 else {
                          res.send("User saved");
             }
                  res.end();
            });
});
在這裡我們可以看到 db.users.save() 幾乎和 mongo 終端機的寫法一樣,第一個參數就是 document 也就是 {name: "Michael", gender: "Male"},如果 saved 是正確的,代表寫入正確再來我們來寫 read,如下。

app.get("/read",function  (req, res) {
    db.users.find({}, function(err, users) {
      if( err || !users) console.log("No  users found");
      else users.forEach( function(user) {
        res.send(JSON.stringify(user)+"\n");
      } );
      res.end();
    });
});
用 db.users.find() 第一個參數是 {} 代表任一個 document 都符合,然後第二個參數是個 function,其中 users 就是從 users.find() 找到的所有的 document 就會存在這個 users array 裡面。然後用 users.forEach() 把每個 document 送給 Browser 呈現出來。如此一來就可以先用 Browser 測試,記得要先開啟 mongod 才能執行 node accessMongo.js。
在 Browser 輸入 http://127.0.0.1:8800/create
再來測試 read http://127.0.0.1:8800/read
恭喜大家表示成功了。
再來就是 update。新增程式碼如下。

app.get("/update",function  (req, res) {
    db.users.update({name: "Michael"}, {$set: {name: "James"}}, function(err, updated) {
      if( err || !updated ) console.log("User not updated");
      else {
          res.send("User updated");
          res.end();
      }
    });
});
和 mongo 的 interactive shell 非常像用 db.users.update 然後第一,二個參數就是 {name: "Michael"}, {$set: {name: "James"}} 和 Terminal 寫法一樣。沒有錯誤的話,
輸入 http://127.0.0.1:8800/update 就會在 Browser 看到如下
表示 update 成功,然後再輸入 http://127.0.0.1:8800/read
同一個 id 名字改成了 James。
最後我們要來測試一下 Delete ,新增如下程式碼

app.get("/remove",function  (req, res) {
    db.users.remove({gender: "Male"});
    res.send("removed ");
    res.end();
});
remove 這個 function 就是把尋找的條件丟入,就可以了,如果用 {} 表示要刪除全部的 documents。在Browser 輸入 http://127.0.0.1:8800/remove
 然後用 read 確定,輸入
空空如也,就是我們要的了。好吧,今天就到此了,咱們下次見。例子一樣放在 GitHub。謝謝各位看倌們。