2011年11月27日 星期日

NodeJS + Express 實作檔案上傳

Standard
Express Web Framework』是基於『Connect Middleware Framework』所開發,大部份的常見功能,藉由 Connect 本身支援或 Connect 的第三方(Third-party)模組,就可以實作出來。如果要實作檔案上傳的功能,可以使用第三方模組『connect-form』來達成。

直接透過 npm 安裝需要的模組:
npm install connect-form

在 app.js 中實作:
var express = require('express');
var form = require('connect-form');
var app = module.exports = express.createServer();

app.configure(function(){
  app.set('views', __dirname + '/views');
  app.set('view engine', 'jade');
  app.use(express.bodyParser());
  app.use(express.methodOverride());
  app.use(form({
    keepExtensions: true,
    uploadDir: __dirname + '/uploads'
  }));
  app.use(app.router);
  app.use(express.static(__dirname + '/public'));
});

app.get('/', function(req, res) {
  res.send('<form method="post" enctype="multipart/form-data">'
    + '<p>Image: <input type="file" name="image" /></p>'
    + '<p><input type="submit" value="Upload" /></p>'
    + '</form>');
});

app.post('/', function(req, res, next) {
  req.form.complete(function(err, fields, files){
    if (err) {
      next(err);
    } else {
      console.log('\nuploaded %s to %s'
        ,  files.image.filename
        , files.image.path);
      res.redirect('back');
    }
  });

  req.form.on('progress', function(bytesReceived, bytesExpected){
    var percent = (bytesReceived / bytesExpected * 100) | 0;
    process.stdout.write('Uploading: %' + percent + '\r');
  });
});

app.listen(3000);

後記

本文的範例與 express 所提供的是一樣的,只是在 app.configure() 裡多了幾行 app.use()。其實目的只是希望告訴讀者,務必要在 app.router 之前引入 connect-form。原因是在 Express Web Framework 設計上,每次處理 Request 時,會依照 app.use() 所引入的順序去使用模組。

由於原生的 app.router 並不處理上傳檔案的特殊需求,我們一定要在 Request 經過 app.router 導引並進入 app.post() 所定義的 handler 之前,讓 connect-form 先行遇處理和檢查 Request 是否為上傳檔案,進而擴充功能甚至進一步分析處理。

2011年11月23日 星期三

在 NodeJS + Express 使用 Cookie-based Session

Standard
開發網站的人應該對 Session 都不陌生,主要是用於 Client 與 Server 之間的溝通和狀態記錄。底層的實作細節本文就不多說明,若仍不理解他的用途,只要知道它可以讓你在不同頁面之間傳遞資料,像是登入狀態等。

一般標準的『Express』利用『Connect Middleware Framework』(之後簡稱 Connect)中的模組來實作 Session 的,而預設的方法是使用 MemoryStore 的方式,也就是將 Session 資料存放於記憶體上。若你開發的是中大型的網站,也可以使用 Connect 的第三方模組『connect-redis』,利用『Redis』這樣的 Key-Value Database 來存放 Session 的資料。這些在 NodeJS + Express 的各類開發文獻中相當常見,你可以從許多網站上找到使用方法。

不過,也許你仍不滿足,希望使用近年來興起的 Cookie-based Session,Connect 也有第三方模組『cookie-sessions』可以使用。

可以直接使用 npm 安裝:
npm install cookie-sessions

在你的 app.js 這樣使用:
var express = require('express');
var CookieStore = require('cookie-sessions');

var app = module.exports = express.createServer();

app.configure(function(){
 app.set('views', __dirname + '/views');
 app.set('view engine', 'jade');
 app.use(express.bodyParser());
 app.use(express.methodOverride());
 app.use(express.cookieParser());
 app.use(CookieStore({ secret: 'FredChien' }));
 app.use(app.router);
 app.use(express.static(__dirname + '/public'));
});

app.get('/', function(req, res) {
 req.session = { email: 'cfsghost@gmail.com' };

 /* Redirect to another page */
 res.redirect('/getsession');
});

app.get('/getsession', function(req, res) {
 if (req.session)
  res.end('This is Fred's email address:' + req.session.email);
});

app.listen(3000);

後記

其實 NodeJS 還算發展階段,相關資源不算很多,若想要實作過去常用的一些功能,會需要花點功夫請教 Google 大神。

2011年11月19日 星期六

千萬別相信 Android 上的應用程式

Standard
或許有些過於危言聳聽,但我們確實得正視這樣的議題:『資料無故從手機中消失』。這是可能發生的問題,原因在於 Android Dalvik VM 的 Garbage Collection 機制太過強大,如果程式開發者在寫程式的過程中都沒注意到這問題,那這肯定是個不定時炸彈。

話說,寫過 Android Application 的人應該都很習慣了變數宣告後,用完了就不管他,彷彿系統會有無限量供應的記憶體一般。不過,這對『慣C』一族來說,永遠是種荒繆的事,實在無法想像『憑什麼』不用釋放記憶體。其實說穿了,這一切是 VM 裡面的 Garbage Collection 在『Hold 住』場面,他會自動將目前 Reference 數量稀少的記憶體回收,以避免系統記憶體被用光後又不釋放。而所有回收的動作,有一系列『聰明』的演算法,有興趣的人可以自己去找相關資訊閱讀。

但是別以為 Garbage Collection 可以聰明到百分之百準確知道,什麼東西回收後是對系統或是應用程式無傷的,因為這就算要人腦來判斷,也一定會犯錯。更者,也別以為 Garbage Collection 不太會動當前正在跑的程式,因為只要符合條件,就算是你程式當前不斷在用的變數,也可能消失被回收,導致程式錯亂,就像被鬼憑空抓走一般。

如果你認為這只是理論,那你就錯了,這問題實際發生在最近筆者幫忙救火的案子裡。詳細情況是,這是瀏覽器相關的案子,當時我們宣告了幾個 Private 變數,然後使該變數在使用者操作觸發後,會被設定了一些數值供接下來的許多運算使用。一般情況下,我們實作的程式都很正常,不過,一旦開到比較複雜或充滿 JavaScript 的網站後,我們的程式彷彿就開始像中邪,完全不照我們的想像去執行,無論怎麼找問題,也找不出個所以然。

由於問題的發生是過程中變數無故被歸零,而我們又確定沒有任何一段自己的程式會讓他歸零,所以只剩下一種可能:『Garbage Collection 為了因應 WebView 過多的需求,而把變數給回收了。』

最後,我們將這變數設為 static 以解決這問題。不過,雖然用 static 暫時解決了問題,那也只是減少變數不被回收的機會,並不是最妥當的做法。

後記

別小看這問題,其實在 Android Framework 中也處處藏滿類似的問題,只是不知道什麼時候會爆出來而已。而這次救火的案子,就倒霉碰到鬼。

或許多半處於最上層的應用程式開發者,都覺得這些底下的事情,應該極穩定又可信賴,從來就不會在意這些東西,但是這可能會造成重大問題,相關知識還是要多充實。

2011年11月13日 星期日

自認帥氣的 WebSocket 簡單命令處理模型

Standard
最近參與的一個 Project,需要在 WebSocket 下實作許多機制,這著實讓人傷透腦筋。又由於 JavaScript 完全是非同步(asynchronous)的設計概念,若遇到必需等待 Server 回應後才能繼續執行的工作,處理起來可是一件異常麻煩的事。雖然很熟悉運用回調函數(Callback Function)和暱名函數(Anonymous Function)來做,但一層包一層的設計,總讓人覺得進入無限階層的夢境地獄,什麼時候醒來都不知道。

這是筆者要達成的需求:『發送自己定義的命令去 Server,然後等待 Server 回應,一旦收到 Server 回應就可以繼續往下處理。』

一般的情況下,多數人都會這樣做(這理使用 Socket.IO 來建立 WebSocket,並和 Server 要當前時間為例):
/* Define handler to receive response from server */
socket.on('Clock', function(data) {
    /* Do something */
    alert(data);
});

/* Send command to server */
socket.emit('Clock', 'Time');

可是,並非每次對 Server 的請求,都希望透過 alert() 跳出結果,我們有時只是想要利用 Server 給的時間做一些其他處理,雖然是下同樣的命令,但後續處理完全不一樣。於是,筆者,設計了這樣的方式,簡單的去處理這種問題:
var command = {
    'Time': [],
    'Date': []
    /* You can define more commands */
};
socket.on('Clock', function(data) {
    if (command[data.command].length) {
        for (var index in command[data.command])
            command[data.command][index](data.result);

        command[data.command] = [];
    }
});

/* API to Send command to server */
function sendCommand(domain, cmd, callback) {
    command[cmd].push(callback);
    if (command[data.command].length == 1) {
        socket.emit(domain, cmd);
    }
}

/* New method to send command and show result in alert window */
sendCommand('Clock', 'Time', function(data) {
    alert(data);
});

/* New method to send command and set window title with result */
sendCommand('Clock', 'Time', function(data) {
    window.title = data;
});

這邊需要在 Server-side 改變回送的格式,告知 Client 現在回應的結果是屬於哪一個命令:
data = {
    command: 'Time',
    result: '11:11:11'
};

這樣的命令處理模型,適合用在讀取即時性低的公用狀態或常數,因為這種做法只會送出一個命令給 Server,程式在發送命令時就會自動合併當下發出的『同樣要求』,直到 Server 回應後,同時間一併觸發所有相應的 callback function。除了能減少傳送的資料量,也避免與 Server 的溝通等待,提升一些效能。

後記

若是不希望合並要求,且想要明確等到自己發送的命令回應後,才觸發自己的 callback function,廣泛的做法是建立計數器,但這種方法就不在本文討論的泛疇。

2011年11月9日 星期三

使用 Jade 快速製作 HTML Template

Standard
如果你用 NodeJS 和『Express Web Framework』來寫網站,又參考官方給的範例程式,『Jade』應該是你首選的 Template Engine。不過 Jade 相當容易,如果你已經很熟悉 HTML,學習它不會是件難事。

Jade 用來表示每一個 HTML Tag 的格式:
<tag>(<attributes>) <inner content>

或是

<tag>(<attributes>)= <inner content>

ex: img(src='/images/logo.jpg')
ex: a(href='http://www.mandice.com/') Mandice
ex: title= This is Jade Example

使用縮排描述巢狀結構( 如同 Python):
Jade 語法:
html
  head
    title= This is Jade Example
  body
    Hello World

結果:
<html>
        <head>
                <title>This is Jade Example</title>
        </head>
        <body>Hello World</body>
</html>

賦予 Element 一個 ID 有兩種方式:
div(id='element1')

或是

div#element1

引入並使用另一個 Jade 檔案:
div#element1= partial('another.jade')

賦予 Tag 有多個 Attributes 有兩種方法:
使用『,』連接多個 Attribute:
img(src='/images/logo.jpg', title='This is a Logo')

使用換行連接多個:
img(
        src='/images/logo.jpg'
        title='This is a Logo'
)

建立 div 元素並指定 ID 的特殊例子,可在開頭省略 div:
#element1(class='test')

後記

在配合使用 Jade 時,當然還會碰到傳入值的情況,以及簡單的判斷式,不過這部份就留給實地操作的人自己去發掘了。 Have fun!

2011年11月8日 星期二

我如何看待 NodeJS

Standard
其實,這是篇推薦文,但推薦的角度和坊間很多人不一樣,所以這也是為什麼標題下『我如何看待 NodeJS』。

誠如大家所知,『NodeJS』原先被開發出來的目的,是為了解決 Web Server 效能的問題,其採用 Google Chrome/Chromium 使用的『Google V8 Javascript Engine』做為直譯器引擎。最特別的是,NodeJS 讓開發者完全可以用 JavaScript 來開發整個伺服器端的所有程式,這意味你可以不用再使用 Python、PHP、Perl、Ruby、ASP 等其他的語言,只需要學會 JavaScript 就可以將網站通通搞定。也因為幾乎所有的 Web 開發者都熟悉 JavaScript,NodeJS 很理所當然的成為一個 Web 圈內受很多人擁護的新技術,在各大 Cloud Hosting 也都開始有支援。

當然不只是如此, NodeJS 也吸收了各方面的經驗,提供應用程式框架(Framework)讓開發者可以快速開發。平心而論,比其他的語言更能兼固快速開發和效能的問題。

談到這裡,還是僅止於坊間多數人介紹的內容,筆者認為,如果只是這樣,也未免小覻了 NodeJS。

事實上,NodeJS 就是 Original JavaScript Without Browser + Libraries,在舊有的 JavaScript 抽離瀏覽器後,再上加上更多 System-level Library 的支援。這讓我們可以重新看待 JavaScript 這個語言,因為它不再只是限於『特定領域』使用,而是提升至『System Level』。

我們可以這樣認定:『JavaScript 已經可以和 Python 相提並論』。因此,使用 JavaScript 寫一支系統應用程式或是桌面應用程式,已經不再是夢,而是伸手可及。如果你有印像,HP WebOS 就是率先引入 NodeJS 的先行者,讓原本被視為 Web 技術的 JavaScript,跳出舊有框框,進入桌面系統的領域。

目前,NodeJS 仍持續發展中,許多函式庫支援也被慢慢的被實作出來。有時為了效能考量,我們也可以自己使用 C/C++ 為他開發更多 Library/Module。可以期待,未來 NodeJS 將會更為強大。

2011年11月5日 星期六

使用 NodeJS + Express + Socket.IO 實作 WebSocket 服務

Standard
一直處於假標準狀態的 HTML5,過程中一直有變化,讓人無法放心使用。不過到了今年,HTML5 總算進入了準決賽的階段,大部份功能已經確定下來,其中 WebSocket 就是最讓人關切的項目之一,因為這意味著未來 Web 不再只是單次性的觸發服務,而是可以讓使用者端與伺服器長時間處於連線狀態,並進行即時性的資料傳交換。

關於 WebSocket,彷間許多隨 HTML5 起舞的人,其實都說明不夠清楚,多半只強調能達成 TCP Socket 形式的連線,和無所不能的優點。其實,WebSocket 並非是我們所知的原生 Socket ,而只是一種架構於 HTTP Protocol 上的延伸定義,所以換句話來說,不能直接拿來連線到非 HTTP 以外的通訊協定。

至於它的做法,就是在瀏覽器向伺服器要資料時,在 Request Header 裡加上『Connection:Upgrade』來告訴伺服器:『我要改變連線形態』。然後伺服器會依照瀏覽器給的 WebSocket 相關標頭(header)和交握用的金鑰(Key),進行認證和改變處理方式。進入 WebSocket 連線的客戶端,就有如在使用 TCP Socket 或 Telnet 一樣,可以隨意在任意時間點傳送資料給伺服器處理,伺服器也可以在這連線下即時回應。所以就某方面來說,這剛好可以含蓋並取代過去 Web 1.0 的 Refresh Page 和 Web 2.0 後的 Long-polling 技術。

寫過網路程式的人都知道,如何定義 Protocol 才是傷腦筋的事,開啟了一個『類 Socket』意味著我們又回到了過去的年代,要自己解決 Protocol 的定義。不過也已經有現成的 Library 可以使用,許多問題我們已經不用擔心,本文將提到的 Socket.IO 就是其中之一。此外,由於 WebSocket 需要 Client/Server 同時實作,所以,這些 Library 多半都包括了兩部份的實作。

Server 端,使用 NodeJS + Express web framework + Socket.IO(0.8.6):
/* Module dependencies */
var express = require('express');
var io = require('socket.io');

/* Initializing Express Framework */
var app = module.exports = express.createServer();
app.listen(3000);

/* Create a Socket.IO instance, to establish WebSocket Service */
var socket = io.listen(app);
socket
    .on('connection', function(client) {
        console.log('A connection was established');
        client
            .on('pretty-girl', function(data) {
                console.log(data);
                socket.emit('ugly-man', 'get out of my way');
            });

        client.on('disconnect', function() {
            console.log('Server has disconnected');
        });
    });

Client 端,使用 Socket.IO(0.8.6):
socket = io.connect();
socket.on('connect', function() {
    console.log('Client has connected to the server!');
})
socket.on('disconnect', function() {
    console.log('The client has disconnected!');
});
socket.on('ugly-man', function(data) {
    console.log(data);
});
socket.emit('pretty-girl', 'hello!');

後記

回到原始的網路溝通,也有許多議題將再次面臨,舉例來說,客戶端(Client)可以同時發送很多道命令給伺服器,但伺服器可能每道命令完成時間有快有慢,以至有些命令是後發卻先完成。那伺服器如何告知客戶端,是哪一道命令完成了?而這種問題非常容易在多功能的服務中見到,尤其是遊戲伺服器。不過,對於過去有過伺服器設計經驗的人,顯然是不用怕。

備註:使用 Socket.IO 要注意版本的變化,因為各版 API 改動伏度都不小。而且也要注意 Client/Server 要使用同一版本,之前筆者犯錯,在 Client 使用官方提供的 CDN 版本,但在 Server 端使用自己下載的最新版本,結果一直無法讓 WebSocket 成功被建立。