Express 和 hapi 如何相互比较?
Posted
技术标签:
【中文标题】Express 和 hapi 如何相互比较?【英文标题】:How do Express and hapi compare to each other? 【发布时间】:2015-08-08 18:57:00 【问题描述】:从 Web 应用程序设计和开发的角度来看,Express 和 Hapi 如何相互比较?对于基本示例,它们看起来很相似,但我有兴趣了解更多有关整体应用程序结构的关键差异的信息。
例如,据我了解,Hapi 使用different 路由机制,不考虑注册顺序,可以进行更快的查找,但与 Express 相比有限。还有其他重要的区别吗?
还有一个article 关于选择 Hapi(通过 Express)开发新的 npmjs.com 网站,这篇文章指出“Hapi 的插件系统意味着我们可以隔离应用程序的不同方面和服务,允许将来使用微服务。另一方面,Express 需要更多配置才能获得相同的功能”,这到底是什么意思?
【问题讨论】:
【参考方案1】:这是一个大问题,需要很长的答案才能完整,所以我将只解决最重要差异的一个子集。抱歉,这仍然是一个冗长的答案。
它们有何相似之处?
你说的完全正确:
对于基本示例,它们看起来很相似
两个框架都在解决相同的基本问题:为在 node.js 中构建 HTTP 服务器提供方便的 API。也就是说,比单独使用底层原生的http
模块更方便。 http
模块可以做我们想做的一切,但是用它来编写应用程序很乏味。
为了实现这一点,它们都使用了在高级 Web 框架中已经存在很长时间的概念:路由、处理程序、插件、身份验证模块。它们可能并不总是具有相同的名称,但它们大致相同。
大多数基本示例如下所示:
创建路线 请求路由时运行函数,准备响应 响应请求快递:
app.get('/', function (req, res)
getSomeValue(function (obj)
res.json(an: 'object');
);
);
哈比:
server.route(
method: 'GET',
path: '/',
handler: function (request, reply)
getSomeValue(function (obj)
reply(obj);
);
);
这里的区别并不完全是开创性的,对吧?那么为什么要选择一个而不是另一个呢?
它们有何不同?
简单的答案是 hapi 更多,而且开箱即用。当您只看上面的简单示例时,这可能不清楚。事实上,这是故意的。简单的案例保持简单。因此,让我们来看看其中的一些重大差异:
哲学
Express 旨在实现最小化。通过在http
上为您提供一个小API,只需轻轻拂去一层薄薄的灰尘,您在添加附加功能方面仍然非常独立。如果要读取传入请求的正文(相当常见的任务),则需要安装separate module。如果您希望将各种内容类型发送到该路由,您还需要检查 Content-type
标头以检查它是什么并相应地对其进行解析(例如,form-data vs JSON vs multi-part),通常使用单独的模块。
hapi 具有丰富的功能集,通常通过配置选项公开,而不需要编写代码。例如,如果我们想确保在处理程序运行之前将请求正文(有效负载)完全读入内存并进行适当的解析(自动基于内容类型),这只是一个简单的option:
server.route(
config:
payload:
output: 'data',
parse: true
,
method: 'GET',
path: '/',
handler: function (request, reply)
reply(request.payload);
);
特点
您只需比较两个项目的 API 文档,即可看到 hapi 提供了更大的功能集。
hapi 包含以下一些 Express 不具备的内置功能(据我所知):
Input and response validation(通过Joi) Server-side caching 具有多种存储选项(mongo、S3、redis、riak),只需几行配置即可启用 Cookie-parsing 会话 文件上传/分段解析 CORS 支持 Logging可扩展性和模块化
hapi 和 Express 以完全不同的方式处理可扩展性。使用 Express,您可以使用 middleware 功能。中间件函数有点像你堆叠起来的过滤器,所有请求在到达你的处理程序之前都会通过它们。
hapi 有request lifecycle 并提供extension points,它们与中间件功能相当,但在请求生命周期中存在几个定义点。
Walmart 构建 hapi 并停止使用 Express 的原因之一是对将 Express 应用拆分为单独的部分并让不同的团队成员安全地处理其块非常困难感到沮丧。出于这个原因,他们在 hapi 中创建了plugin system。
插件就像一个子应用程序,您可以在 hapi 应用程序中做所有可以做的事情,添加路由、扩展点等。在插件中,您可以确保不会破坏应用程序的其他部分,因为路由的注册顺序无关紧要,您不能创建冲突的路由。然后,您可以将此插件组合到服务器中并进行部署。
生态系统
由于 Express 提供的开箱即用功能非常少,因此当您需要向项目添加任何内容时,您需要向外看。很多时候,在使用 hapi 时,您需要的功能要么是内置的,要么是核心团队创建的模块。
Minimal 听起来很棒。但是,如果您正在构建一个严肃的生产应用程序,那么您最终可能会需要所有这些东西。
安全
hapi 由 Walmart 团队设计,用于管理黑色星期五的流量,因此安全性和稳定性一直是最受关注的问题。出于这个原因,框架做了很多额外的事情,例如限制传入的有效负载大小以防止耗尽您的进程内存。它还具有诸如最大事件循环延迟、使用的最大 RSS 内存和 v8 堆的最大大小之类的选项,超过这些选项您的服务器将响应 503 超时而不仅仅是崩溃。
总结
您自己评估它们。考虑一下您的需求以及两者中的哪一个可以解决您最关心的问题。在两个社区(IRC、Gitter、Github)中畅游,看看你喜欢哪个。不要只听我的话。和快乐的黑客!
免责声明:作为book on hapi 的作者,我有偏见,以上主要是我个人的观点。
【讨论】:
Matt,感谢您的大量帖子,“可扩展性和模块化”和“安全性”部分对我来说是最有帮助的部分。我想值得一提的是,Express 4 中的新路由系统为子应用程序提供了改进的模块化。 很好的答案马特。我们也对 Hapi 和 Express 感到困惑,我们看到 Hapi 的一个缺点是它没有像 Express 那样广泛的社区支持,如果我们卡在某个地方,可能会成为一个主要问题。需要您对此发表意见。 Express 是通用的,而 hapi 是更多的企业。 @MattHarrison 很好的答案,现在我正在读你关于 Hapi 的书,它很棒。我即将开发一个新的图书市场,在后端使用 Hapi,在前端使用 vue.js,在习惯了 Hapi 之后,我想积极参与 Hapi 项目。 @Humoyun 太好了!请注意,自 youtube.com/playlist?list=PLi303AVTbxaxqjaSWPg94nccYIfqNoCHz【参考方案2】:我的组织与 Hapi 合作。这就是我们喜欢它的原因。
Hapi 是:
得到主要军团的支持。这意味着社区支持将很强大,并且在未来的版本中为您提供支持。很容易找到热情的 Hapi 人,并且那里有很好的教程(尽管不像 ExpressJs 教程那样数量和庞大)。截至本文发布之日,npm 和沃尔玛使用 Hapi。 它可以方便分布式团队处理后端服务的各个部分,而无需全面了解 API 表面的其余部分(Hapi 的插件架构就是这种品质的缩影)。 让框架做框架应该做的事情:配置。之后,框架应该是不可见的,并允许开发人员将他们真正的创造力集中在构建业务逻辑上。使用 Hapi 一年后,我绝对觉得 Hapi 做到了这一点。我……感觉很开心!如果您想直接听取 Eran Hammer(Hapi 的领导)的意见
在过去四年中,hapi 逐渐成为许多项目的首选框架,无论大小。 hapi 的独特之处在于它能够扩展到大型部署和大型团队。随着项目的发展,其复杂性也随之增加——工程复杂性和流程复杂性。 hapi 的架构和理念可以处理增加的复杂性,而无需不断重构代码[read more]
开始使用 Hapi 不会像 ExpressJs 那样容易,因为 Hapi 没有相同的“明星力量”......但是一旦你感觉舒服,你就会获得很多里程。作为一个不负责任地使用 ExpressJs 几年的新黑客,我花了大约 2 个月的时间。如果您是一位经验丰富的后端开发人员,您就会知道如何阅读文档,而且您可能甚至不会注意到这一点。
Hapi 文档可以改进的领域:
-
如何验证用户和创建会话
处理跨域请求 (CORS)
上传文件(多部分,分块)
我认为身份验证将是其中最具挑战性的部分,因为您必须决定使用哪种身份验证策略(基本身份验证、Cookie、JWT 令牌、OAuth)。虽然从技术上讲,会话/身份验证环境如此分散并不是 Hapi 的问题……但我确实希望他们为此提供一些帮助。这将大大增加开发者的幸福感。
剩下的两个其实并没有那么难,文档可以写得稍微好一点。
【讨论】:
【参考方案3】:关于 Hapi 或为什么选择 Hapi JS 的速览?
Hapi 以配置为中心 它在框架中内置了身份验证和授权 它是在经过实战考验的气氛中发布的,并且确实证明了它的价值 所有模块都有 100% 的测试覆盖率 它注册了远离核心 HTTP 的最高抽象级别 通过插件架构轻松兼容
Hapi 在性能方面是更好的选择 Hapi 使用不同的路由机制,它可以进行更快的查找,并考虑注册顺序。 然而,与 Express 相比,它非常有限。并且由于 Hapi 插件系统,它是可能的 隔离不同方面和服务,这些方面和服务将在未来以多种方式帮助应用程序。
用法
与 Express 相比,Hapi 是最受欢迎的框架。 Hapi 主要用于大型企业应用程序。
开发人员在创建企业应用程序时不选择 Express 的几个原因是:
在 Express 中路由更难编写
中间件在大多数情况下都会成为障碍;每次定义路线时,都必须编写尽可能多的代码。
对于希望构建 RESTful API 的开发人员来说,Hapi 将是最佳选择。 Hapi 具有微服务架构,还可以根据某些参数将控制从一个处理程序转移到另一个处理程序。使用 Hapi 插件,您可以享受 围绕 HTTP 进行更高级别的抽象,因为您可以将业务逻辑划分为易于管理的部分。
Hapi 的另一个巨大优势是,当您配置错误时,它会提供详细的错误消息。 Hapi 还允许您默认配置文件上传大小。如果最大上传大小受到限制,您可以向用户发送错误消息,告知文件大小太大。这将保护您的服务器免于崩溃,因为文件上传将不再尝试缓冲整个文件。
无论使用 express 可以实现什么,使用 hapi.js 也可以轻松实现。
Hapi.js 非常时尚,代码组织得很好。如果您看到它是如何进行路由并将核心逻辑放入控制器中的 你一定会爱上它的。
Hapi.js 官方提供了几个专门用于 hapi.js 的插件,范围从基于令牌的身份验证到会话管理等等, 这是一个广告。不是说传统的npm不能用,都是hapi.js支持的
如果您在 hapi.js 中编写代码,代码将非常易于维护。
【讨论】:
“如果您看到它是如何进行路由并将核心逻辑放入控制器中的......”。我在文档中没有看到任何显示控制器使用的示例。所有路由示例都使用作为函数的处理程序属性。我将这种方式与 Laravel(php 框架)和 AdonisJs(Node.js 框架)在路由方面所做的比较,在其中我们可以使用控制器进行路由。我可能错过了 HAPI 文档中显示使用控制器进行路由的部分内容。所以如果这个功能确实存在的话,那对我来说是好事,因为我习惯在 Laravel 中使用控制器进行路由。【参考方案4】:我最近开始使用 Hapi,对此我很满意。我的理由是
更容易测试。例如:
server.inject
允许您运行应用并在不运行和监听的情况下获得响应。
server.info
给出当前的 uri、端口等。
server.settings
访问配置,例如server.settings.cache
获取当前缓存提供者
如有疑问,请查看应用程序的任何部分或支持的插件的 /test
文件夹,以查看有关如何模拟/测试/存根等的建议。
我的感觉是 hapi 的架构模型允许您信任但验证例如是我的plugins registered 吗?如何声明module dependency?
它开箱即用,例如file uploads,从端点返回流等。
基本插件与核心库一起维护。例如template parsing、caching 等。额外的好处是相同的编码标准适用于基本事物。
合理的错误和错误处理。 Hapi validates config options 并保留内部路由表以防止重复路由。这在学习时非常有用,因为早期会抛出错误,而不是需要调试的意外行为。
【讨论】:
【参考方案5】:再补充一点,Hapi 从版本 16 开始支持“http2”调用(如果我没记错的话)。然而,express 直到 express 4 才直接支持“http2”模块。尽管他们已经在 express 5 的 alpha 版本中发布了该功能。
【讨论】:
【参考方案6】:'use strict';
const Hapi = require('hapi');
const Basic = require('hapi-auth-basic');
const server = new Hapi.Server();
server.connection(
port: 2090,
host: 'localhost'
);
var vorpal = require('vorpal')();
const chalk = vorpal.chalk;
var fs = require("fs");
var utenti = [
name: 'a',
pass: 'b'
,
name: 'c',
pass: 'd'
];
const users =
john:
username: 'john',
password: 'secret',
name: 'John Doe',
id: '2133d32a'
,
paul:
username: 'paul',
password: 'password',
name: 'Paul Newman',
id: '2133d32b'
;
var messaggi = [
destinazione: 'a',
sorgente: 'c',
messsaggio: 'ciao'
,
destinazione: 'a',
sorgente: 'c',
messsaggio: 'addio'
,
destinazione: 'c',
sorgente: 'a',
messsaggio: 'arrivederci'
];
var login = '';
var loggato = false;
vorpal
.command('login <name> <pass>')
.description('Effettua il login al sistema')
.action(function (args, callback)
loggato = false;
utenti.forEach(element =>
if ((element.name == args.name) && (element.pass == args.pass))
loggato = true;
login = args.name;
console.log("Accesso effettuato");
);
if (!loggato)
console.log("Login e Password errati");
callback();
);
vorpal
.command('leggi')
.description('Leggi i messaggi ricevuti')
.action(function (args, callback)
if (loggato)
var estratti = messaggi.filter(function (element)
return element.destinazione == login;
);
estratti.forEach(element =>
console.log("mittente : " + element.sorgente);
console.log(chalk.red(element.messsaggio));
);
else
console.log("Devi prima loggarti");
callback();
);
vorpal
.command('invia <dest> "<messaggio>"')
.description('Invia un messaggio ad un altro utente')
.action(function (args, callback)
if (loggato)
var trovato = utenti.find(function (element)
return element.name == args.dest;
);
if (trovato != undefined)
messaggi.push(
destinazione: args.dest,
sorgente: login,
messsaggio: args.messaggio
);
console.log(messaggi);
else
console.log("Devi prima loggarti");
callback();
);
vorpal
.command('crea <login> <pass>')
.description('Crea un nuovo utente')
.action(function (args, callback)
var trovato = utenti.find(function (element)
return element.name == args.login;
);
if (trovato == undefined)
utenti.push(
name: args.login,
pass: args.pass
);
console.log(utenti);
callback();
);
vorpal
.command('file leggi utenti')
.description('Legge il file utenti')
.action(function (args, callback)
var contents = fs.readFileSync("utenti.json");
utenti = JSON.parse(contents);
callback();
);
vorpal
.command('file scrivi utenti')
.description('Scrive il file utenti')
.action(function (args, callback)
var jsontostring = JSON.stringify(utenti);
fs.writeFile('utenti.json', jsontostring, function (err)
if (err)
return console.error(err);
);
callback();
);
vorpal
.command('file leggi messaggi')
.description('Legge il file messaggi')
.action(function (args, callback)
var contents = fs.readFileSync("messaggi.json");
messaggi = JSON.parse(contents);
callback();
);
vorpal
.command('file scrivi messaggi')
.description('Scrive il file messaggi')
.action(function (args, callback)
var jsontostring = JSON.stringify(messaggi);
fs.writeFile('messaggi.json', jsontostring, function (err)
if (err)
return console.error(err);
);
callback();
);
// leggi file , scrivi file
vorpal
.delimiter(chalk.yellow('messaggi$'))
.show();
const validate = function (request, username, password, callback)
loggato = false;
utenti.forEach(element =>
if ((element.name == username) && (element.pass == password))
loggato = true;
console.log("Accesso effettuato");
return callback(null, true,
name: username
)
);
if (!loggato)
return callback(null, false);
;
server.register(Basic, function (err)
if (err)
throw err;
);
server.auth.strategy('simple', 'basic',
validateFunc: validate
);
server.route(
method: 'GET',
path: '/',
config:
auth: 'simple',
handler: function (request, reply)
reply('hello, ' + request.auth.credentials.name);
);
//route scrivere
server.route(
method: 'POST',
path: '/invia',
config:
auth: 'simple',
handler: function (request, reply)
//console.log("Received POST from " + request.payload.name + "; id=" + (request.payload.id || 'anon'));
var payload = encodeURIComponent(request.payload)
console.log(request.payload);
console.log(request.payload.dest);
console.log(request.payload.messaggio);
messaggi.push(
destinazione: request.payload.dest,
sorgente: request.auth.credentials.name,
messsaggio: request.payload.messaggio
);
var jsontostring = JSON.stringify(messaggi);
fs.writeFile('messaggi.json', jsontostring, function (err)
if (err)
return console.error(err);
);
console.log(messaggi);
reply(messaggi[messaggi.length - 1]);
);
//route leggere (json)
server.route(
method: 'GET',
path: '/messaggi',
config:
auth: 'simple',
handler: function (request, reply)
messaggi = fs.readFileSync("messaggi.json");
var estratti = messaggi.filter(function (element)
return element.destinazione == request.auth.credentials.name;
);
var s = [];
console.log(request.auth.credentials.name);
console.log(estratti.length);
estratti.forEach(element =>
s.push(element);
//fare l'array con stringify
//s+="mittente : "+element.sorgente+": "+element.messsaggio+"\n";
);
var a = JSON.stringify(s);
console.log(a);
console.log(s);
reply(a);
);
server.start(function ()
console.log('Hapi is listening to ' + server.info.uri);
);
function EseguiSql(connection, sql, reply)
var rows = [];
request = new Request(sql, function (err, rowCount)
if (err)
console.log(err);
else
console.log(rowCount + ' rows');
console.log("Invio Reply")
reply(rows);
);
request.on('row', function (columns)
var row = ;
columns.forEach(function (column)
row[column.metadata.colName] = column.value;
);
rows.push(row);
);
connection.execSql(request);
server.route(
method: 'POST',
path: '/query',
handler: function (request, reply)
// Qui dovrebbe cercare i dati nel body e rispondere con la query eseguita
var connection = new Connection(config);
// Attempt to connect and execute queries if connection goes through
connection.on('connect', function (err)
if (err)
console.log(err);
else
console.log('Connected');
console.log(request.payload.sql);
EseguiSql(connection, request.payload.sql, reply);
);
);
server.connection(
host: process.env.HOST || 'localhost',
port: process.env.PORT || 8080
);
var config =
userName: process.env.DB_USER,
password: process.env.DB_PASSWORD,
server: process.env.DB_SERVER,
options:
database: process.env.DB_NAME,
encrypt: true
【讨论】:
欢迎来到 ***。您能否详细说明您的回答以及它与 OP 发布的问题的关系?以上是关于Express 和 hapi 如何相互比较?的主要内容,如果未能解决你的问题,请参考以下文章
ionic 后台Api服务, 使用rest-hapi , node.js 创建 RESTful API Service , 附完整源代码