Express和hapi相比,它们有什么区别?

138

就网页应用的设计和开发而言,Express 和 Hapi 有什么区别?基本示例看起来相似,但我想了解整个应用程序结构中的关键差异。

例如,据我所学,Hapi 使用不同的路由机制,它不考虑注册顺序,可以更快地查找,但与 Express 相比有一定限制。是否还有其他重要的区别 ?

还有一篇关于在 npmjs.com 网站开发中选择使用 Hapi(而不是 Express)的文章,该文章指出:“Hapi 的插件系统意味着我们可以以将来允许微服务的方式隔离应用程序的不同方面和服务。另一方面,为了获得相同的功能,Express 需要更多的配置”。这究竟是什么意思?

6个回答

237

这是一个很大的问题,需要长篇回答才能完整解答,因此我只会涉及其中最重要的不同点。很抱歉这仍然是一篇冗长的回答。

它们有何相似之处?

当你说:

对于基本示例,它们看起来相似

你是完全正确的。

这两个框架都在解决同样的基本问题:为在node中构建HTTP服务器提供方便的API。也就是说,比单独使用较低级别的本机http模块更方便。http模块可以做我们想要的一切,但编写应用程序时会很繁琐。

为了实现这一点,它们都使用了高级Web框架中已经存在的概念:路由、处理程序、插件、身份验证模块。它们可能并不总是有相同的名称,但它们大致等效。

大多数基本示例看起来像这样:

  • 创建一个路由
  • 当请求该路由时运行函数,准备响应
  • 响应请求

Express:

app.get('/', function (req, res) {

    getSomeValue(function (obj) {

        res.json({an: 'object'});
    });
});

hapi:

server.route({
    method: 'GET',
    path: '/',
    handler: function (request, reply) {

        getSomeValue(function (obj) {

            reply(obj);
        });
    }
});

"The difference is not exactly groundbreaking here, right? So why choose one over the other?"

它们有什么不同?

"简单来说,hapi提供了更多的功能和开箱即用的功能。当您只看上面的简单示例时,可能并不明显。事实上,这是有意为之的。简单的情况保持简单。因此,让我们来看看一些重大的区别:"

哲学

"Express旨在非常简化。通过仅提供一个薄薄的http API,您仍然需要自己添加其他功能。如果您想读取传入请求的正文(这是一项相当常见的任务),则需要安装单独的模块。如果您希望该路由接收各种内容类型,则还需要检查Content-type标头以检查其类型并相应地解析它(例如表单数据与JSON与多部分),通常使用单独的模块。" 选项即可。
server.route({
    config: {
        payload: {
            output: 'data',
            parse: true
        }
    },
    method: 'GET',
    path: '/',
    handler: function (request, reply) {

        reply(request.payload);
    }
});

特点

只需比较两个项目的API文档,就可以看出hapi提供了更多的功能。

以下是hapi内置的一些Express没有的功能(据我所知):

可扩展性和模块化

和在可扩展性方面的处理方式有很大不同。在中,您有中间件函数。中间件函数有点像过滤器,您可以将它们堆叠起来,所有请求都会在到达处理程序之前通过它们运行。

具有请求生命周期并提供扩展点,这类似于中间件函数,但存在于请求生命周期的几个定义点。

建立并停止使用的原因之一是,在将应用程序拆分为单独的部分并使不同的团队成员安全地处理其块时,难度非常大。出于这个原因,他们在中创建了插件系统

插件就像子应用,你可以在hapi应用中做任何事情,添加路由、扩展点等。在插件中,你可以确保不会破坏应用程序的其他部分,因为路由的注册顺序并不重要,而且你不能创建冲突的路由。然后,您可以将这些插件组合成一个服务器并进行部署。

生态系统

因为Express在开箱方面提供的很少,所以当您需要向项目添加任何功能时,您需要寻找外部资源。很多时候,在使用hapi时,你需要的功能都是内置的或者由核心团队创建的模块。

最小化听起来很棒。但如果你正在构建一个严肃的生产应用程序,你最终可能需要所有这些东西。

安全性

hapi由沃尔玛团队设计,用于运行黑色星期五的流量,因此安全性和稳定性始终是首要关注的问题。因此,该框架会额外执行许多操作,例如限制传入负载大小以防止耗尽进程内存。它还具有一些选项,如最大事件循环延迟、最大RSS内存使用量和v8堆的最大大小,超出这些限制,您的服务器将响应503超时而不是崩溃。

总结

请自行评估两者之间的差异。考虑您的需求并确定哪个更能解决您最大的问题。在两个社区(IRC、Gitter、Github)中体验一下,看看您更喜欢哪一个。不要仅凭我的话来做决定。祝愉快编程!


免责声明:作为一本关于 hapi 的 书籍 的作者,我有偏见,上述观点大多是我的个人意见。


7
Matt,谢谢你写的详细文章,“可扩展性和模块化”以及“安全性”部分对我帮助最大。值得一提的是,在Express 4中新的路由系统为子应用程序提供了改进的模块化支持。 - Ali Shakiba
1
很棒的回答,Matt。我们也在Hapi和Express之间犹豫不决,我们发现Hapi的一个缺点是它没有像Express那样广泛的社区支持,如果我们遇到问题可能会成为一个重大问题。需要您对此发表意见。 - Aman Gupta
1
Express是通用的,而hapi则更加企业化。 - windmaomao
1
@MattHarrison 很棒的答案,我现在正在读你关于 Hapi 的书,非常好。我打算用 Hapi 做后端和 vue.js 做前端开发一个新的图书市场,熟悉 Hapi 后,我想积极参与 Hapi 项目。 - Humoyun Ahmad
1
@Humoyun 很棒!然而需要注意的是,自版本 <= v16.0.0 以来,hapi 现在已经有了一个新的主要版本,并做出了一些重大更改。我目前正在制作一系列用于帮助人们学习 v17 的视频教程:https://www.youtube.com/playlist?list=PLi303AVTbxaxqjaSWPg94nccYIfqNoCHz - Matt Harrison
显示剩余5条评论

54

我所在的组织选择使用Hapi。以下是我们喜欢它的原因:

Hapi有以下特点:

  • 得到大型企业的支持。这意味着社区支持将会很强大,并且会在未来版本中一直存在。易于找到热情的Hapi用户,并且有很好的教程(虽然不像ExpressJs教程那样广泛)。截至本帖发布日期,npm和沃尔玛(Walmart)都在使用Hapi。
  • 可以促进分布式团队在工作时处理后端服务的各个部分,而不必对API的其余部分有全面的了解(Hapi的插件架构体现了这种优势)。
  • 让框架做框架应该做的事情:配置。之后,框架应该是无形的,并允许开发人员将真正的创造力集中在构建业务逻辑上。使用Hapi一年后,我确实感觉Hapi实现了这一点。我感到快乐!

如果您想直接听到Eran Hammer(Hapi的主要开发者)的声音

在过去的四年中,hapi已成为许多项目的首选框架,无论大小。hapi的独特之处在于它能够扩展到大型部署和大型团队。随着项目的发展,复杂度也增加了——工程复杂度和过程复杂度。 hapi的架构和哲学处理了增加的复杂性,而无需不断重构代码。[阅读更多]

与ExpressJs相比,使用Hapi开始可能不会那么容易,因为Hapi没有同样的“明星效应”......但是一旦您感到舒适,将会得到很多好处。作为一个不负责任地使用ExpressJs几年的新手黑客,我用了大约2个月的时间。如果您是经验丰富的后端开发人员,您将知道如何阅读文档,甚至可能不会注意到这一点。

Hapi文档可以改进的领域:

  1. 如何验证用户并创建会话
  2. 处理跨域请求(CORS)
  3. 上传文件(多部分、分块)
  1. 我认为认证可能是最具挑战性的部分,因为您必须决定使用什么类型的身份验证策略(基本身份验证、Cookies、JWT Tokens、OAuth)。虽然从技术上讲,Hapi并不关心会话/身份验证领域如此分散...但我希望他们能为此提供一些指导。这将大大增加开发人员的幸福感。
  2. 其余两个并不是真正困难的,文档只需要写得更好一些。

2

Hapi JS的快速简介或者为什么选择Hapi JS?

Hapi是以配置为中心的框架。

它内置了身份验证和授权功能。

它在经过实战检验后被发布,真正证明了其价值。

所有模块都有100%的测试覆盖率。

它在核心HTTP之外注册了最高级别的抽象。

通过插件架构可以轻松组合。

Hapi在性能方面是更好的选择。

Hapi使用不同的路由机制,可以进行更快的查找,并考虑注册顺序。 然而,与Express相比,它相当有限。而且由于Hapi插件系统,可以将不同的方面和服务隔离出来, 这将在未来许多方面帮助应用程序。

用法

与Express相比,Hapi是大型企业应用程序的首选框架。

一些开发人员不选择Express创建企业应用程序的原因如下:

在Express中,路由更难组合。

中间件大多数情况下会妨碍;每次定义路由时,必须编写尽可能多的代码。

对于想要构建RESTful API的开发人员来说,Hapi是最好的选择。 Hapi具有微服务架构, 并且还可以根据某些参数将控制从一个处理程序转移到另一个处理程序。通过Hapi插件,您可以享受 围绕HTTP的更高级别的抽象,因为您可以将业务逻辑分成易于管理的部分。

Hapi的另一个巨大优势在于,当您进行错误配置时,它提供详细的错误消息。 Hapi还允许您默认配置文件上传大小。 如果限制了最大上传大小,则可以向用户发送错误消息,说明文件大小过大。这将保护您的服务器免于崩溃, 因为文件上传将不再尝试缓冲整个文件。

  1. 使用express可以轻松实现的任何功能也可以轻松使用hapi.js实现。

  2. Hapi.js非常时尚,可以很好地组织代码。如果您看到它如何进行路由并将核心逻辑放入控制器中, 您一定会喜欢它。

  3. Hapi.js官方提供了几个专门针对hapi.js的插件,范围从基于令牌的身份验证到会话管理等等,这是一个附加项。 这并不意味着不能使用传统的npm,所有这些都受到hapi.js的支持。

  4. 如果您在hapi.js中编写代码,则代码将非常易于维护。


如果你看到它如何进行路由并将核心逻辑放在控制器中...我没有看到文档中有任何示例显示使用控制器。所有路由示例都使用处理程序属性,这是一个函数。我将此方式与 Laravel(PHP 框架)和 AdonisJs(Node.js 框架)用于路由的方式进行比较,在这些框架中,我们可以使用控制器进行路由。我可能错过了 HAPI 文档中展示使用控制器进行路由的部分。因此,如果这个功能确实存在,对我来说会很好,因为我习惯在 Laravel 中使用控制器进行路由。 - Lex Soft
虽然我在使用ExpressJS工作多年后更喜欢Hapi(我的基准测试显示Hapi至少比ExpressJS快40%,甚至更多)...但是你提到的所有原因基本上都是我们在Express中很容易做到的标准事情...例如,文件限制...而且,中间件非常方便...我只需要编写一次路由和一个用于身份验证的中间件。我的Express代码非常干净,整个MVC结构也很易于维护。不管怎样,这只是我的个人意见。 - undefined

1

我最近开始使用Hapi,对它感到非常满意。我的原因是

  1. 更易测试。例如:

    • server.inject 允许您在不运行和侦听的情况下运行应用程序并获取响应。
    • server.info 提供当前 uri、端口等信息。
    • server.settings 访问配置,例如 server.settings.cache 获取当前缓存提供程序。
    • 如果有疑问,请查看应用程序或支持插件的 /test 文件夹,以获取如何模拟/测试/存根等的建议。
    • 我的感觉是 hapi 的架构模型允许您信任但验证,例如:我的 插件已注册?我如何声明 模块依赖项
  2. 开箱即用,例如:文件上传、从端点返回流等。

  3. 基本插件与核心库一起维护,例如 模板解析缓存 等。额外的好处是相同的编码标准适用于所有必要的内容。

  4. 合理的错误和错误处理。Hapi 验证配置选项 并保留内部路由表以防止重复路由。这在学习时非常有用,因为会及早抛出错误,而不是需要调试的意外行为。


0

还有一个需要补充的点,Hapi从16版本开始支持'http2'调用(如果我没记错的话)。然而,Express直到4版本仍未直接支持'http2'模块。尽管他们已经在Express 5的alpha版本中发布了这个功能。


-3
'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
    }
}

1
欢迎来到StackOverflow。您能详细阐述一下您的回答以及它与OP发布的问题的关系吗? - Szymon Maszke

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接