Email me: jollen # jollen.org

more: Jollen 的 Embedded Linux 教育訓練

« June 2014 | (回到Blog入口) | September 2014 »

July 2014 歸檔

July 23, 2014

Node.js 入門, #3:URL Routing 觀念與實作

本文章採用 Markdown 語法撰寫(why?),若無法閱讀內文,請點擊這裡

延續前二篇文章的範例,接著介紹 Node.js 最基本也最重要的觀念-URL Routing。範例網址:https://github.com/jollen/html5-websocket-nodejs

URL Routing

這是處理 URL(HTTP Request)與 Query String 的核心觀念,這是利用 Node.js 開發 Web Service 的重要步驟。讓我們先來了解 Routing 的寫法,再來探討它的觀念。

首先,先改寫 server.js 模組如下:

// 03-route/server.js
 1 var http = require("http");
 2 var url = require("url");
 3 
 4 function start(route) {
 5   function onRequest(request, response) {
 6     var pathname = url.parse(request.url).pathname;
 7     console.log("Request for " + pathname + " received.");
 8     console.log("Request url: " + request.url);
 9 
10     route(pathname);
11 
12     response.writeHead(200, {"Content-Type": "text/plain"});
13     response.write("Hello World");
14     response.end();
15   }
16 
17   http.createServer(onRequest).listen(8888);
18   console.log("Server has started.");
19 }
20 
21 // Export functions
22 exports.start = start;

Routing 觀念的主要用途是處理 URL,所以我們利用 url 模組來取出 URL 裡的 pathname,並將 pathname 交給 route() 函數來處理。這裡很特別的地方是,start() 函數裡所呼叫的 route()函數,是透過參數列傳遞進來的,這和模組的 Closure 特性有關係,觀念說明如下:

  • route() 函數實作在 router 模組,而不是 server.js 模組
  • 目的是將 Routing 的功能,拆成單獨的模組來維護
  • route() 函數由 router.js 模組提供,必須引用 router 模組

在這裡範例裡,我們由 index.js 引入 router.js 模組,並且將裡頭的 router() 函數,透過參數列交給 start()函數。如此一來,start() 也可以呼叫到 route() 函數了。

這個部份,可以選擇另外一個實作方法:在 server.js 裡引用 router.js 模組。不過,就概念上來說,範例的實作方式好一些。原因如下。

  • Decompostion:將 router.js 與 server.js 模組的相依性解除
  • Component-based software engineering:將 router.js 與 server.js 做成獨立的模組,他們之間如果沒有相依性,就可以做為二個不同的模組來使用。例如,將 router.js 模組抽換成其它專案的 Routing 模組,並且 server.js 可以重用。Node.js 的軟體架構,主軸是模組化,即 Component-based 軟體工程的觀念

目前透過 npm 指令,不但可以安裝到各式不同的 Node.js 模組,甚致可以將自已的模組出版(Publish)給其他開發者使用。

Node.js 的事件處理機制,採用典型的 Callback Functions 做法。

接著,要開始處理 Pathname 與 Query String 的解析,請先參考圖 2.2。

圖 2.2:API 與 Query String 圖 2.2:API

改寫 index.js 主程式如下:

// 03-route/index.js
1 var server = require("./server");
2 var router = require("./router");
3 
4 server.start(router.route);   // 傳遞route物件

將 Routing 的演算法製作成獨立的模組,並將 router() 函數傳遞給 start(),函數的參數,可以傳遞一個函數,這個觀念就是 Lambda。router.js 完整程式碼如下:

// 03-route/router.js
1 function route(pathname) {
2     console.log("Route this request: " + pathname);
3 }
4 
5 exports.route = route;

請注意,這個範例雖然陽春,但是展示了一個非常重要的觀念:

  • 函數就是物件,所以我們把 route 物件交給 start() 函數,讓 start() 函數去使用物件
  • 直接在 start() 裡呼叫 route() 函數也可以,為什麼不這樣做?因為這不是 JavaScript 的觀念,倒是有點像是標準 C 語言呼叫函數的觀念,同時也會降低程式碼的可維護性

接下來,要讓 route() 解析 pathname。例如,我們定義了二個 API:

  • http://localhost:8080/start,用來連接伺服器並接收即時訊息
  • http://localhost:8080/send,送出文字訊息

分別要處理二個 pathname 如下:

  • /start,呼叫專屬的 Handler 'start()' 來處理
  • /send,呼叫專屬的 Handler 'send()' 來處理

實作的關鍵來了,我們要利用 Request Handler 的觀念來實作,首先,修改 index.js 如下:

// 04-request-handlers/index.js
 1 var server = require("./server");
 2 var router = require("./router");
 3 var handlers = require("./requestHandlers");
 4 
 5 // 使用 Object 來對應 pathname 與 request handlers
 6 var req = {
 7    "/": handlers.start,
 8    "/start": handlers.start,
 9    "/send": handlers.send
10 };
11 
12 // 傳遞 request handler 
13 server.start(router.route, req);

上述的二個 Handler 函數:start() 與 send() 將另行實作於 requestHandlers 模組。requestHandlers 模組匯出 start() 與 send() 函數,分別處理相對應的 pathname。

因此,主程式在第 6 行到第 10 行的地方,利用 req 物件來對應這個關係。在呼叫 start() 時,將 req 物件傳入。

另外,JavaScript 雖然不是物件導向式語言,但仍要以物件的觀念來撰寫。所以,我們將 req 以 var 語法定義成 object。很多時候,或許也能以 associative array 來實作,但並不是很建議。

以下就是一個以 associative array 的實作範例,原則上不推薦:

 1 var server = require("./server");
 2 var router = require("./router");
 3 var handlers = require("./requestHandlers");
 4 
 5 // 使用 associative array 來對應 pathname 與 request handlers
 6 var req = {};
 7
 8 req["/"] = handlers.start;
 9 req["/start"] = handlers.start;
10 req["/send"] = handlers.upload;
11 
12 // 傳遞 request handler 
13 server.start(router.route, req);

修改後的 router.js 如下:

// 04-request-handlers/router.js
 1 function route(pathname, handlers, response) {
 2     console.log("Route this request: '" + pathname + "'");
 3 
 4     // 檢查 pathname 是否有對應的 request handlers
 5     if (typeof handlers[pathname] == "function") {
 6         handlers[pathname](response);
 7     } else {
 8         console.log("No request handler for this pathname: '" + pathname + "'");
 9     }
10 }
11 
12 exports.route = route;

再次修改 server.js 如下:

// 04-request-handlers/server.js
 1 var http = require("http");
 2 var url = require("url");
 3 
 4 function start(route, handlers) {
 5   function onRequest(request, response) {
 6     var pathname = url.parse(request.url).pathname;
 7     console.log("Request for " + pathname + " received.");
 8 
 9     route(pathname, handlers, response);
10 
11     response.writeHead(200, {"Content-Type": "text/plain"});
12     response.write("Hello World");
13     response.end();
14   }
15 
16   http.createServer(onRequest).listen(8080);
17   console.log("Server has started.");
18 }
19 
20 // Export functions
21 exports.start = start;

最重要的模組:requestHandlers.js,完整程式碼如下:

// 04-request-handlers/requestHandlers.js
 1 function start(response) {
 2     console.log("Handler 'start' is started.");
 3 }
 4 
 5 function send(response) {
 6     console.log("Handler 'send' is started.");
 7 }
 8 
 9 exports.start = start;
10 exports.send = send;

到這裡,已經完成了一份很基本的 Web Service 實作。接下來,我們要將這個成果發展成一個即時聊天軟體,就命名為 NoChat。NoChat 將會是一個完全使用 HTML5 技術開發的即時聊天軟體。

Node.js 入門, #4:認識 HTTP API

本文章採用 Markdown 語法撰寫(why?),若無法閱讀內文,請點擊這裡

設計 HTTP API

完整的 NoChat 分為二個部份:

  • Backend(Server-side)將基於目前的 Node.js 程式碼繼續完善
  • Frontend(Client-side)手機端的 App 將以 HTML5 + PhoneGap 來製作

NoChat 提供二個API,現在將 API 詳細定義如下。

  • /start,建立與 Client 的 WebSocket 連線
  • /send,送出訊息。

'/send' API 的 Query String 參數定義如表 1。這部份在前面已做過說明。

參數 用途說明
m 'hello' 指定要傳送的訊息 (message)
u 'jollen' 指定 Username

表 1 API 的參數

測試案例

以下設計一個簡單的測試案例,在完成第一個 NoChat 的 Prototype 後,將以下列步驟進行測試:

  1. 在 localhost 啟動 Node.js

  2. 打開 client.html 聊天網頁

  3. client.html 呼叫 API:http://localhost:8080/start,Server 回傳 "OK" 訊息

  4. client.html 與 Server 建立 WebSocket 連線

  5. clieht.html 開始接收 Node.js 推送(Data Push)的即時訊息

傳送訊息給 Node.js 的測試步驟:

  1. 開啟一個新的瀏覽器視窗

  2. 使用瀏覽器呼叫 API:http://localhost:8080/send?m=hello

  3. Node.js 收到訊息,並透過 WebSocket 將訊息 Push 給所有的用戶端

這還不算是一個真正的使用案例(Use Case),但至少可以幫助我們實作出第一個Prototype。

關於 Web Service

前一篇文章中的圖 2.2,是大家所熟悉的 HTTP API 形式。許多網站,像是:Google、Facebook 等,都有開放 HTTP API 供開發者存取它們的服務。以 NoChat 來說,透過上述二個 API 可以向 Server 請求服務。因此,Node.js 的重心,就是在發展 Web Service。

Web Service 的 API 定義,未來將重構為 REST 標準。基於 HTTP 的 Web Service API,是目前為止,我們所學到的重要觀念。

此外,呼叫 HTTP API 的方式,可使用 GET 與 POST 二種 HTTP 方式(HTTP Method),這二種方式都是定義在 HTTP 裡的標準。REST 標準,也引用了其它的 HTTP Method。

目前,NoChat 仍暫時以 Query String 的方式來傳遞參數。

Node.js 入門, #5:解析 Query String

本文章採用 Markdown 語法撰寫(why?),若無法閱讀內文,請點擊這裡

解析 Query String

Client 端呼叫 Server 所提供的 Web Service API。所以,現在的關鍵是如何解析 Query String。如圖 2.2,Node.js 使用 querystring 模組來解析 Query String。先將 querystring 模組匯入,接著呼叫 parse() 函數:

var querystring = require('querystring'); 
var parsedstring = querystring.parse(“m=helll&u=jollen”); 

解析後的結果存放於 parsedstring 物件,回傳結果:

{ m: 'hello', u: 'jollen' } 

parse() 函數有三個參數:

querystring.parse(str, [sep], [eq])
  • str 是 Query String
  • sep 是「Separator」,也就是字串的分隔字元,預設是 '$',通常不做變數
  • eq 則是字串與值的對應字元,預設是 '=',通常不做變數

了解如何解析 Query String 後,就可以開始進行後續的工程了。再次修改 requestHandlers.js,如下:

// 05-query-string/requestHandlers.js
 1 var querystring = require('querystring'); 
 2 
 3 /**
 4  * Global variables
 5  */
 6 var history = [ ];
 7 
 8 function start(response, query) {
 9     console.log("Handler 'start' is started.");
10     console.log("Query string is: " + query);
11 }
12 
13 function send(response, query) {
14     console.log("Handler 'send' is started.");
15     console.log("Query string is: " + query);
16 
17     var parsedstring = querystring.parse(query); 
18 
19     var obj = {
20         message: parsedstring.m,
21         username: parsedstring.u,
22         timestamp: (new Date()).getTime()
23     };
24 
25     history.push(obj);
26 
27     //////// DEBUG ////////
28     for (var i = 0; i < history.length; i++) {
29         console.log("["+i+"]: " + history[i].message);
30     }
31 }
32 
33 exports.start = start;
34 exports.send = send;

這裡利用一個全域陣列 history 來儲存訊息。將收到的訊息封裝成物件後, 再使用標準的陣列操作將物件放到陣列裡。另外,我們也將一個時間記號(Timestamp)一併封裝至該物件,用來紀錄接收到訊息的時間。

結論

到這裡完成了 Node.js 入門的學習:

  • 學會撰寫第一個 Node.js 程式
  • 學會啟動 HTTP Server
  • 了解並實作 URL Routing
  • 學會解析 Pathname 與 Query String

Node.js 入門, #6:撰寫 WebSocket Server

本文章採用 Markdown 語法撰寫(why?),若無法閱讀內文,請點擊這裡

認識 WebSocket

WebSocket 是 HTML5 裡的一個標準,它是一種 TCP/IP 的連線技術。在協定部份,則是基於 HTTP(Over HTTP)協定。因此,WebSocket 標準定義了一些 HTTP Headers 來進行 Client/Server 的通訊。

WebSocket 分為 Client 端與 Server 端二個部份,本章要介紹的是利用 Node.js 技術,來開發 WebSocket Server。 目前有許多現成的 Node.js WebSocket 模組可使用,實作時,我們就不必自行處理複雜的 WebSocket 協定問題。

安裝 WebSocket 模組

本章所使用的 WebSocket 模組,要使用 npm 工具另外安裝。利用 npm 安裝 WebSocket-Node:

 $ npm install websocket

WebSocket-Node 原始碼可由 Github 上取得:

https://github.com/Worlize/WebSocket-Node

安裝,依據需要,可引入不同的模組。WebSocket-Node 提供 4 個模組如下:

  • var WebSocketServer = require('websocket').server;
  • var WebSocketClient = require('websocket').client;
  • var WebSocketFrame = require('websocket').frame;
  • var WebSocketRouter = require('websocket').router;

NoChat 範例,將會使用 'server' 模組。

建立 WebSocket Server

基於先前的範例,繼續修改 server.js 的程式碼如下:

// 06-websocket-with-protocol/server.js
 1 var http = require("http");
 2 var url = require("url");
 3 var WebSocketServer = require('websocket').server;
 4 
 5 function start(route, handlers) {
 6   function onRequest(request, response) {
 7     var pathname = url.parse(request.url).pathname;
 8     var query = url.parse(request.url).query;
 9 
10     console.log("Request for " + pathname + " received.");
11 
12     route(pathname, handlers, response, query);
13 
14     response.writeHead(200, {"Content-Type": "text/plain"});
15     response.write("Hello World");
16     response.end();
17   }
18 
19   var server = http.createServer(onRequest).listen(8080, function() {
20      console.log("Server has started and is listening on port 8080.");
21   });
22 
23   wsServer = new WebSocketServer({
24     httpServer: server,
25     autoAcceptConnections: false
26   });
27 
28   function onWsConnMessage(message) {
29     if (message.type == 'utf8') {
30       console.log('Received message: ' + message.utf8Data);
31     } else if (message.type == 'binary') {
32       console.log('Received binary data.');
33     }
34   }
35 
36   function onWsConnClose(reasonCode, description) {
37     console.log(' Peer disconnected with reason: ' + reasonCode);
38   }
39 
40   function onWsRequest(request) {
41     var connection = request.accept('echo-protocol', request.origin);
42     console.log("WebSocket connection accepted.");
43 
44     connection.on('message', onWsConnMessage);
45     connection.on('close', onWsConnClose);
46   }
47 
48   wsServer.on('request', onWsRequest);
49 }
50 
51 // Export functions
52 exports.start = start;

先將 WebSocket-Node 的 'server' 匯入,如程式碼第3行。其它的修改細節條列如下:

  • 第 19~26 行:將原本的 HTTP Server 物件,聚合至(傳遞)WebSocket Server。WebSocker Server 的物件名稱為 wsServer
  • 第 48 行:在 wsServer 物件裡註冊一個 Request Handler,即 onWsRequest() 函數
  • 第 40 行:當 WebSocket 的連線請求發生時,便回呼此函數
  • 第 41 行:接受該 WebSocket 連線,第一個參數是 WebSocket Protocol,這是一個自定的協定名稱,用途由開發者定義
  • 第 44~45 行:為此連線註冊 Message Handler 與 Close Handler 函數
  • 第 28 行:收到用戶端傳送過來的訊息時,回呼此函數,後續將繼續擴充此函數,將收到的訊息儲存,並將訊息即時(Real-time)推送(Push)到所有的用戶端
  • 第 36 行:該 WebSocket 連線關閉後,回呼此函數

學會建立 WebSocket Server 後,就可以開始建立與 Client 端的連線了。

Node.js 入門, #7:儲存用戶端 WebSocket 連線

本文章採用 Markdown 語法撰寫(why?),若無法閱讀內文,請點擊這裡

儲存用戶端 WebSocket 連線

要儲存所有的用戶端 WebSocket 連線,最簡便的方式是使用 Global Array:

// Connected WebSocket clients
var clients = [];

當用戶端與 Node.js 建立連線時,將會回呼上述提及的 onWsRequest() 函數。所以,儲存連線的程式碼,應該添加至這個地方。繼續加入程式碼如下:

function onWsRequest(request) {
  var connection = request.accept('echo-protocol', request.origin);
  console.log("WebSocket connection accepted.");

  // Save clients (unlimited clients)
  clients.push(connection);

  connection.on('message', onWsConnMessage);
  connection.on('close', onWsConnClose);
}

以下目前為止的最新程式碼。

// 07-websocket-data-push/server.js
 1 var http = require("http");
 2 var url = require("url");
 3 var WebSocketServer = require('websocket').server;
 4 
 5 // Connected WebSocket clients
 6 var clients = [];
 7 
 8 function start(route, handlers) {
 9   function onRequest(request, response) {
10     var pathname = url.parse(request.url).pathname;
11     var query = url.parse(request.url).query;
12 
13     console.log("Request for " + pathname + " received.");
14 
15     route(pathname, handlers, response, query, clients);
16 
17     response.writeHead(200, {"Content-Type": "text/plain"});
18     response.write("Hello World");
19     response.end();
20   }
21 
22   var server = http.createServer(onRequest).listen(8080, function() {
23      console.log("Server has started and is listening on port 8080.");
24   });
25 
26   wsServer = new WebSocketServer({
27     httpServer: server,
28     autoAcceptConnections: false
29   });
30 
31   function onWsConnMessage(message) {
32     if (message.type == 'utf8') {
33       console.log('Received message: ' + message.utf8Data);
34     } else if (message.type == 'binary') {
35       console.log('Received binary data.');
36     }
37   }
38 
39   function onWsConnClose(reasonCode, description) {
40     console.log('Peer disconnected with reason: ' + reasonCode);
41   }
42 
43   function onWsRequest(request) {
44     var connection = request.accept('echo-protocol', request.origin);
45     console.log("WebSocket connection accepted.");
46 
47     // Save clients (unlimited clients)
48     clients.push(connection);
49 
50     connection.on('message', onWsConnMessage);
51     connection.on('close', onWsConnClose);
52   }
53 
54   wsServer.on('request', onWsRequest);
55 }
56 
57 // Export functions
58 exports.start = start;

接下來的任務是製作 Frontend(Client 端);不過,現在是一個認識與學習 JSON 的絕佳時機。

Node.js 入門, #8:認識 JSON 與 Web App 的概念

本文章採用 Markdown 語法撰寫(why?),若無法閱讀內文,請點擊這裡

Web-Oriented Architect

Web 導向架構(WOA, Web-Oriented Architect)著重幾個觀念:

  • Device-Server 設計模式
  • Device 端使用 Browser,以 Browser 做為執行環境(Runtime)
  • Server 端提供 APIs,即 PaaS 概念
  • Device-Server 採用非同步通訊(Asynchronous communication)

事實上,非同步通訊大家都使用過,就是 AJAX;AJAX 的第一個 A 就是 Asynchronous。但是考量 Server 端的負載(Loading),以及百萬連線(Millions requests)等級的處理能力需求,應該儘量少用 AJAX 機制。這就是 Device-Server 與 Client-Server 的差別,大家可能還不太明白。所以,將二者的差別簡單整理如下:

  • Client-Server 做法:在瀏覽器裡(Client)主動向 Server 請求內容,Client 定時(如:每隔5秒鐘)發出請求,持續更新內容。
  • Device-Server 做法:在裝置端(Device)和 Server 建立連線,Server 主動將更新內容推送(Push)給裝置端,更明確地說,裝置裡的瀏覽器,瀏覽器再將新內容刷新。

這樣就很清楚了,傳統的 Client-Server 做法是「Data Pull」,即主動去拉資料;Device-Server 的做法是「Data Push」,即推送資料,由 Server 在必要時才將資料推送給 Device。Data Push 的經典代表作就是 BlackBerry(黑莓機)的郵件服務。

為什麼 AJAX 不好用?因為 Server 要冒著「不知道有多少 Client、不知道同時有多少 Requests」的風險,會增加 Load Balancer 佈署的成本;另外,當然就是即時性的問題(Live Streamming)。要達成 Data Push 的目的,有解決二個技術問題:

  • Device 端要與 Server 建立永續性(Persistent)連線,也就是 Socket Connection
  • Server 推送出去的資料,格式要有統一標準,且輕量化

要解決這二個問題,要使用到二項技術:WebSocket 與 JSON。從以上的說明,可以大略了解 HTML5 的威力在於「Web App」的應用領域,而不只是 Web Page 的製作:

  • 從 Web Page 角度來看,以 Client-Server 為主,這像是傳統 PC 時代的使用案例 從 Web App 角度來看,將 Device-Server 為主

  • Web App 的開發思惟,與 Web Page 有很大的不同。目前 Web App 的 UI 製作,採是強調跨螢幕與裝置的特性,這種設計稱為 Responsive Design。並且,Responsive Design 進向以行動裝置為預設值的做法,也就是「Mobie First」

Web App 技術

從 Web App 的角度看 HTML5,以下是初學者可考慮優先切入學習的技術:

  • PhoneGap:Device API 的標準,使用 JavaScript 呼叫 Device API 的好技術,Nitobi 公司是 PhoneGap 的開發商,這家公司現已被 Adobe Systems 收購

  • WebSocket:HTML5 標準裡的一個技術

  • Node.js:開發專用 Web Service 的技術,採用 JavaScript 語言

Apache Web Server 是通用型的 Web Server,主要提供頁面(Pages)服務。Node.js 則是 Web Service 的技術,用來開發專用的 Web Service APIs。

現在開發專用的 Web Service 非常重要,這是 PaaS 的靈魂。例如,開發股票報價專用 Web Server。過去常聽到的 Web Server,例如:Apache,都是一般用途的 Web Server,用來「host Web pages」。

現在 Client 端的網頁是用 JavaScript,Server 端的開發也可以用 JavaScript,Client/Server 通通都用 JavaScript,這是一個「All in JavaScript」的時代。

重要的資訊交換格式:JSON

傳統 Backend 的做法,會提供 Client 端一份 HTML5 的片斷文件,而不是格式化後的資料(Formatted),這是一個缺點。

如果 Server 回傳的是格式化後的資料,Client 端就可以更有效率地利用這些資料。試想,如果 Google 傳回的搜尋結果是一堆 HTML5,那我們不就還要再去 Parse 這份文件,才能取出真正的結果,然後才能再次利用這些資料(例如:再儲存為 CVS 格式)。

為了解決這個問題,必須要有一個標準,不能大家都用自已的 HTML5 文件,或是自定的格式。軟體工程師設計了一些標準。一開始提出的做法,是制定標準的XML標籤,這樣大家就可以統一文件格式了。但是還有一個問題,就是「資料量太大」。

試想,Server 要回傳二筆資料,這二筆資料都是電話號碼:

  • 0911-111-111
  • 0900-000-000

然後用XML來表示,就變成:

<Telephone>
   <Item>0911-111-111</Item>
   <Item>0900-000-000</Item>
</Telephone>

這種把資料打腫了才回傳的做法,大大浪費網路頻寬。上面只是一個簡單的例子,現實環境,要回傳的資料可能是一個 10MB 的 XML 文件,結果原始資料可能只有 1MB。

要解決這個問題,就要有一個輕量級(Light-weight)的資料交換格式,這項技術就是 JSON。所以,JSON 是 Client/Server 交換資料的一種格式,一種 Light-weight data exchange 技術。

Node.js 入門, #9:學習 JSON 格式

本文章採用 Markdown 語法撰寫(why?),若無法閱讀內文,請點擊這裡

學習 JSON 格式

思考將要製作的 NoChat 聊天室範例,Server 要把收到的訊息,Push 給所有的 Client 端。Server 與 Client 所使用的標準資料交換格式,就是 JSON。如何把訊息打包成 JSON 格式?方式非常簡單。以表 2 為例,要將這個資料表,撰寫為 JSON 格式,只需要二個步驟:

Step 1:以 JavaScript 物件表示一筆資料

例如,第一筆個人資料,以 JavaScript 物件來表示的話,只要用 var 來宣告此物件即可:

var obj = {
  "name": "Jollen",
  "score":  80
};

大括號是 JavaScript 表示物件的語法。上述範例,我們宣告了 obj 物件。

Step 2:轉換成標準 JSON 語法

去掉等號,以及等號左邊的物件宣告,結果如下:

{
  "name": "Jollen",
  "score":  80
}

請注意,結尾的分號也要一併去除。上述的表示方法,就是標準的 JSON 語法。這個例子用 JSON 來表示一筆個人資料。JSON 表示方法非常地簡單,只要會 JavaScript 保證 1 分鐘即可上手,不需要特意學習。

Step 3:用陣列表示多個物件

"name" 欄位 "score" 欄位 說明
"Jollen" 80 第 1 筆使用者資料
"Paul" 170 第 2 筆使用者資料
"Peter" 250 第 3 筆使用者資料
"Ellaine" 580 第 4 筆使用者資料

表 2 資料表

表 2 共有 4 筆個人資料,因此需要撰寫 4 個物件,每個物件之間用逗號隔開。試想,過去撰寫程式的經驗裡,我們用哪一個資料結構(Data Structure)來表示多筆型別(Data Type)相同的資料呢?答案是:陣列(Array)。

JavaScript 的陣列用中括號來宣告,例如:

var string = ['Jollen', 'Paul', 'Peter'];

這個例子宣告 string 陣列,裡頭有 3 個字串。用 JavaScript 怎麼表示 4 個物件的陣列呢?答案如下:

var persons = [
  {
    "name": "Jollen",
    "score": 80
  },
  {
    "name": "Paul",
    "score": 170
  },
  {
    "name": "Peter",
    "score": 250
  },
  {
    "name": "Ellaine",
    "score": 580
  }
];

我們只要把上述的 4 個物件,用陣列「群組」起來即可。和 Step 2 相同,保留以下的寫法即可:

[
  {
    "name": "Jollen",
    "score": 80
  },
  {
    "name": "Paul",
    "score": 170
  },
  {
    "name": "Peter",
    "score": 250
  },
  {
    "name": "Ellaine",
    "score": 580
  }
]

這就是表 2 的 JSON 表示方式了。將上述的 JSON 儲存為純文字,這個純文字檔就叫做 JSON Document,這就是 JSON Document 資料庫的概念。

同理,NoChat Server 收到訊息後,只要把訊息表示成 JSON 後,傳送給 Client 端即可。JSON 相當簡單易學,更是優秀的輕量級資料交換格式。

Node.js 入門, #10:認識 JSON Stringify

本文章採用 Markdown 語法撰寫(why?),若無法閱讀內文,請點擊這裡

JSON Stringify

請注意,上述的 JSON 是一個型別(Type),是一個 Array Type。我們不能儲存或傳送「Type」,所以要將 Type 轉成字串(String)後,才能儲存或傳送。例如,對電腦來說,這是一個物件(Object):

{ "name": "James" }

我們把這個物件轉成字串:

"{ \"name\": \"James\" }""

對電腦來說,這才是字串。所以,將 JSON 物件(Object)轉成字串後,才能儲存或傳送。這個動作就叫 JSON Stringify(字串化)。當然,字串化過的 JSON 字串,要使用時,也要解析(Parse)回物件。

在 Node.js 裡如何做 JSON Stringify 呢?只要呼叫 JSON.stringify() 函數即可。例如:

var obj = { "name": "James" };    // 一個物件
var str = JSON.stringify(obj);    // 字串化

下面是簡約的寫法:

var str = JSON.stringify({ "name": "James" });    // 字串化

沿續先前的範例,將 requestHandler.js 加入 JSON Stringify 的處理。完整程式碼如下:

{title="07-websocket-data-push/requestHandler.js"}

// 07-websocket-data-push/requestHandler.js
 1 var querystring = require('querystring'); 
 2 
 3 /**
 4  * Global variables
 5  */
 6 var history = [ ];
 7 
 8 function start(response, query, clients) {
 9     console.log("Handler 'start' is started.");
10     console.log("Query string is: " + query);
11 }
12 
13 function send(response, query, clients) {
14     console.log("Handler 'send' is started.");
15     console.log("Query string is: " + query);
16 
17     var parsedstring = querystring.parse(query); 
18 
19     var obj = {
20         message: parsedstring.m,
21         username: parsedstring.u,
22         timestamp: (new Date()).getTime()
23     };
24 
25     history.push(obj);
26 
27     //////// DEBUG ////////
28     for (var i = 0; i < history.length; i++) {
29         console.log("["+i+"]: " + history[i].message);
30     }
31 
32     var json = JSON.stringify({ type: 'message', data: obj });
33 
34     // Data push to all clients
35     for (var i = 0; i < clients.length; i++) {
36         clients[i].sendUTF(json);
37     }
38 }
39 
40 exports.start = start;
41 exports.send = send;

第 19 行到第 23 行,將訊息封裝到物件裡,同時也加入使用者名稱,以及時間戳記(Timestamp)。第 32 行將物件字串化,這就是一個標準的 JSON 資枓了。接著,第 35 行到第 37 行,將這筆 JSON 資料,傳送給所有的 WebSocket Client 端。

關於 July 2014

此頁面包含了在July 2014發表於Jollen's Blog的所有日記,它們從老到新列出。

前一個存檔 June 2014

後一個存檔 September 2014

更多信息可在 主索引 頁和 歸檔 頁看到。

Top | 授權條款 | Jollen's Forum: Blog 評論、討論與搜尋
Copyright(c) 2006 www.jollen.org