注:截止本文完成时,hapi 的版本为:16.4.3
在我个人的认知中,route(路由)是任何一个框架中最为基础,也是最为重要的部分。
首先,假设我的练习项目的目录结构为:
.
├── package.json
├── public
│ ├── doom.jpg
│ └── doom3.jpg
└── server.js
目录 public 下的2张图片用作练习,随便找2张即可。
和其它许多支持设定 route 的框架一样,hapi 的 route 由3个基本要素组成:
所以,一个最简单的 hapi 程序就是在 server.js 文件中写下:
'use strict';
const Hapi = require('hapi');
const server = new Hapi.Server();
server.connection({ port: 3000, host: 'localhost' });
server.route({
method: 'GET',
path: '/article',
handler: function (request, reply) {
reply('Article list');
}
});
server.start((err) => {
if (err) {
throw err;
}
console.log('Server running at:', server.info.uri);
});
然后访问 http://localhost:3000/article 时就会看到输出“Article list”。
上面的例子中,当用户访问 /article 时会返回一个“Article list(文章列表)”,那如果用户想要访问 id 为123的那篇文章,对应的 route 怎么写呢?很简单,我们再增加一个 route 配置就行了,如下:
'use strict';
const Hapi = require('hapi');
const server = new Hapi.Server();
server.connection({ port: 3000, host: 'localhost' });
//显示文章列表
server.route({
method: 'GET',
path: '/article',
handler: function (request, reply) {
reply('Article list');
}
});
//显示特定 ID 的文章内容
server.route({
method: 'GET',
path: '/article/{id}', //Path 中用“{}”括起来的部分就表示 route 中的参数名称。
handler: function (request, reply) {
var article_id = request.params.id; //获取参数的值也很直观。
reply('Article ID is: ' + article_id);
}
});
server.start((err) => {
if (err) {
throw err;
}
console.log('Server running at:', server.info.uri);
});
此时,如果访问 http://localhost:3000/article/123 会看到输出“Article ID is: 123”。
虽然现在可以看到 ID 为123的文章了,但假如用户忘记了输入“123”而访问 http://localhost:3000/article/ 时就会看到错误提示:
{
statusCode: 404,
error: "Not Found",
message: "Not Found"
}
如何让 ID 成为可选的呢?当没有输入具体 ID 时给予用户一个友好的提示信息。办法是修改“/article/{id}”为“/article/{id?}”。修改后的 server.js 如下:
'use strict';
const Hapi = require('hapi');
const server = new Hapi.Server();
server.connection({ port: 3000, host: 'localhost' });
//注意,这里取掉了原有的“/article”的 route 定义。因为当“/article/{id}”中的“id”变为可选后,其定义就和“/article”相冲突了。
//在冲突的情况下如果运行 server.js 会看到错误提示“Error: New route /article/{id?} conflicts with existing /article”。
//显示特定 ID 的文章内容
server.route({
method: 'GET',
path: '/article/{id?}', //为 path 中的参数加上“问号”就表示该参数是可选的。
handler: function (request, reply) {
var article_id = request.params.id ? request.params.id : false;
if (article_id) {
reply('Article ID is: ' + article_id);
} else {
reply('找不到对应的文章');
}
}
});
server.start((err) => {
if (err) {
throw err;
}
console.log('Server running at:', server.info.uri);
});
当再访问 http://localhost:3000/article/ 时就会看到比较友好的提示信息“找不到对应的文章”。
简单的说就是,只有 route 中的“最后一个”参数可以被设定为可选参数,即:
很容易记的规则,对吧?
假设我们设定了2个 route,分别为:
此时,如果我们访问 http://localhost:3000/filename.jpg,那么负责响应的将会是第一个 route,而不是第二个。
'use strict';
const Hapi = require('hapi');
const server = new Hapi.Server();
server.connection({ port: 3000, host: 'localhost' });
server.route({
method: 'GET',
path: '/filename.jpg',
handler: function (request, reply) {
reply('I am the 1st route.');
}
});
server.route({
method: 'GET',
path: '/filename.{ext}',
handler: function (request, reply) {
reply('I am the 2nd route.');
}
});
server.start((err) => {
if (err) {
throw err;
}
console.log('Server running at:', server.info.uri);
});
所以,牢记是第一个匹配的 route 生效。
先说一个 route 中的概念“segment(分割)”,所谓“分割”就是指 path 中的“/”,比如“/greet/Jack”这个 route 中就有2个 segment:greet 部分和 Jack 部分。
假设我们定义一个 route 用来向人们问好,代码应该是:
'use strict';
const Hapi = require('hapi');
const server = new Hapi.Server();
server.connection({ port: 3000, host: 'localhost' });
server.route({
method: 'GET',
path: '/greet/{name}',
handler: function (request, reply) {
const name = request.params.name;
reply('Hi! ' + name);
}
});
server.start((err) => {
if (err) {
throw err;
}
console.log('Server running at:', server.info.uri);
});
当我们访问 http://localhost:3000/greet/Jack 时会看到“Hi! Jack”。但如果我们想要以 http://localhost:3000/greet/Jack/Tome/Bill 的形式同时问候多个人的时候就会报错。因为在 hapi 看来,'/greet/{name}' 中的 name 对应的只是 greet 之后的那个 segment,而多出来的部分就无法匹配到相应的 route 了。这种情况下,可以使用“通配符”。让我们修改代码为:
server.route({
method: 'GET',
path: '/greet/{name*3}', //这里加上了一个通配符“*3”
handler: function (request, reply) {
const name = request.params.name;
reply('Hi! ' + name);
}
});
现在,再访问 http://localhost:3000/greet/Jack/Tome/Bill 看看,会得到输出“Hi! Jack/Tome/Bill”。很好,可如果我们再多加一个人名呢?访问 http://localhost:3000/greet/Jack/Tome/Bill/Max 看看,又报错了吧?我们再次修改代码:
server.route({
method: 'GET',
path: '/greet/{name*}', //这里的通配符去掉了“3”,只留下“*”。
handler: function (request, reply) {
const name = request.params.name;
reply('Hi! ' + name);
}
});
现在再访问 http://localhost:3000/greet/Jack/Tome/Bill/Max 试试,可以正常得到输出了吧?
联系前后来看,我们就可以明白 route 中通配符的工作原理了:当通配符“*“配合数字使用时表示匹配具体的 segment 数量。而当舍弃数字使用时,就表示匹配所有的 segment 了。也正因为这个特定,所以通配符只能用在 route 中的最后一个 segment 中。
另外需要注意的时,当使用通配符时,接收到的参数是带有“/”符号的,比如“Jack/Tome/Bill/Max”。如果想要区分它们,只能自己处理,比如:
server.route({
method: 'GET',
path: '/greet/{name*}',
handler: function (request, reply) {
const name = request.params.name.split('/'); //分割处理
reply('Hello ' + userParts[0] + ' ' + userParts[1] + '!');
}
});
Route 中的 handler 是一个函数,该函数接收2个参数:request 和 reply,顾名思义一个负责处理来自外界的请求,一个负责处理响应和输出。这2个参数都是 object,也没什么可讲的,模仿范例多去用就明白了。两者的具体参考见 API 文档:
在 hapi 的 route 中定义中还可以传入一个 config 对象,类似:
server.route({
method: 'GET',
path: '/hello/{user?}',
handler: function (request, reply) {
const user = request.params.user ? encodeURIComponent(request.params.user) : 'stranger';
reply('Hello ' + user + '!');
},
config: {
description: 'Say hello!',
notes: 'The user parameter defaults to \'stranger\' if unspecified',
tags: ['api', 'greeting']
}
});
用官方的话说就是,这个 config 对象在代码的运行上什么用都没有,但是配合某些插件后可以自动为你的 route 生成说明文档。现在明白为什么说 hapi 适合用来写 API 服务了吧。