本文翻譯自:
https://www.airpair.com/node.js/posts/nodejs-framework-comparison-express-koa-hapi
1、介紹
直至今日,Express.js仍然是最為流行的Node.js Web應用程式框架。它似乎已經逐漸成為大多數Node.js Web應用程式的基礎依賴框架,包括很多流行的框架,比如Sail.js就是以Express.js為基礎搭建的。然而現在我們有了更多“類sinatra”(注:sinatra是一款Ruby框架,程式碼非常簡潔,號稱開發一個部落格專案只需要100行程式碼)似的框架可以選擇。也就是接下來我們將分別介紹的Koa和Hapi兩個框架。
本文的目的並不是打算去說服大家去使用其中的任何一款框架,而是希望能夠幫助大家去對比分析這三個框架的優劣勢。
2、框架背景
今天我們對比的這三款框架其實都有很多的共通點。比如他們都可以幾行程式碼就能建立一個服務,而且進行REST API的開發也是小菜一碟。下面我們就分別來看這三款框架吧。
2.1、Express
2009年6月26日,TJ Holowaychuk 第一次提交了Express的程式碼。在2010年1月2日,Express正式釋出了0.0.1版本,截止當時,作者已經提交了超過660次程式碼。當時Express的兩位主要開發維護者分別是TJ 以及 Ciaron Jessup。第一版釋出的時候,Express在Github的readme.md介紹檔案中式這麼描述這塊框架的:
一款基於node.js以及Chrome V8引擎,快速、極簡的JS服務端開發框架。
5年多後今天,Express目前已經發布到4.10.1版本,提交超過4925次程式碼,目前主要是採用StrongLoop進行開發維護,因為TJ同學已經轉入GO語言開發社群了。
2.2、Koa
Koa是在一年以前也就是在2013年8月17日由TJ同學(是的,還是他...)首次提交的程式碼。他當時是這麼描述Koa的:“更具有表現力,更健壯的Node.js中介軟體。基於co元件的generators處理非同步回撥,無論是Web應用還是REST API開發,你的程式碼都將變得更加優雅”。(注:Koa2釋出後,已經放棄了引入co元件,而是開始採用ES7的async/await語法處理非同步回撥)。輕量化的Koa號稱不超過400行程式碼。(注:SLOC是原始碼行數,又分為物理程式碼行數LOC,以及邏輯程式碼行數LLOC)。截止目前,Koa已經發布了0.13.0版本,超過585次的程式碼提交。
2.3、Hapi
Hapi是由來自於沃爾瑪實驗室的Eran Hammer同學在2011年8月5日首次提交的。原本他只是Postmile(這是一款在node.js上開發的協作列表工具,服務端由Hapi完成)的一個核心部件,同樣也是基於Express開發。後來Hapi才被獨立出來作為一款框架進行開發維護,Eran同學在他的部落格裡這樣說道:
“Hapi的核心思想是配置優於程式碼,所以業務程式碼必須從傳輸層中剝離出來”
至今為止,Hapi已經提交超過3816次程式碼,版本是7.2.0,當前仍然是由Eran Hammer進行主要開發維護。
OK,最後讓我們來通過社群的統計資料來看看這三個框架的活躍程度:
參考項 |
Express.js |
Koa.js |
Hapi.js |
Github點贊數 |
16158 |
5846 |
3283 |
程式碼貢獻者 |
163 |
49 |
95 |
依賴包數量 |
3828 |
99 |
102 |
StackOverFlow提問數 |
11419 |
72 |
82 |
3、建立服務
基本上每個剛開始接觸Node.js的開發者第一步操作就是建立一個服務。因為下面我們將依次使用每個框架來分別建立一個服務,來看看他們之間的相似處與不同的地方。
3.1、Express
var express = require('express'); var app = express(); var server = app.listen(3000, function() { console.log('Express is listening to http://localhost:3000'); });
上面的操作對於大多數Node開發者來說應該都是很熟練了。我們先引入express,然後建立一個例項物件並將其賦值給變數app。接下來是例項化一個服務,並且開始監聽3000埠。app.listen() 其實就是對nodejs原生的http.createServer()進行了一層封裝。
3.2、Koa
var koa = require('koa'); var app = koa(); var server = app.listen(3000, function() { console.log('Koa is listening to http://localhost:3000'); });
顯而易見,Koa的語法和Express非常相似。其實來說你只需要將引入express修改為引入koa即可。同樣的,app.listen() 也是對http.createServer()進行了一層封裝。
3.3、Hapi
var Hapi = require('hapi'); var server = new Hapi.Server(3000); server.start(function() { console.log('Hapi is listening to http://localhost:3000'); });
Hapi的語法比較特別一些。不過,第一步還是引入hapi,但是這裡是例項化存入一個hapi app變數中,然後就可以建立一個指定埠的服務了。在Express和Koa中這一步我們得到的是一個回撥函式,但是Hapi返回的是一個server物件。一旦我們通過server.start()來呼叫這個在3000埠的服務以後,他將會返回一個回撥函式。然後跟Koa和Express不一樣的地方在於,這個回撥並不是對http.CreateServer()進行的一層封裝,而是Hapi自己實現的邏輯。
4、路由
接下來我們繼續深入瞭解作為一個服務的一個重要功能,那就是路由。第一步我們將使用每個框架來分別建立一個“Hello World”應用,然後再繼續關注一些更實用的功能,REST API。
4.1 Hello World
4.1.1 Express
var express = require('express'); var app = express(); app.get('/', function(req, res) { res.send('Hello world'); }); var server = app.listen(3000, function() { console.log('Express is listening to http://localhost:3000'); });
我們使用get()方法來捕獲“GET /”請求,然後呼叫一個回撥函式來處理請求,該回撥函式擁有兩個引數:req與res。在這個例子中我們僅僅使用了res的res.send()方法來向頁面返回一個字串。Express包含了很多內建的方法來處理路由功能。下面是幾個Express中常用的方法(只是部分,並不是全部方法):get, post, put, head, delete…
4.1.2 Koa
var koa = require('koa'); var app = koa(); app.use(function *() { this.body = 'Hello world'; }); var server = app.listen(3000, function() { console.log('Koa is listening to http://localhost:3000'); });
Koa和Express有些許的不同之處,因為他使用了ES6 的generators語法。(注:generators是ES6提出的一種非同步回撥的解決方法,在ES7中將直接升級為async/await)在方法前面加上一個 * 表示該方法返回一個generator物件。generators函式的作用就是使得非同步函式產生一些同步的值,但是這些值仍然是在當前的請求範圍之類。(注:generator對通過yield 定義不同的狀態值,return也算是一個狀態值。詳情瞭解:http://es6.ruanyifeng.com/#docs/generator )在app.use()中,generator函式對響應體進行賦值。在Koa中this物件,其實就是對node的request與response物件進行的封裝。this.body在Koa中是一個響應體物件的方法。它基本上能被賦值為任何值,字串、buffer、資料流、物件或者是null。Koa核心庫提供了很多中介軟體,這裡我們只是使用了其中的一個,這個中介軟體可以捕獲所有的路由,然後響應一個字串。
4.1.3 Hapi
var Hapi = require('hapi'); var server = new Hapi.Server(3000); server.route({ method: 'GET', path: '/', handler: function(request, reply) { reply('Hello world'); } }); server.start(function() { console.log('Hapi is listening to http://localhost:3000'); });
這裡我們使用了由server物件提供的一個內建方法:server.route(),這個方法需要這些引數:path(必填)、method(必填)、vhost以及handler(必填)。這個HTTP方法可以處理我們常見的GET/PUT/POST/DELETE請求,也可以使用*來處理所有路由請求。回撥函式會被Hapi預設傳入request物件以及reply方法,reply是必須被執行的方法,而且需要傳入一項資料,這個資料可以是字串、序列化的物件或者流。
4.2 REST API
Hello World程式從來都沒有太多的期望,因為它只能展示建立及執行一個應用最基本最簡單的操作。REST API幾乎是所有大型應用程式所必須的一個功能,同時對於我們更好的理解這些框架有很大的幫助。因此接下來我們將看看這幾個框架是如何來處理REST API。
4.2.1 Express
var express = require('express'); var app = express(); var router = express.Router(); // REST API router.route('/items') .get(function(req, res, next) { res.send('Get'); }) .post(function(req, res, next) { res.send('Post'); }); router.route('/items/:id') .get(function(req, res, next) { res.send('Get id: ' + req.params.id); }) .put(function(req, res, next) { res.send('Put id: ' + req.params.id); }) .delete(function(req, res, next) { res.send('Delete id: ' + req.params.id); }); app.use('/api', router); // index app.get('/', function(req, res) { res.send('Hello world'); }); var server = app.listen(3000, function() { console.log('Express is listening to http://localhost:3000'); });
我們在現有的Hello World程式上增加了REST API。Express提供了一些縮寫的方法來處理路由。這是Express 4.x 版本的語法,其實跟Express 3.x 版本差不多,同樣希望你不再使用express.Router()方法,而是換成新的API:app.use('/api', router)。新的API可以讓我們使用app.route()來替換之前的router.route(),當然了需要新增一個描述性的動詞/api.這是一個不錯的修改,因為降低開發者出現錯誤的機會,同時對原有的HTTP方法進行了最小的一個修改。
4.2.2 Koa
var koa = require('koa'); var route = require('koa-route'); var app = koa(); // REST API app.use(route.get('/api/items', function*() { this.body = 'Get'; })); app.use(route.get('/api/items/:id', function*(id) { this.body = 'Get id: ' + id; })); app.use(route.post('/api/items', function*() { this.body = 'Post'; })); app.use(route.put('/api/items/:id', function*(id) { this.body = 'Put id: ' + id; })); app.use(route.delete('/api/items/:id', function*(id) { this.body = 'Delete id: ' + id; })); // all other routes app.use(function *() { this.body = 'Hello world'; }); var server = app.listen(3000, function() { console.log('Koa is listening to http://localhost:3000'); });
很明顯,Koa並不能像Express那樣去降低route動詞的重複性。它同時還需要引入一個獨立的中介軟體來處理路由。我選擇使用koa-route,是因為他主要是由Koa小組來開發維護,當然也還有很多其他開發者貢獻的路由中介軟體可以選擇。從方法名的關鍵字上來看,koa的路由和express也是非常相似的,例如.get(), .put(), .post(), 以及 .delete()。
Koa在處理路由有一個優勢,它使用了ES6 的generator函式,從而降低了回撥函式的複雜度。
4.2.3 Hapi
var Hapi = require('hapi'); var server = new Hapi.Server(3000); server.route([ { method: 'GET', path: '/api/items', handler: function(request, reply) { reply('Get item id'); } }, { method: 'GET', path: '/api/items/{id}', handler: function(request, reply) { reply('Get item id: ' + request.params.id); } }, { method: 'POST', path: '/api/items', handler: function(request, reply) { reply('Post item'); } }, { method: 'PUT', path: '/api/items/{id}', handler: function(request, reply) { reply('Put item id: ' + request.params.id); } }, { method: 'DELETE', path: '/api/items/{id}', handler: function(request, reply) { reply('Delete item id: ' + request.params.id); } }, { method: 'GET', path: '/', handler: function(request, reply) { reply('Hello world'); } } ]); server.start(function() { console.log('Hapi is listening to http://localhost:3000'); });
跟其他框架相比,Hapi的路由配置給人的第一印象就是程式碼清爽,可讀性高。甚至連必填的配置引數method,path,hanlder以及reply都非常容易辨別。跟Koa一樣,Hapi路由的程式碼重複性也比較高,所以出錯的機率也比較大。之所有這麼做,是因為Hapi更希望使用配置來完成路由,這樣我們的程式碼會更清爽,在小組內也會更容易的維護。Hapi同樣試圖去提高程式碼錯誤處理能力,因為有的時候他甚至不需要開發者編寫任何程式碼(注:意思是完全都過配置實現,回撥函式也是用預設的。這樣出錯的 概率就小了很多,也更容易上手)。如果你試圖去訪問一個沒有在REST API中定義的路由,那麼Hapi將會返回一個包含狀態值與錯誤資訊的JSON物件。
5、優劣勢
5.1 Express
5.1.1 優勢
Express擁有最大社群,比僅僅是跟這三個框架相比,而是對於所有的Nodejs框架來說也是最大的。目前來說,他是最為三者中最為成熟的框架,接近5年的開發投入,同時還採用了StrongLoop(注:StrongLoop是一個程式管理工具,提供CLI與UI介面。)對線上倉庫的程式碼進行管理。他提供了一種簡單的方式來建立和執行一個服務,同時路由的內建也使得程式碼得到了重複使用。
5.1.2 劣勢
在使用Express過程中,我們往往要處理很多單調乏味的任務。比如他沒有內建的錯誤處理機制,另外對於同樣一個問題可以有很多中介軟體來供選擇,這也使得開發者容易迷失在中介軟體的選擇中,總而言之就是,一個問題你會有N多解決方案。Express聲稱自己是可配置選擇的,這其實不沒有好或不好,但是對於一個剛剛接觸Express的開發者來說,這就是他的劣勢了。另外,Express跟其他的框架相比也還有很大的差距。
5.2 Koa
5.2.1 優勢
Koa的一個小進步就是他的程式碼比較富有表現力,開發中介軟體也比其他框架更容易得多。Koa是一個很基礎的準系統框架,開發者可以選擇(或開發)他們所需要的中介軟體,而不是去選擇Express或Hapi的中介軟體。他同時也是三者中唯一一個積極擁抱ES6的框架,比如採用了ES6 generators函式。
5.2.2 劣勢
目前Koa還處於不穩定版本,還處在開發階段。使用ES6進行開發的確是處於領先水平,比如Koa需要基於Nodejs 0.11.9以上的版本執行,而目前nodejs的文字版本是0.10.33。這是一件可以算作好也可以算作不好的事情,就像Express開發者有很多中介軟體要選擇甚至自己開發中介軟體一樣。比如我們在上面看到的一樣,對於路由來說就有很多中介軟體供我們選擇。
5.3 Hapi
5.3.1 優勢
Hapi一直很自豪的說他們的框架是配置優於程式碼,當然也有很多開發者可能會質疑把這一點算作是優勢。但這一點對於大型專案組來說,的確是可以保持程式碼的統一性以及程式碼複用性。另外這款框架是由沃爾瑪實驗室支援的,也有很多大公司線上上環境使用Hapi,表明他已經通過了嚴峻的測試,因為這些公司會考慮得更多才會使用Hapi來執行他們的專案。因此所有的這些跡象都表明Hapi正在朝一個偉大的框架發展。
5.3.2 劣勢
Hapi的定位更傾向於大型或複雜的應用程式。對於一個簡單的應用來說,Hapi在程式碼上反而有些顯得冗餘了,另外目前Hapi所提供的樣例程式也比較少,使用Hapi進行開發的開源應用同樣很少。因此,如果選擇Hapi的話,你可能要投入更多精力進行開發,而不是簡單的呼叫一個第三方中介軟體。
6、總結
我們已經看了三個框架還算不錯具有代表性的一些樣例程式碼。Express仍然是當下最為流行,以及最被人所知曉的框架。當開始一個新的開發專案時,可能大家的第一反應就是用Express來建立一個服務。但是現在更希望大家多考慮考慮使用Koa或者Hapi。Koa積極擁抱ES6的語法,展示了promise的真正魅力。目前整個web開發社群也都意識到ES6的優勢,正在逐步往上面遷移。Hapi應該是大型專案組或者大型專案的第一選擇。他所倡導的配置優於程式碼會使得專案組 在程式碼的重複性上受益不淺,這也正是大多數專案組所追求的目標。現在行動起來,嘗試一款新的框架吧,可能你會喜歡他也可能會討厭他,但如果不去嘗試你永遠也不會知道結果是什麼,最終所有的這些經歷都會讓你成長為一個更加優秀的開發者。