贊助商連結

m.icrt.ianwu.tw 開發紀實

緣由

一切的開始都是因為我想知道 ICRT 現在播放的歌曲是哪一首,在 ICRT 的網站上有提供目前播放的歌曲資訊,但是這個網頁是 desktop size 的網頁,所以用智慧型手機看很不方便。所以就想開發個 App 來擷取 ICRT 網頁上的歌曲資訊。於是去年完成了 Android 上的 ICRT 現正播放的 App,功能相當簡單就是抓 ICRT 的網頁回來 parser 出歌曲資訊(詳細操作可以看這篇文章)。

最近我改用 iphone,所以也想在 iphone 上看 ICRT 現在播放的歌曲,但是 iphone app 的上架成本太高,要花 USD 99 註冊開發者帳號才能上架,所以轉而尋求 mobile web 的解決方案,所以才會誕生 http://m.icrt.ianwu.tw 這個 mobile website。

架構

一開始考慮的架構是 j2ee + mysql,因為之前有玩過 Play Framework,再加上本身擅長的就是 java,所以採用 java 似乎是很理所當然。不過,後來想想之前參加 Yahoo Open Hack Day 用 Node.js 的開發經驗還不錯,同時老狗也想學學新把戲,所以最後就決定用 node.js。

至於後端的儲存有考慮過來玩個 NoSQL,不過後來想想,這個小小的 mobile website 也不會產生大量資料,同時我對 NoSQL 也不是很熟,而且 m.icrt 也已經用了 node.js 這個新玩意,實在不宜再加新玩具了,最後就定案 node.js + mysql 來搞 http://m.icrt.ianwu.tw 這個 mobile site。

開發期

用 test driven 的方式,每個 module 跟 function 都有 test case,這樣才能確保 module 的品質。

用上的 node.js module

mobile web site

一開始有考慮是否要用 responsive web 但是因為本人不是專業的 web design 這部份的知識很缺乏,只是知道有 responsive web 這個技術,所以還是走保險一點的路,單作 mobile web 就好了。

這邊選用的 mobile web framework 是自己比較熟悉的 jquery mobile 同時又找到 Graphite 這個漂亮的 theme,套上漂亮的 theme 後,整個 m.icrt 質感提昇不少。

加上 iphone home screen icon

雖然是 mobile website,但是因為 iphone 可以把網站加到 home screen 上,預設會是網頁的截圖,

但是可以透過 html 指定 icon

1
<link rel="apple-touch-icon" href="/custom_icon.png"/>

這張圖就是使用前與使用後,是不是差很多呢~

ref

用 command line 的方式 update database

要讓 javascript 可以像 bash 執行也很簡單,只要加上 #!/usr/bin/env node 再設定為 755 這樣就可以執行了。

另外,可以搭配 commander 這樣可以更方便地打造 command line javascript

最後 m.icrt 必須定時到 ICRT 官方網站去抓目前播放的歌曲,這裡用最簡單的方式來達成定時的機制,就是用 cron job 的方式定時執行。

Web Storage 存最愛歌曲

為了簡化架構,所以 user 的最愛歌曲是用 web storage 來儲存,根據 Can I use 的顯示,有 90% 以上的支持度,所以就大膽地放心使用。但是,用 Web Storage 儲存我的最愛會有幾個缺點,第一個就是當 user 清除資料的時候就會被清除掉,另外一個缺點就是無法跨裝置,因為是存在裝置上的瀏覽器。不過,為了簡化架構以及達成易用的目的,我還是決定採用 web storage 來儲存最愛歌曲,最後再透過 export 的功能來匯出就好了。

Export 最愛歌曲

因為上述 Web Storage 的缺點,所以設計一個 export 的功能讓 user 可以透過 email 匯出他的最愛歌曲,不過 m.icrt 是個 mobile web,所以要呼叫 native 的 mail app 有點困難(我 google 過,沒啥答案,如果大大有解法可以留言跟我說),所以就決定採用 Amazon Simple Email Service (Amazon SES),透過 AWS SES 把信寄給 user。

使用 AWS SES 除了不用自己維護 mail server 之外,同時 AWS SES 一天免費的 quota 是一萬封,對我來說已經非常夠用了,設定 SES 也挺簡單的,只要加一個 user 取得 Access Key ID 跟 Secret Access Key,接著再認證 sender email address,這樣就可以寄信了。

最愛歌曲 email 樣板

絕大多數的 email client (gmail, hotmail, outlook express) 都有支援 html format 的電子郵件,所以我希望 export 出去的最愛歌曲可以套用好看得 email 樣板,這樣收到 export 才會感到心情愉快 XD

拉回正題,因為我不是專業的 web designer,所以只能上網找看看有沒有好看的樣板,Email BlueprintsMailChimp 釋出的樣板,裡面有許多的樣板,單欄、兩欄、三欄都有,還有 responsive templates 可真是貼心,BTW,授權是採用 CC 姓名標示-相同方式分享 授權,所以也沒有授權的問題。

i18n

nodejs i18n 的解決方案有蠻多的,隨便搜尋就一大堆,在試用了幾套之後決定採用 i18next-node

決定採用 i18next-node 是因為置換語系字串的 function 做在 template 這一層,所以不會動到 routes 裡面的 code,這樣我覺得乾淨很多。

同時預設是支援 jade 所以只有小小地修改樣板就完成 i18n 的支援。

不過 i18next-node 還蠻強大的,之後會再有專篇來分享 i18next-node

favicon

最後還要製作一個 favicon,因為本人也不是個專業的 visual designer,所以 favicon 的確挺苦惱我的,但是還是得硬著頭皮上,最後就用 favicon.cc 土炮一個 favicon 出來

至於在 expressjs 要換 favicon 也非常簡單,只要在 app.js 裡面 favicon middleware 加上 favicon 的路徑就可以了

1
app.use(express.favicon(__dirname + '/public/images/favicon.ico'));

這就是使用前與使用後的差異,是不是提昇了些許的專業感呢

PS 瀏覽器會 cache favicon,如果修改後 favicon 沒變化的話,可以透過清除 browser cache 的方式解決。

上線期

檢查 pakage.json dependencies

pakage.json dependencies 要檢查兩個地方:

第一個是檢查是否有多餘的 dependencies,常常在開發時期會亂試一些 lib,試完又忘記移除,所以在要準備上線時再檢查一次,移除不需要的 dependencies。

第二個是 lib 的版本,在 dependencies 裡面的 lib 可以用 "*",這樣 npm 就會去抓最新版本,但是這樣會產生一些問題,像是網站需要 scale 要將網站 deploy 到另外一台 server 時,這樣在重新 npm install 又會去抓最新版本,如果一切沒事就沒事,最怕就是新版的 lib 有地雷,這樣就內牛滿面了,所以一定要確認好目前使用的 lib 的版本。

另外一個確認版本的好方法就是在 npm install 的時候加上 -save 這個選項,這就 npm 就會把 lib 跟版本寫進 package.json,像是這樣用

1
npm install mysql -save

這樣 npm 就會把 mysql 以及剛剛安裝的版本寫到 package.json 內(要先有 package.json 這樣 -save 才會發生作用)。

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"author": "Ian Wu <onlinemad@gmail.com>",
"name": "TestForBlog",
"description": "This is test for blog",
"version": "0.0.1",
"main": "index.js",
"dependencies": {
"mysql": "~2.0.0-alpha8"
},
"devDependencies": {
"mocha": "*"
}
}

檢查 code 裡面是否有 console.log

通常在寫 code 的時候會放一些 console.log 來印一些變數,這樣在程式執行時就可以觀察到變數的狀態。不過,在 util 裡面也有一個叫做 util.log,也是印一些東西,與 console.log 的差別在於 util.log 會帶時間戳記上去,所以這樣在寫入 log 檔的時候就會把時間寫進去,這樣以後在調閱 log 檔的時候才知道在什麼時間發生什麼事情。

調整 express 的設定

express 專案要上 production 有一些需要調整的地方,以下是一些調整的地方

調整 app.js

一開始初始化 express 專案,你可以看到下面這些 code,這些是 express 基本的 middleware。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// all environments
app.set('port', process.env.PORT || 3000);
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(express.cookieParser('your secret here'));
app.use(express.session());
app.use(app.router);
app.use(require('stylus').middleware(__dirname + '/public'));
app.use(express.static(path.join(__dirname, 'public')));
// development only
if ('development' == app.get('env')) {
app.use(express.errorHandler());
}

為了因應 production 跟 development 的環境,需要一些修改,

區隔 production 跟 development 的 middleware

這裡改用 app.configure('env', function(){}); 來區隔 production 跟 development,因應不同的環境 load 不一樣的 middleware,像是下面的 code 這樣。

1
2
3
4
5
6
7
8
9
app.configure(function(){
// common middleware
});
app.configure('development', function(){
// development middleware
});
app.configure('production', function(){
// production middleware
});

調整 middleware

有些 middleware 會因為 production 跟 development 有不同的設定,像是 logger 跟 session 這兩個 middleware。

logger 的部份,在 development 的時候就全寫到 console 就好了,在production 時就必須寫到 logger 裡面,這樣以後才能從 logger 裡面找問題。

session 的部份,在 development 的時候用內建的就可以了,不過在production 時就必須使用 MemoryStore 來儲存 session 這部份等下會有更多的解釋。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// development only
app.configure('development', function(){
app.use(express.logger('dev'));
app.use(express.session());
});
// production only
app.configure('production', function(){
var fs = require('fs');
var access_logfile = fs.createWriteStream('../log/icrt.www.log', {flags: 'a'});
app.use(express.logger({stream: access_logfile }));
var mcStore = require('connect-memcached')(express);
var mc = new mcStore({hosts: "localhost:11211"});
app.use(express.session({key: "icrt", secret: "your secret here", store:mc}));
});

修改 cookieParser 的 secret key

在 cookie middleware 就已經有告訴你要換掉 secret key app.use(express.cookieParser('your secret here')); 這邊就自行產生一個 secret key 就可以了。

修改完的 app.js

下面就是修改過後的 app.js ,這樣就可以因應 production 跟 development 不同環境使用不同的 middleware。

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
// all environments
app.configure(function(){
app.set('port', process.env.PORT || 3000);
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.use(express.favicon());
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(express.cookieParser('your secret here'));
app.use(i18next.handle);
app.use(require('stylus').middleware(__dirname + '/public'));
app.use(express.static(path.join(__dirname, 'public')));
});
// development only
app.configure('development', function(){
app.use(express.logger('dev'));
app.use(express.session());
app.use(app.router);
app.use(express.errorHandler());
});
// production only
app.configure('production', function(){
var fs = require('fs');
var access_logfile = fs.createWriteStream('../log/icrt.www.log', {flags: 'a'});
app.use(express.logger({stream: access_logfile }));
var mcStore = require('connect-memcached')(express);
var mc = new mcStore({hosts: "localhost:11211"});
app.use(express.session({key: "icrt", secret: "your secret here", store:mc}));
app.use(app.router);
app.use(express.errorHandler());
});

ref

設定環境變數

express 預設會以 development 模式 run web application,要切換成 production 模式只要 export 一個環境變數,這樣 express 就會以 production 模式來 run web application。

1
export NODE_ENV=production

安裝 memcached

export production 環境變數,如果直接 run web application 會收到下面的警告。

1
2
3
Warning: connection.session() MemoryStore is not
designed for a production environment, as it will leak
memory, and will not scale past a single process.

這是因為 session 是存在記憶體,同時 express 並不會去清除過期的 session,所以時間越久就會吃越多的記憶體。

解決的方法很簡單,只要把 session 存到外部的 MemoryStore 就可以,同時 express 支援多種 DB 當作 MemoryStore 像是 Redis, MongoDB 或是 memcached。

這裡我選擇 memcached,因為架設 memcached 很簡單,同時速度上也不錯,不過缺點就是 session 的總量是根據 memory 的大小來決定的。

不過我想 m.icrt 也不會有太多人同時在線上,所以應該是還好,等變大之後再來換 MemoryStore。

ref

建立一個專用的 user account

一開始在測試時是用自己的帳號,但是因為自己的帳號是有 sudo 的權限,所以一定要開一個帳號是專門 run node application 用,同時這個帳號也要作一些保護,像是 deny ssh 的權限、改用 rbash 限制 user 只能逛自己家。

ref

設定 nginx

到了最後一個步驟,就是設定 nginx 的 reverse proxy,因為 expressjs 是用 port 4000,一般 web site 都是走 port 80,當然 nodejs 也可以開 port 80,但是需要使用 root 才能開 port 80,這樣風險太高,所以採用 nginx 的 reverse proxy 就是最好的方式。

這是我用的設定,很基本的設定,如果有更好的設定,也可以留言給我

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
upstream micrt {
server 127.0.0.1:4000;
}
server {
server_name m.icrt.ianwu.tw;
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://micrt/;
proxy_redirect off;
}
}

用 forever run app.js

用 node 指令 run web application 如果碰到 exception 整個 node 就會停下來,所以我們可以用 forever 來 run web application。forever 會監測 node process 的狀態,如果死掉了就會再把 web application 叫起來,除此之外還可以指定 log 檔的位置,這樣就會把 console 的 log 寫到 log 檔裡面,這樣才能夠留 log 檔以便於後面的追蹤。

一般我常下的指令是這樣 -a 是 append log -o 指定 stdout log 檔的位置 -e 指定 stderr log 檔的位置。

1
forever start -a -o ~/log/icrt.www.log app.js

結語

從 4/28 第一個 commit 開始,m.icrt 斷斷續續寫了兩個月(這篇文章也斷斷續續寫了兩個月 XD),終於到了收尾的階段,心中所想的功能都實現出來,也學到了許多新的東西。

之後還有想要強化的地方,像是導入 backbone.js 之類前端的 framework,還有換到像是換到 MariaDB 之類的。

最後,謝謝收看這篇文章,如果有什麼可以改進的地方,也可以留言給我 :)