2017年2月26日 星期日

Koa 2 起手式!

Standard


在 Node.js 的世界裡,說到今天最潮的 Web Framework,肯定就是 Koa!其用了最新的 JavaScript 語法和特性,來改善 Web Framework 的設計。只不過,Koa 雖然相對於其他舊的 Web Framework 來說有相當多的進步,但很多人卻相當討厭 Koa 的 Generator 設計,尤其是那些「*」符號,那不知所謂的 yield 也讓很多人不舒服。所以至今仍然有許多人在使用 express 來當作自己的 Web Framework,寧可繼續使用那老派的 callback 設計,而不肯嘗試 Koa。

隨著 ECMAScript 標準的進步,Koa 才剛被開發出來沒多久,原本的開發團隊就立即著手打造 Koa 2 ,開始更進一步採用更新的 JavaScript 特性,以 async/await 語法重新打造了全新且更簡潔的框架。可惜的是,由於 async/await 語法一直遲遲沒有被 JavaScript 引擎原生支援,因此總需要靠 babel 編譯打包程式碼後,才能正常跑在 Node.js 之上。這讓 Koa 2 一直無限期處於非穩定版,讓原開發者從開發的一開始,就打算等到 V8 和 Node.js 開始原生支援 async/await 後,才會被以穩定版(stable)的姿態釋出。

所以,即使 Koa 2 到了今天已經相當穩定,也開始有不少人使用在自己的線上服務,卻一直無限期處於非穩定版的狀態。

另外,由於 Koa 2 大量使用 Async/Await,如果你還對 Async/Await 的使用還不熟悉,建議在閱讀本文之前,先閱讀舊文「JavaScript 好用的 async 異步函數!」來學習如何使用。

學習 Koa 的好時機來囉


總算,日前 Node.js v7.6.0 釋出後已經正式宣布原生支援了 async/await 語法,而且不需要額外的參數選項。伴隨著這個消息,Koa 2.0 也隨即正式釋出了!

Node.js 內建支援 ES7 的 async/await 真的是非常棒的消息!過去我們使用 async/await,都還需要 babel 的協助才能正常跑在舊版的 Node.js,不但開發上相當麻煩,非原生的各種 ES7 特性也浪費不少額外的記憶體和效能,這樣的問題在斤斤計較效能的 Server 環境下,更是讓人頭痛。

如今 Node.js 的原生支援,讓我們已經不需要再擔心種種問題,讓我們可以得到簡潔的程式碼和兼顧效能,現在就是準備轉換到 Koa 2 的最好時機!:-)

安裝 Koa 2


現在,我們終於可以直接使用 NPM 命令安裝 Koa 2:

npm install koa

開發第一個應用程式


如果你有開發過 Koa 或 Express 的網站應用程式,Koa 2 的寫法其實相當雷同,差別是 Express 使用的是普通函數當 callback、Koa 是使用 Generator Function,而 Koa 2 是使用 Async Function。

一個簡單的範例如下:

const Koa = require('koa');

const app = new Koa();

app.use(async function(ctx) {
    ctx.body = 'Hello World';
});

app.listen(3001);

當 ctx.body 被設定一個內容後,連線就會回傳該內容回瀏覽器。在這範例中,無論發什麼要求給 Server ,都會得到「Hello World」的回傳。

註:如果你使用過 Koa,會發現 Koa 2 已經不再使用 this 關鍵字,而是改成一個 context 物件代入到函數之中。

使用異步函數打造的 Middleware


koa.use() 將用來載入 Middleware,所有連線工作都會經過 Middleware 處理。這也是為什麼,前一個例子裡,我們使用 koa.use() 設定了一個處理函數後,所有連線透會通過該函數進行處理並回傳同樣的值。

要注意的是,該函數是一個「異步函數(Async Function)」,要用到 async 關鍵字來宣告:

app.use(async function() {
    // ...
});

註:如果你有過 express 開發經驗,對於 koa.use() 會相當熟悉,Koa 同樣支援了 Middleware 的架構,你可以將過去的程式輕易移植到這新的框架上。

自訂 Router 和路徑管理


之前的範例直接使用 koa.use(),會將所有的連線都導入同一個處理函數,輸出同一個結果。若我們想要自訂不同的路徑,讓不同路徑用不同的處理函數,將需要額外安裝「koa-router」模組:

npm install koa-router

然後可以用 koa-router 來管理對外的連線路徑:

const Koa = require('koa');
const Router = require('koa-router');

const app = new Koa();
const router = Router();

// 設定根路徑的處理函數
router.get('/', async function(ctx) {
    ctx.body = 'Hello World';
});

app.use(router.routes());

app.listen(3001);

接收 QueryString 資料


QueryString 可說是歷史悠久且非常常見的傳值方法,藉由一個網址後面加上一個「?」字元後,就可以使用鍵值(Key/Value)來進行資料傳遞,並用「&」區隔多組資料。一個簡單的實際應用如下:

http://my_server/send?name=fred&msg=Hello

取得資料的方法如下:

ctx.query.name
ctx.query.msg

接收 body 資料


當我們使用「POST」或「PUT」方法,我們就可以利用 body 傳送一些資料到伺服器,像是網頁表單時常使用這樣的傳值方法。若想要取得 body 的資料,必須先安裝「koa-bodyparser」模組。

截至本文釋出為止,該模組還沒有隨著 Koa 2 推出正式支援的版本,所以預設下載回來的版本還是支援舊的 Koa,所以必須指定版本號「next」:

npm install koa-bodyparser@next

當然,你也可以用「koa-convert」模組將舊的 Koa Middleare 直接轉換給 Koa 2 使用:

npm install koa-convert

使用 koa.use() 載入 koa-bodyparser,koa 就會自動在處理連線時使用它解析 body:

var bodyParser = require('koa-bodyparser');

// 若想使用 koa-convert 進行轉換,要先載入模組:
// const convert = require('koa-convert');
// 再以 convert(bodyParser()) 包裝
app.use(bodyParser());

然後可以在路徑處理函數中,正常取得 body 內的資訊:

ctx.request.body.name
ctx.request.body.msg

錯誤處理


在 Koa 2 中,可以透過 ctx.throw() 進行錯誤處理,並回傳狀態值和內容給客戶端,他會中斷目前的處理函數,實際使用情境如下:


router.get('/api/v1/user', async function(ctx) {

    // 檢查 Token,若有問題回傳 400 HTTP StatusCode
    if (ctx.query.token == '123')
        ctx.throw(400);

    // 若已經拋出 400 的狀態,接下來的程式不會被執行
    ctx.body = 'Hello World';
});

加入多個 Middleware


所有的連線要求可以透過一系列、不只一個 Middleware 來進行處理,我們可以利用多次 koa.use() 來加入多個 Middleware,多個 Middleware 可以用來做到很多功能,例如記錄和顯示每個連線的狀態。

加入多個 Middleware 的範例如下:

const Koa = require('koa');

const app = new Koa();

app.use(async function(ctx, next) {
    // 略過這個 Middleware,讓下一個 Middleware 來接著處理
    await next();
});

app.use(async function(ctx) {
    ctx.body = 'Hello World';
});

app.listen(3001);

加入 Logger 來記錄連線要求


koa-logger 是一個能顯示連線要求狀態的第三方 Middleware,可以先透過 NPM 安裝它:

npm install koa-logger

然後可以直接以 app.use() 引用:

const Koa = require('koa');
const logger = require('koa-logger');

const app = new Koa();

// 加入 logger 在其他的 Middleware 之前
app.use(logger());

app.use(async function(ctx) {
    ctx.body = 'Hello World';
});

app.listen(3001);

然後,你的應用程式就會輸出漂亮的連線要求訊息:

  <-- GET /api/v1/hello
  --> GET /api/v1/hello 200 8,257ms 2b

在 Koa 2 裡使用 Mongoose


異步函數使用 await 關鍵字對 Promise、Thunk 進行等待,使開發者不再需要用到大量的 callback function,讓程式碼比較不會「橫著長大」。所以,只要 Mongoose 可以在做任何工作時,回傳一個 Promise 物件,我們就可以在 Koa 2 中使用 await 等它完成。

還好,Mongoose 有支援這個功能,但我們得使用 .exec() 這個方法來取得 Promise:

router.get('/api/v1/user', async function(ctx) {

    // 利用 exec() 取得 Promise,然後以 await 等待完成
    ctx.body = await Users.find({}).exec();
});

函數宣告的習慣改變


在本文的範例中,仍然還是使用「function()」這樣的函數宣告方式,但很多開發者為了減少程式碼,大量改用 Arrow Function(箭頭函數)來宣告函數,所以你會大量看到這樣的情況:

router.get('/api/v1/user', async (ctx) => {
    // ...
});

因為多數情況下,改用 Arrow Function 來宣告是沒有問題的,所以很多懶惰的開發者都會這樣使用。但建議你,如果有空時,還是要了解這種函數與普通函數宣告的差別。

後記


這篇文章其實卡很久了,一直遲疑著要不要在正式穩定版之前公開,剛好趁著這幾天 Node.js v7.6.0 和 Koa 2 正式版釋出,所有的顧慮就沒有啦。:-D

2017年2月3日 星期五

Node.js 也可以使用 Protocol Buffers!

Standard

Protocol Buffers (protobuf)」是一套 Google 所提出的結構化資料的包裝技術,讓資料便於網路傳輸或交換,如同常見的 JSON 和 XML 等技術一般。但相對於其他常見技術,protobuf 設計上更易於用來包裝二進位資料,應用在串流(Streaming)技術上,在資料包裝上也更為節省空間,在包裝或解析上也更有效率。

註一:若採用 JSON,由於原本的設計上並無法處理二進位資料,所以如果要包裝二進位資料,傳統做法會將資料轉換成 base64 的格式,再以字串(String)的格式儲存。因為這等於二次包裝資料,導致處理上非常沒有效率。

註二:與 Google Protocol Buffers 類似的技術還有 MessagePack 及 Facebook 採用的 Apache Thrift,有興趣的人可以自行參考比較。

跨語言的優點


另外,Protocol Buffers 最大的優點,就是擁有跨程式語言的設計,提供了一個標準通用的 .proto 定義方法,讓我們定義資料結構和格式。只需要載入這些我們事先準備好的資料定義,就可以輕易生成給不同語言(如:C++、C#、Go、Java、Objective-C 或 Python)用的資料解析器、包裝方法,讓我們可以在不同的語言之間,解析或包裝相同的結構資料。

Protocol Buffers 的使用場景?


若在純粹的 Web 應用下,大多數情況,我們不需要處理二進位資料,或是需要非常精準的資料格式,也不會進行單筆高流量的資料交換,所以使用 JSON 或 XML 已經足以。但若你的應用有串流、二進位資料的需求,Protocol Buffers 就是你可以考慮的選擇。

像是筆者在一些公司專案中,會運用 Message Queuing 進行各種訊息資料傳遞,以達成各種資料處理需求。但由於訊息資料內可能有大大小小等各種資料形式和資料型態需求,導致 JSON 包裝已經完全不敷使用,甚至有效能上的疑慮,這時就會採用 Prorocol Buffers 來打包資料。

安裝 ProtoBuf.js


Google 官方其實並沒有實作 JavaScript 版本的 Protocol Buffers 支援,但還好廣大的社群已經有高手開發出 JavaScript 的模組「ProtoBuf.js」,除了在 Node.js 上可以使用以外,甚至可以在瀏覽器中使用

所以,如果想在 Node.js 裡使用,可以直接透過 NPM 安裝模組:

npm install protobufjs

補註:Protocol Buffers v3.0.0 beta 2 開始官方支援 JavaScript,未來有機會轉用官方的版本。

使用 .proto 定義自己的資料格式


開始使用 Protocol Buffers 的第一個步驟,就是建立一個 .proto 檔來描述定義一個自己的資料格式相當簡單,一個簡單的定義如下。

Product.proto 內容:

package Ecommerce;

message Product {
    bool available = 1; // 是否上架(布林值)
    string name = 2;    // 產品名稱(字串)
    string desc = 3;    // 產品說明(字串)
    float price = 4;    // 價格(浮點數)
}

實際上 Protocol Buffers 支援了更多資料格式,有興趣的人可以自行參考官方所整理的表格:「Scalar Value Types」。

使用我們定義的 .proto 來包裝資料


若要包裝資料,要先載入 .proto 檔案裡的資料定義,然後使用此定義去進行接下來的工作,而 ProtoBuf.js 提供了一個 encode 方法來進行資料包裝。

由於經過 Protocol Buffers 包裝後的資料是二進位格式,所以 ProtoBuf.js 提供了 finish 方法輸出成 Node.js 的 Buffer 格式:

var ProtoBuf = require('protobufjs');

// 載入 Product.proto 檔案
ProtoBuf.load('Product.proto', function(err, root) {
    if (err)
        throw err;

    // 並取得 Product 資料定義
    var Product = root.lookup('Ecommerce.Product');
    
    // 準備包裝的資料
    var data = {
        available: true,
        name: 'ApplePen',
        desc: 'The combination of Apple and Pen',
        price: 100.0
    };
    
    // 包裝資料後回傳 Buffer 格式(二進位形態)
    var msgBuffer = Product.encode(data).finish();
});

解開已包裝的資料


若我們有一個已包裝過的資料(無論是從哪裡收到的資料),可以直接使用 decode 方法去解開它:

var ProtoBuf = require('protobufjs');

// 載入 Product.proto 檔案
ProtoBuf.load('Product.proto', function(err, root) {
    if (err)
        throw err;

    // 並取得 Product 資料定義
    var Product = root.lookup('Ecommerce.Product');
    
    // 解開
    var data = Product.decode(msgBuffer);
});

二進位資料形態的欄位


前面提到,Protocol Buffers 可以包裝二進位資料,若我們想要設定某個欄位為二進位的資料,可以將其資料型態設為「bytes」:

package MyTest;

message Example {
    bytes binData = 1; 
}

然後,當我們在包裝資料時,該欄位應該是一個 Buffer 的物件:

var msgBuffer = Example
    .encode({
        binData: new Buffer('This is binary data')
    })
    .finish();

解開時,該欄位會是一個 Buffer 物件:

var data = Example.decode(msgBuffer);

// 將 Buffer 內容轉成字串形式輸出
console.log(data.binData.toString());

ProtoBuf.js 的效能表現


Protocol Buffers 這類的技術,不外乎就是把一個執行期的 JavaScript 物件,轉換包裝成二進位、字串等資料格式,使資料訊息便於透過網路和其他媒介傳送。實務上,與 JavaScript 物件轉成 JSON 字串是同樣的意思。

所以若要評估這樣技術的效能,最實際的方式就是測試、比較他們的「轉換」的效率,ProtoBuf.js 官方提供了一些「效能測試」,方便我們在自己機器上進行 Protocol Buffers 與原生 JSON 處理的效能比較。

從官方的測試結果來看,從資料包裝的速度,ProtoBuf.js 的效能快過於「JSON.stringify」將近一倍,如果是轉成二進位形式(to Buffer)更是快三倍左右;從解開包裝的速度來看,ProtoBuf.js 效能則是「JSON.parse」的三至四倍效能以上。

整體比較起來,ProtoBuf.js 則是比純 JSON 的處理快上一倍以上。

節錄官方 Github 上的測試結果(機器:i7-2600K。Node.js 版本:6.9.1):

benchmarking encoding performance ...

Type.encode to buffer x 547,361 ops/sec ±0.27% (94 runs sampled)
JSON.stringify to string x 310,848 ops/sec ±0.73% (92 runs sampled)
JSON.stringify to buffer x 173,608 ops/sec ±1.51% (86 runs sampled)

      Type.encode to buffer was fastest
   JSON.stringify to string was 43.5% slower
   JSON.stringify to buffer was 68.7% slower

benchmarking decoding performance ...

Type.decode from buffer x 1,294,378 ops/sec ±0.86% (90 runs sampled)
JSON.parse from string x 291,944 ops/sec ±0.72% (92 runs sampled)
JSON.parse from buffer x 256,325 ops/sec ±1.50% (90 runs sampled)

    Type.decode from buffer was fastest
     JSON.parse from string was 77.4% slower
     JSON.parse from buffer was 80.3% slower

benchmarking combined performance ...

Type to/from buffer x 254,126 ops/sec ±1.13% (91 runs sampled)
JSON to/from string x 122,896 ops/sec ±1.29% (90 runs sampled)
JSON to/from buffer x 88,005 ops/sec ±0.87% (89 runs sampled)

        Type to/from buffer was fastest
        JSON to/from string was 51.7% slower
        JSON to/from buffer was 65.3% slower

benchmarking verifying performance ...

Type.verify x 6,246,765 ops/sec ±2.00% (87 runs sampled)

benchmarking message from object performance ...

Type.fromObject x 2,892,973 ops/sec ±0.70% (92 runs sampled)

benchmarking message to object performance ...

Type.toObject x 3,601,738 ops/sec ±0.72% (93 runs sampled)

其他使用場景?


只要你有需要跟其他系統、服務、外部程式進行資料交換,Protocol Buffers 就有他適用的地方。

舉例來說,現在很多人開始採用 WebSocket 取代傳統的 Socket,使得 WebSocket 不再只是應用在瀏覽器之中,甚至可能是各種機器與機器之間的溝通。在這種情況下,其中交換、傳遞的資訊可能不是普通純文字這麼簡單,也很有可能是二進位類型、串流形式的資料,導致 JSON 可能因此不適合用於當作其中的資料交換格式。這時,就可以 Protocol Buffers 與 WebSocket 搭配使用。

不只如此,在這 IoT 當道的年代,在這訊息技術滿天飛的年代 AMQP、MQTT 等各種通訊技術下,以及需要許多爆量資料收集分析的場景,Protocol Buffers 也很有發揮的空間。

後記


要注意的是,Protocol Buffers 雖然是個好東西,但並非是個用來完全取代 JSON 的解決方案,JSON 仍有其可讀性高、易操作及通用性高等優點。在多數 API 設計的場景之下,JSON 仍然是最好的選擇。