[NodeJS] 第一次 Node 就上手 Part 2

凡是都有 Part 2,所以這時候來一個 Part 2 也是很合理的(挺,上一次介紹了 expressjs, jade, socket.io,這一次來介紹一下有關於 Session 的部份,以及如何與資料庫做結合。

首先,你必須知道什麼叫做 Session,以及什麼叫做資料庫。

Session? Database?

RTFM

TL;DR

Session in NodeJS

其實 NodeJS 並沒有特別實做 Session 這個項目,所以第三方套件就有相當多的選擇,

上述的 Express 與 connect 其實是講同一件事情,Express 本身是使用 Connect (Connect Middleware Framework) 所提供的方法,所以以下我會以 Connect 來稱呼他們。雖然我很不想講,不過關於 Connect 所提供的 Session 可以去看官方的介紹

然而,connect-redis 其實也是 Connect 的一個延伸,由於 Connect 所儲存 Session 的方式,是把他儲存在記憶體中(MemoryStore),這是預設的。而這個第三方套件,是可以將 Session 儲存在 Redis 這個資料庫中。關於 Redis 我們往後再做介紹,當然也有 connect-mongo 可以用就是了。

最後,你也可以用前陣子很流行的 Cookie base 的儲存方式,來放你的 Session。cookie-sessions 就是一個第三方套件,用於將 Session 放在 Cookie 裡面,不過,他需要用到 Connect,而有趣的地方是,Connect 本身也提供了 Cookie session middleware,所以要怎麼用就見仁見智。

DO IT BY YOURSELF, 如果真的那麼想做,那這裡有一篇文章可以參考一下,

node.js and Session Handling

Session in Connect

既然人家都把輪子準備好了,那麼我們直接拿來滾也是很合理的。

(function() {
  var app, express;

  express = require('express');

  app = express.createServer();

  app.configure(function() {
    return this.use(express.session({
      secret: "@#$TYHBVGHJIY^TWEYKJHNBGFDWGHJKUYTWE#$%^&*&^%$#"
    }));
  });

  app.listen(3000, function() {
    return console.info("Express server listening on port " + (this.address().port) + " in " + process.env.NODE_ENV + " mode");
  });

}).call(this);

然後我們執行了一下,服務正常啟動了,但是瀏覽器連線的時候,馬上出了錯誤!

$ node test.js
Express server listening on port 3000 in development mode

TypeError: Cannot read property 'connect.sid' of undefined
    at Object.session [as handle] (/home/hinablue/node-in-session/node_modules/express/node_modules/connect/lib/middleware/session.js:299:32)
    at next (/home/hinablue/node-in-session/node_modules/express/node_modules/connect/lib/http.js:203:15)
    at Object.handle (/home/hinablue/node-in-session/node_modules/express/lib/http.js:83:5)
    at next (/home/hinablue/node-in-session/node_modules/express/node_modules/connect/lib/http.js:203:15)
    at HTTPServer.handle (/home/hinablue/node-in-session/node_modules/express/node_modules/connect/lib/http.js:216:3)
    at HTTPServer.emit (events.js:70:17)
    at HTTPParser.onIncoming (http.js:1514:12)
    at HTTPParser.onHeadersComplete (http.js:102:31)
    at Socket.ondata (http.js:1410:22)
    at TCP.onread (net.js:374:27)

少了什麼東西嗎?是的,依照 Express 官方說明,我們還需要 cookieParser() 這個東西,所以我們把他加上去,

(function() {
  var app, express;

  express = requi/home/hinablue/workspace/works/jsdc/re('express');

  app = express.createServer();

  app.configure(function() {
    this.use(express.cookieParser());
    return this.use(express.session({
      secret: "@#$TYHBVGHJIY^TWEYKJHNBGFDWGHJKUYTWE#$%^&*&^%$#"
    }));
  });

  app.listen(3000, function() {
    return console.info("Express server listening on port " + (this.address().port) + " in " + process.env.NODE_ENV + " mode");
  });

}).call(this);

然後我們再試一次,就正常運作了呢,接下來我們做一個 / 來讓瀏覽器可以顯示一點東西,

(function() {
  var app, express;

  express = require('express');

  app = express.createServer();

  app.configure(function() {
    this.use(express.cookieParser());
    return this.use(express.session({
      secret: "@#$TYHBVGHJIY^TWEYKJHNBGFDWGHJKUYTWE#$%^&*&^%$#"
    }));
  });

  app.get("/", function(req, res) {
    console.info(req.session.item);
    req.session.item = 'Hello World';
    return res.send('Hello World');
  });

  app.listen(3000, function() {
    return console.info("Express server listening on port " + (this.address().port) + " in " + process.env.NODE_ENV + " mode");
  });

}).call(this);

然後我們執行之後,打開瀏覽器拜訪一下 Hello World

$ node test.js
Express server listening on port 3000 in development mode
undefined
Hello World

你會發現,我們第一次訪問的時候,session.item 是空值(undefined),而第二次訪問的時候,session.item 已經變為 Hello World 了。這樣是一個簡易的 Session 在 Express 當中的操作。如果你需要更多操作的方法,可以查看官方的文件說明

Session in MongoDB

在 Express 當中,Session 預設儲存的方式是記憶體。如果你要儲存在資料庫中也是可以的,關於資料庫呢,這邊除了 MongoDB 之外,還有另外一個,叫做 Redis。他與 MongoDB 一樣是 key/value 的資料庫。差別大概在他有 VMware sponsored 這樣吧(喂

由於先前我們已經介紹過 MongoDB,所以這邊先以 MongoDB 的儲存方式來介紹。雖然 Connect 推薦使用 Redis 來作儲存媒介,不過也沒硬性規定就是。

BUT!

原生的寫法是行不通的,

(function() {
  var app, express, mongo, mongoServer;

  express = require('express');

  mongo = require('mongodb');

  mongoServer = new mongo.Server('localhost', 27017, {
    auto_reconnect: true,
    pooSize: 5
  });
/home/hinablue/workspace/works/jsdc/
  app = express.createServer();

  app.configure(function() {
    this.use(express.cookieParser());
    return this.use(express.session({
      secret: "@#$TYHBVGHJIY^TWEYKJHNBGFDWGHJKUYTWE#$%^&*&^%$#",
      store: new mongo.Db('mysession', mongoServer)
    }));
  });

  app.get("/", function(req, res) {
    console.info(req.session.item);
    req.session.item = 'Hello World';
    return res.send('Hello World');
  });

  app.listen(3000, function() {
    return console.info("Express server listening on port " + (this.address().port) + " in " + process.env.NODE_ENV + " mode");
  });

}).call(this);

馬上就會出現錯誤,

$ node test.js
Express server listening on port 3000 in development mode
TypeError: Object #<Db> has no method 'get'
    at Object.session [as handle] (/home/hinablue/node-in-session/node_modules/express/node_modules/connect/lib/middleware/session.js:318:11)
    at next (/home/hinablue/node-in-session/node_modules/express/node_modules/connect/lib/http.js:203:15)
    at Object.cookieParser [as handle] (/home/hinablue/node-in-session/node_modules/express/node_modules/connect/lib/middleware/cookieParser.js:44:5)
    at next (/home/hinablue/node-in-session/node_modules/express/node_modules/connect/lib/http.js:203:15)
    at Object.handle (/home/hinablue/node-in-session/node_modules/express/lib/http.js:83:5)
    at next (/home/hinablue/node-in-session/node_modules/express/node_modules/connect/lib/http.js:203:15)
    at HTTPServer.handle (/home/hinablue/node-in-session/node_modules/express/node_modules/connect/lib/http.js:216:3)
    at HTTPServer.emit (events.js:70:17)
    at HTTPParser.onIncoming (http.js:1514:12)
    at HTTPParser.onHeadersComplete (http.js:102:31)

不要問我為什麼,這個問題是 mongodb-native 中的動作,跟 Connect 中的作法不同所導致。所以,請使用 connect-mongo

(function() {
  var app, express, mongoStore;

  express = require('express');

  mongoStore = require('connect-mongo')(express);

  app = express.createServer();

  app.configure(function() {
    this.use(express.cookieParser());
    return this.use(express.session({
      secret: "@#$TYHBVGHJIY^TWEYKJHNBGFDWGHJKUYTWE#$%^&*&^%$#",
      store: new mongoStore({
        host: 'localhost',
        port: 27017,
        db: 'mysessions',
        collection: 'sessions'
      })
    }));
  });

  app.get("/", function(req, res) {
    console.info(req.session.item);
    req.session.item = 'Hello World';
    return res.send('Hello World');
  });

  app.listen(3000, function() {
    return console.info("Express server listening on port " + (this.address().port) + " in " + process.env.NODE_ENV + " mode");
  });

}).call(this);

然後我們從 console 進去 mongo 查看一下,

$ mongo
MongoDB shell version: 2.0.4
connecting to: test
> show dbs
local (empty)
mysessions    0.203125GB
> use mysessions
switched to db mysessions
> show collections
sessions
system.indexes
> db.sessions.find()
{ "_id" : "vB4Gd1rZJQ8e6s0rbE0rEqrR.c0VnxIUGAxsQfi9zA2XoQ9lV9oIs5ULsn+Y/99wu1pI", "session" : "{\"lastAccess\":1334752463452,\"cookie\":{\"originalMaxAge\":14400000,\"expires\":\"2012-04-18T16:34:23.452Z\",\"httpOnly\":true,\"path\":\"/\"},\"item\":\"Hello World\"}", "expires" : ISODate("2012-04-18T16:34:23.452Z") }
>

這樣你就看到我們剛剛存的東西了。

Session in Redis

另外就是 Redis 了,首先要安裝,

$ sudo apt-get install redis-server

然後我們改寫一下,

(function() {
  var app, express, redisStore;

  express = require('express');

  redisStore = require('connect-redis')(express);

  app = express.createServer();

  app.configure(function() {
    this.use(express.cookieParser());
    return this.use(express.session({
      secret: "@#$TYHBVGHJIY^TWEYKJHNBGFDWGHJKUYTWE#$%^&*&^%$#",
      store: new redisStore()
    }));
  });

  app.get("/", function(req, res) {
    console.info(req.session.item);
    req.session.item = 'Hello World';
    return res.send('Hello World');
  });

  app.listen(3000, function() {
    return console.info("Express server listening on port " + (this.address().port) + " in " + process.env.NODE_ENV + " mode");
  });

}).call(this);

然後用瀏覽器跑一下,我們再用 redis-cli 來看看儲存的結果,

$ node test.js
Express server listening on port 3000 in development mode
undefined
Hello World

$ redis-cli
redis 127.0.0.1:6379> KEYS *
1) "sess:8nQiHfxTHfouhSaB5tgj1cQY.UP0VLd1qOneh0D30H2+ivDyYLlX/GcXLdIl9dczWAlg"
redis 127.0.0.1:6379> MGET "sess:8nQiHfxTHfouhSaB5tgj1cQY.UP0VLd1qOneh0D30H2+ivDyYLlX/GcXLdIl9dczWAlg"
1) "{\"lastAccess\":1334753540196,\"cookie\":{\"originalMaxAge\":14400000,\"expires\":\"2012-04-18T16:52:20.197Z\",\"httpOnly\":true,\"path\":\"/\"},\"item\":\"Hello World\"}"
redis 127.0.0.1:6379> exit

這樣就會發現,我們剛剛所儲存的 Session 了!

Session in Cookie

最後我們來說一下 Cookie base 的儲存方式。這個儲存方式有 Cookie 先天的限制,所以,如果你的 Session 所儲存的內容過多(大於 4kb),或是你有些敏感的資料的話,是不建議使用這種方式來儲存的。

(function() {
  var app, connect, express;

  connect = require('connect');

  express = require('express');

  app = express.createServer();

  app.configure(function() {
    this.use(express.cookieParser('@#$TYHBVGHJIY^TWEYKJHNBGFDWGHJKUYTWE#$%^&*&^%$#'));
    return this.use(express.cookieSession());
  });

  app.get("/", function(req, res) {
    console.info(req.session.item);
    req.session.item = 'Hello World';
    return res.send('Hello World');
  });

  app.get("/page2", function(req, res) {
    console.log(req.session.item);
    return res.send(req.session.item);
  });

  app.listen(5000, function() {
    return console.info("Express server listening on port " + (this.address().port) + " in  mode");
  });

}).call(this);

執行結果呢?

$ node test.js
Express server listening on port 3000 in development mode
Hello World
Hello World

你會發現,我一打開瀏覽器就會出現 Session 的資料了,為什麼?因為這是用 Cookie 儲存的關係,所以不要問我為什麼。另外,注意! express.cookieParser() 裡面的 secret 一定要寫,不然會錯誤喔!

我這裡貼上 Header 的結果給大家參考一下,

Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Charset:UTF-8,*;q=0.5
Accept-Encoding:gzip,deflate,sdch
Accept-Language:zh-TW,zh;q=0.8,en-US;q=0.6,en;q=0.4
Cache-Control:max-age=0
Connection:keep-alive
Cookie:connect.sid=dj5sq4si22fODOemcEpTrGzT.h%2BHj4JNtvtQpJp%2BA6pUdh1xiEzTv0hoZH%2BLy%2FKr9%2F1g; connect.sess=j%3A%7B%22item%22%3A%22Hello%20World%2C%20asdjfkla%3Bdfja%3Blskdfja%3Bsldkfja%3Bsldfjas%3Bldfja%3Bsldfkjas%3Bldfja%3Bsldfjka%3Bdlfja%3Bsldfjka%3Bsldfja%3Bldkjfa%3Bsldfj%22%7D.41ZpYhK3SpKO5CxWXwa1eJlODvp%2Fj1%2BE9a0dbFL9emk
Host:localhost:5000
User-Agent:Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.24 Safari/536.5

請注意 Cookie 的部份,這個部份有 connect.sess 這是 Connect 預設的,裡面是加密過的資料,當你所儲存的 Session 越多時,這個東西就會越來越長。如果過長會炸掉,就這樣!

結語

Session 還是用 DB 來存會好一點,就這樣啦(飄走

Hina Chen
偏執與強迫症的患者,算不上是無可救藥,只是我已經遇上我的良醫了。
Taipei