Express和hapi如何比较?


133

从Web应用程序设计和开发的角度来看,Express和Hapi如何相互比较?对于基本示例,它们看起来很相似,但是我有兴趣了解有关整个应用程序结构中关键差异的更多信息。

例如,据我了解,Hapi使用不同的路由机制,该机制不考虑注册顺序,可以进行更快的查找,但是与Express相比受到限制。还有其他重要区别吗?

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

Answers:


231

这是一个很大的问题,需要长答案才能完成,因此,我仅解决最重要差异的一部分。抱歉,这仍然是一个冗长的答案。

它们有何相似之处?

您说的是绝对正确的:

对于基本示例,它们似乎相似

两种框架都在解决相同的基本问题:提供一个方便的API,用于在节点中构建HTTP服务器。也就是说,比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的目标是极简的。通过为您提供一个小的API,并在上加一点灰尘http,就增加其他功能而言,您仍然可以独自一人。如果要读取传入请求的正文(非常常见的任务),则需要安装一个单独的模块。如果您希望将各种内容类型发送到该路由,则还需要检查Content-type标头以检查其内容并相应地进行解析(例如,表单数据,JSON和多部分),通常使用单独的模块。

hapi具有丰富的功能集,通常通过配置选项公开,而不需要编写代码。例如,如果我们要确保在运行处理程序之前将请求正文(有效负载)完全读入内存并进行适当地解析(自动基于内容类型),那么这只是一个简单的选择

server.route({
    config: {
        payload: {
            output: 'data',
            parse: true
        }
    },
    method: 'GET',
    path: '/',
    handler: function (request, reply) {

        reply(request.payload);
    }
});

特征

您只需要比较两个项目的API文档,即可看到hapi提供了更大的功能集。

hapi包括Express内置的以下某些内置功能(据我所知):

可扩展性和模块化

hapi和Express以完全不同的方式实现可扩展性。使用Express,您可以使用中间件功能。中间件功能有点像过滤器,您可以堆叠它们,所有请求在到达处理程序之前都会通过它们运行。

hapi具有请求生命周期并提供扩展点,这些扩展点与中间件功能相当,但是在请求生命周期中存在几个定义的点。

沃尔玛制造hapi并停止使用Express的原因之一是对将Express应用程序拆分成单独的部分以及让不同的团队成员安全地工作的困难程度感到沮丧。因此,他们在hapi中创建了插件系统

插件就像一个子应用程序,您可以在hapi应用程序中做所有事情,添加路由,扩展点等。在插件中,您可以确保不破坏应用程序的另一部分,因为顺序路线注册无关紧要,并且您不能创建冲突的路线。然后,您可以将此插件组合到服务器中并进行部署。

生态系统

由于Express开箱即用的功能很少,因此当您需要向项目中添加任何内容时,您需要向外看。很多时候使用hapi时,您需要的功能要么是内置的,要么是核心团队创建的模块。

最小的声音很棒。但是,如果您要构建一个认真的生产应用程序,则最终可能会需要所有这些东西。

安全

hapi由沃尔玛团队设计,用于运行黑色星期五交通,因此安全性和稳定性一直是头等大事。因此,该框架做了很多其他事情,例如限制传入的有效负载大小以防止耗尽进程内存。它还具有一些选项,例如最大事件循环延迟,使用的最大RSS内存和v8堆的最大大小,超过这些限制,服务器将以503超时响应而不仅仅是崩溃。

摘要

自己评估它们。考虑一下您的需求,以及这两个解决您最大的顾虑。在两个社区(IRC,Gitter和Github)中畅游一番,看看您喜欢哪个。不要只听我的话。祝您黑客愉快!


免责声明:我对一本有关hapi的作者有偏见,以上内容是我个人的看法。


7
马特,感谢您发表大量文章,“可扩展性和模块化”和“安全性”部分对我来说是最有用的部分。我想值得一提的是Express 4中的新路由系统为子应用程序提供了改进的模块化。
阿里·沙基巴

1
好的答案,马特。我们也对Hapi和Express感到困惑,我们看到的Hapi的一个缺点是它没有像Express那样广泛的社区支持,如果我们被困在某个地方,可能会成为一个主要问题。同样需要您的意见。
阿曼·古普塔

1
Express是通用的,而hapi则是更多的企业。
windmaomao

1
@MattHarrison很好的答案,现在我正在阅读您关于Hapi的书,太好了。适应Hapi之后,我将要在后端使用Hapi并在前端使用vue.js开发书籍的新市场,我希望积极参与Hapi项目。
Humoyun Ahmad

1
@Humoyun太好了!但是请注意,自<= v16.0.0起,有一个新的hapi主版本进行了相当大的更改。我目前正在制作一个旨在让人们学习v17的截屏视频系列:youtube.com/playlist
Matt Harrison

54

我的组织正在使用Hapi。这就是为什么我们喜欢它。

哈比是:

  • 在主要军团的支持下。这意味着社区支持将很强大,并且在以后的发行版中都将为您提供支持。找到热情的Hapi员工很容易,并且那里有不错的教程(尽管不如ExpressJs教程那么多且广泛)。截至本发布日期,npm和沃尔玛使用Hapi。
  • 它可以促进在后端服务的各个部分上工作的分布式团队的工作,而无需全面了解其余API表面(Hapi的插件体系结构就是这种品质的缩影)。
  • 让框架执行框架应做的事情:配置事物。之后,该框架应该是不可见的,并允许开发人员将其真正的创造力集中在构建业务逻辑上。使用Hapi一年后,我肯定会觉得Hapi实现了这一目标。我感到开心!

如果您想直接从Eapi Hammer(Hapi的负责人)那里听到

在过去的四年中,hapi成为许多大小项目的首选框架。hapi的独特之处在于它具有扩展到大型部署和大型团队的能力。随着项目的发展,其复杂性也随之增加–工程复杂性和过程复杂性。hapi的体系结构和理念可以处理不断增加的复杂性,而无需不断重构代码[阅读更多]

开始使用Hapi不会像ExpressJs那样容易,因为Hapi没有相同的“明星力量” ...但是一旦您感到舒适,您将获得很多里程。作为一名新黑客,我以不负责任的方式使用ExpressJ几年了约2个月。如果您是经验丰富的后端开发人员,您将知道如何阅读文档,甚至可能不会注意到这一点。

Hapi文档可以改进的地方:

  1. 如何验证用户身份并创建会话
  2. 处理跨域请求(CORS)
  3. 上传文件(分段,分块)

我认为身份验证将是其中最具挑战性的部分,因为您必须确定要使用哪种身份验证策略(基本身份验证,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还允许您默认配置文件上传大小。如果最大上传大小受到限制,则可以向用户发送错误消息,告知文件大小太大。这样可以防止服务器崩溃,因为文件上传将不再尝试缓冲整个文件。

  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

1

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

  1. 更容易测试。例如:

    • server.inject 允许您运行该应用并获得响应,而无需运行和收听。
    • server.info 给出当前的uri,端口等
    • server.settings访问配置,例如server.settings.cache获取当前的缓存提供程序
    • 如有疑问/test,请查看应用程序任何部分的文件夹或受支持的插件,以查看有关如何模拟/测试/存根的建议。
    • 我的感觉是hapi的体系结构模型允许您信任但可以验证,例如,我的插件是否已注册?如何声明模块依赖关系
  2. 它开箱即用,例如文件上传,从端点返回流等。

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

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


-1

补充一点,Hapi已开始支持从版本16开始的“ http2”调用(如果我没记错的话)。但是,直到Express 4为止,Express尚未直接支持'http2'模块。尽管他们已在Express 5的Alpha版中发布了该功能。


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

欢迎使用StackOverflow。您能否详细说明您的回答以及它与OP发布的问题之间的关系?
Szymon Maszke '19

-3
    const Hapi = require('hapi');
var Connection = require('tedious').Connection;
var Request = require('tedious').Request;
var TYPES = require('tedious').TYPES;
const server = new Hapi.Server();
var vorpal = require('vorpal')();

server.connection({
    host: process.env.HOST || 'localhost',
    port: process.env.PORT || 3000
});
server.start(function (err) {
    if (err) {
        throw err;
    }
    console.log("server running at : " + server.info.uri);
});

var config =
{
    userName: 'sa',
    password: 'password.123',
    server: 'localhost',

    options:
    {
        database: '',
        port: 1433
    }
}

server.route(
    {
        method: 'GET',
        path: '/{categoria}',
        handler: function (request, reply) {
            var connection = new Connection(config);
            connection.on('connect', function (err) {
                if (err) {
                    console.log(err);
                }
                else {
                    console.log('Connected');
                    EseguiSqlGet(connection, request.params.categoria, reply);
                }
            });
        }
    }
);
function EseguiSqlGet(connection, cat, reply) {
    var rows = [];
    var sql = 'SELECT * FROM Prodotti INNER JOIN Categorie
 on Categorie.IdCategoria = Prodotti.IdCategoria
 WHERE Categorie.IdCategoria = ' + cat ;
    request_sql = new Request(sql, function(err, rowCount) {
        if (err) {
            console.log(err);
        } else {
            console.log(rowCount + ' rows');
            console.log("Invio Reply")
            reply(rows);
        }
    });

    request_sql.on('row', function(columns) {
        var row = {};
        columns.forEach(function (column) {
            row[column.metadata.colName] = column.value;
        });
        rows.push(row);
    });

    connection.execSql(request_sql);
}
// POST
server.route(
    {
        method: 'POST',
        path: '/inserisci',
        handler: function (request, reply) {
            var connection = new Connection(config);
            connection.on('connect', function (err) {
                if (err) {
                    console.log(err);
                }
                else {
                    console.log('Connected');
                    EseguiSqlPost(connection,reply, 
request.payload.idcat, request.payload.nome, request.payload.prezzo );
                }
            });
        }
    }
);
function EseguiSqlPost(connection,reply, cat,nome,prezzo) {

    var sql = "INSERT INTO Prodotti
 VALUES("+ cat +",'"+nome+"',"+prezzo+")";
    request_sql = new Request(sql, function(err, rowCount) {
        if (err) {
            console.log(err);
        } else {
            console.log(rowCount + ' rows');
            console.log("Invio Reply")
            reply('riga aggiunta');
        }
    });

    /*request_sql.on('row', function(columns) {
        var row = {};
        columns.forEach(function (column) {
            row[column.metadata.colName] = column.value;
        });
        rows.push(row);
    });
*/
    connection.execSql(request_sql);
}






//VORPAL COMMAND PROMT
var categoria = [
    {

        'idcategoria':'1',
        'nome':'ciao',

    }
]


vorpal
    .command('inserisci <categoria> <nome>')
    .action(function(args, callback)
    {
        categoria.push(   
{'idcategoria':args.categoria,'nome':args.nome}     );
        console.log(JSON.stringify(categoria));
        callback();
    });

vorpal
.delimiter("delimeter")
.show();
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.