本文翻译自:
/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
6月26日,TJ Holowaychuk 第一次提交了Express的代码。在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是在一年以前也就是在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同学在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也算是一个状态值。详情了解:/#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 APIrouter.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);// indexapp.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 APIapp.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 routesapp.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应该是大型项目组或者大型项目的第一选择。他所倡导的配置优于代码会使得项目组 在代码的重复性上受益不浅,这也正是大多数项目组所追求的目标。现在行动起来,尝试一款新的框架吧,可能你会喜欢他也可能会讨厌他,但如果不去尝试你永远也不会知道结果是什么,最终所有的这些经历都会让你成长为一个更加优秀的开发者。