官方原文参考:
快速入门
设置一个服务器非常的快速和容易,下面是一个简单构建服务的例子
Copy var restify = require('restify');
function respond(req, res, next) {
res.send('hello ' + req.params.name);
next();
}
var server = restify.createServer();
server.get('/hello/:name', respond);
server.head('/hello/:name', respond);
server.listen(8080, function() {
console.log('%s listening at %s', server.name, server.url);
});
试着使用curl命令来获取使用restify建立的服务返回什么。
Copy $ curl -is http://localhost:8080/hello/mark -H 'accept: text/plain'
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 10
Date: Mon, 31 Dec 2012 01:32:44 GMT
Connection: keep-alive
hello mark
$ curl -is http://localhost:8080/hello/mark
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 12
Date: Mon, 31 Dec 2012 01:33:33 GMT
Connection: keep-alive
"hello mark"
$ curl -is http://localhost:8080/hello/mark -X HEAD -H 'connection: close'
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 12
Date: Mon, 31 Dec 2012 01:42:07 GMT
Connection: close
请注意,默认情况下,curl使用Connection: keep-alive
,为了使HEAD头方法立即返回,您需要通过设置Connection: close
。
由于CURL经常与REST API一起使用,restify含有一个插件来处理以上这种问题。插件检查用户代理是否为curl方式。如果是,则将连接头设置为“close”,并删除“Content-Length“ header。
Copy server.pre(restify.plugins.pre.userAgentConnection());
Sinatra 样式处理链
像许多其他基于Node.js的REST框架一样,restify利用Sinatra样式语法来定义路由和服务这些路由的函数处理程序:
备注:Sinatra是一款非常轻量的Web框架,基于Ruby语言开发,旨在以最小的精力为代价快速创建Web应用为目的的DSL(领域专属语言)。
Sinatra是Blake Mizerany在2007年9月开发的Ruby语言的Web框架,并在2009年1月18日发布0.9.0版本。
Sinatra最大的特点就是非常轻量、快速,整个源码也只有1000多行。
Copy server.get('/', function(req, res, next) {
res.send('home')
return next();
});
server.post('/foo',
function(req, res, next) {
req.someData = 'foo';
return next();
},
function(req, res, next) {
res.send(req.someData);
return next();
}
);
在restify服务器,有三个不同的处理程序链:
所有三个处理程序链都接受单个函数、多个函数或函数数组。
通用的pre(前置)操作链:server.pre()
通过 restify 服务器的 pre()方法可以注册处理器到 pre 处理器链中。对所有接收的 HTTP 请求,都会预先调用该处理器链中的处理器。该处理器链的执行发生在确定路由之前,因此即便是没有路由与请求相对应,也会调用该处理器链。该处理器链适合执行日志记录、性能指标数据采集和 HTTP 请求清理等工作。典型的应用场景包括记录所有请求的信息,类似于 Apache httpd 服务器的访问日志功能,以及添加计数器来记录访问相关的性能指标。
Copy // dedupe slashes in URL before routing
server.pre(restify.plugins.dedupeSlashes());
通用的操作链:server.use()
通过 restify 服务器的 use()方法可以注册处理器到 use 处理器链中。该处理器链在选中了一个路由来处理请求之后被调用,但是发生在实际的路由处理逻辑之前。对于所有的路由,该处理器链中的处理器都会执行。该处理器链适合执行用户认证、应用相关的请求清理和响应格式化等工作。典型的应用场景包括检查用户是否完成认证,对请求和响应的 HTTP 头进行修改等。
Copy server.use(function(req, res, next) {
console.warn('run for all routes!');
return next();
});
使用 next()
Copy server.use([
function(req, res, next) {
if (someCondition) {
res.send('done!');
return next(false);
}
return next();
},
function(req, res, next) {
// if someCondition is true, this handler is never executed
}
]);
在每个处理器的实现中,应该在合适的时机调用 next()方法来把处理流程转交给处理器链中的下一个处理器。具体的时机由每个处理器实现根据需要来决定。这给处理 HTTP 响应带来了极大的灵活性,也使得处理器可以被有效复用。每个处理器的实现逻辑也变得更加简单,只需要专注于完成所设计应有的功能就可以了。在处理完成之后,调用 next()方法即可。在某些情况下,可能不需要由处理器链中的后续处理器来继续进行处理,比如 HTTP 请求的格式是非法的。这个时候可以通过 next(false)来直接终止整个处理器链。在调用 next()方法的时候,也可以传入一个 Error 对象,使得 restify 直接把错误信息发送给客户端并终止处理链。在这种情况下,HTTP 响应的状态码由 Error 对象的属性 statusCode 来确定,默认使用 500。调用 next.ifError(err)并传入一个 Error 对象可以使得 restify 抛出异常并终止进程,可以用来在出现无法恢复的错误时终止程序。
Copy server.use(function(req, res, next) {
return next(new Error('boom!'));
});
如果要发送一个404的响应,使用NotFoundError 并传入构造函数值 ,返回一个statusCode为404.
Copy server.use(function(req, res, next) {
return next(new NotFoundError('not here!'));
});
使用Error对象调用res.send()会产生类似的结果,这个片段发送带有序列化错误的http 500 :
Copy server.use(function(req, res, next) {
res.send(new Error('boom!'));
return next();
});
在以下示例 中,通过 pre()方法注册的处理器会记录请求的完整路径。第一个 use()方法注册了 restify 的插件 queryParser,其作用是把请求的查询字符串解析成 JavaScript 对象。第二个 use()方法注册的处理器把请求的 HTTP 头 Accept 设置为 application/json,也就是 API 只接受 JSON 格式的请求。最后通过 get()方法注册了两个对于 GET 请求的处理器,第一个设置了响应的额外 HTTP 头 X-Test,第二个设置响应的内容。当请求包含了查询参数 boom 时,服务器会直接返回 500 错误。
Copy const restify = require('restify');
const server = restify.createServer();
server.pre((req, res, next) => {
console.log('req: %s', req.href());
return next();
});
server.use(restify.plugins.queryParser());
server.use((req, res, next) => {
req.headers.accept = 'application/json';
return next();
});
server.get('/handler_chain', [(req, res, next) => {
res.header('X-Test', 'test');
return next();
}, (req, res, next) => {
if (req.query.boom) {
return next(new Error('boom!'));
}
res.send({
msg: 'handled!'
});
return next();
}]);
server.listen(8000, () => console.log('%s listening at %s', server.name, server.url));
注解:优先级 pre > use > get
路由
在“basic”模式下,restify路由与express/sinatra几乎相同,因为HTTP谓词与参数化资源一起使用,以确定要运行哪些处理程序链。与命名占位符相关的值在req.params
中可用。在传递给您之前,这些值将被URL解码。
Copy function send(req, res, next) {
res.send('hello ' + req.params.name);
return next();
}
server.post('/hello', function create(req, res, next) {
res.send(201, Math.random().toString(36).substr(3, 8));
return next();
});
server.put('/hello', send);
server.get('/hello/:name', send);
server.head('/hello/:name', send);
server.del('hello/:name', function rm(req, res, next) {
res.send(204);
return next();
});
Copy server.get(/^\/([a-zA-Z0-9_\.~-]+)\/(.*)/, function(req, res, next) {
console.log(req.params[0]);
console.log(req.params[1]);
res.send(200);
return next();
});
请求类似于:
Copy $ curl localhost:8080/foo/my/cats/name/is/gandalf
将得到的结果:req.params[0]
得到foo
和req.params[1]
得到my/cats/name/is/gandalf
。
路由可以由以下任何HTTP动词指定:del
,get
,head
,opts
,post
,put
, 和patch。
Copy server.get(
'/foo/:id',
function(req, res, next) {
console.log('Authenticate');
return next();
},
function(req, res, next) {
res.send(200);
return next();
}
);
多媒体
如果用字符串(不是正则表达式)定义参数化路由,则可以从服务器中的其他地方呈现它。这有助于将HTTP响应链接到其他资源,而不必在整个代码库中硬编码URL。路径和查询字符串参数都可以适当地进行URL编码。
Copy server.get({name: 'city', path: '/cities/:slug'}, /* ... */);
// in another route
res.send({
country: 'Australia',
// render a URL by specifying the route name and parameters
capital: server.router.render('city', {slug: 'canberra'}, {details: true})
});
返回:
Copy {
"country": "Australia",
"capital": "/cities/canberra?details=true"
}
多版本路由
Copy var restify = require('restify');
var server = restify.createServer();
function sendV1(req, res, next) {
res.send('hello: ' + req.params.name);
return next();
}
function sendV2(req, res, next) {
res.send({ hello: req.params.name });
return next();
}
server.get('/hello/:name', restify.plugins.conditionalHandler([
{ version: '1.1.3', handler: sendV1 },
{ version: '2.0.0', handler: sendV2 }
]));
server.listen(8080);
尝试请求:
Copy $ curl -s localhost:8080/hello/mark
{"hello":"mark"}
$ curl -s -H 'accept-version: ~1' localhost:8080/hello/mark
"hello: mark"
$ curl -s -H 'accept-version: ~2' localhost:8080/hello/mark
{"hello":"mark"}
$ curl -s -H 'accept-version: ~3' localhost:8080/hello/mark | json
{
"code": "InvalidVersion",
"message": "~3 is not supported by GET /hello/mark"
}
在第一种情况下,我们没有指定一个Accept-Version
的头,所以相当于发送一个*。REST将选择匹配最高版本的路由。在第二种情况下,我们显式地请求V1,它让我们响应来自版本1处理程序函数的响应,随后我们请求V2版本并返回JSON。最后,我们要求一个不存在的版本并返回了一个错误。
你可以在创建服务器时通过一个version字段定义路由上的多版本。最后,您可以通过使用数组来支持多个版本的API:
Copy server.get('/hello/:name' restify.plugins.conditionalHandler([
{ version: ['2.0.0', '2.1.0', '2.2.0'], handler: sendV2 }
]));
在这种情况下,您可能需要知道更多信息,例如原始请求的版本字符串是什么,以及路由支持的版本数组中的匹配版本是什么。这些信可以使用以下两种方法获取:
Copy server.get('/version/test', restify.plugins.conditionalHandler([
{
version: ['2.0.0', '2.1.0', '2.2.0'],
handler: function (req, res, next) {
res.send(200, {
requestedVersion: req.version(),
matchedVersion: req.matchedVersion()
});
return next();
}
}
]));
以下是匹配到路由返回的结果:
Copy $ curl -s -H 'accept-version: <2.2.0' localhost:8080/version/test | json
{
"requestedVersion": "<2.2.0",
"matchedVersion": "2.1.0"
}
补充示例:
Copy const restify = require('restify');
const server = restify.createServer();
server.get({
path: '/versioned',
version: '1.0.0',
}, (req, res, next) => {
res.send('V1');
return next();
});
server.get({
path: '/versioned',
version: ['2.0.0', '2.1.0', '2.2.0'],
}, (req, res, next) => {
res.send({
requestedVersion: req.version(),
matchedVersion: req.matchedVersion(),
});
return next();
});
server.listen(8000, () => console.log('%s listening at %s', server.name, server.url));
内容协商
在之前的示例中,我们都是使用 send()方法来直接发送响应内容。如果传入的是 JavaScript 对象,restify 会自动转换成 JSON 格式。这是由于 restify 内置提供了对于不同响应内容类型的格式化实现。内置支持的响应内容类型包括 application/json、text/plain 和 application/octet-stream。restify 会根据请求的 Accept 头来确定响应的内容类型。可以在创建 restify 服务器时,添加额外的响应内容类型的支持。
Copy var server = restify.createServer({
formatters: {
'application/foo': function formatFoo(req, res, body) {
if (body instanceof Error)
return body.stack;
if (Buffer.isBuffer(body))
return body.toString('base64');
return util.inspect(body);
}
}
});
如果无法确定,则默认使用 application/octet-stream。
Copy server.get('/foo', function(req, res, next) {
res.setHeader('content-type', 'text/css');
res.send('hi');
return next();
});
将要返回一个content-type的是application/octet-stream
的结果:
Copy $ curl -i localhost:3000/
HTTP/1.1 200 OK
Content-Type: application/octet-stream
Content-Length: 2
Date: Thu, 02 Jun 2016 06:50:54 GMT
Connection: keep-alive
如前所述,对JSON、文本和二进制格式的内置格式化程序进行重整。当覆盖或附加新的格式化器时,“优先级”可能会改变;为了确保设置为所需的优先级,您应该在格式化器定义上设置q-value,确保按您希望的方式进行排序:
Copy restify.createServer({
formatters: {
'application/foo; q=0.9': function formatFoo(req, res, body) {
if (body instanceof Error)
return body.stack;
if (Buffer.isBuffer(body))
return body.toString('base64');
return util.inspect(body);
}
}
});
restify 响应对象保留了一个服务相应节点的所有“原始”方法。
Copy var body = 'hello world';
res.writeHead(200, {
'Content-Length': Buffer.byteLength(body),
'Content-Type': 'text/plain'
});
res.write(body);
res.end();
错误处理
希望以相同的方式处理错误的情况是很常见的。作为一个例子,您可能希望在所有内部服务器错误上提供500页面的服务。在这种情况下,您可以为InternalServer错误事件添加侦听器,当遇到此错误时,restify将此错误事件作为next(error)
语句的一部分,将始终触发该错误事件。这为您提供了一种在服务器上处理同一类错误的方法。还可以使用泛型ReStimeError事件,该事件将捕获所有类型的错误。
发送404的示例:
Copy server.get('/hello/:foo', function(req, res, next) {
// resource not found error
var err = new restify.errors.NotFoundError('oh noes!');
return next(err);
});
server.on('NotFound', function (req, res, err, cb) {
// do not call res.send! you are now in an error context and are outside
// of the normal next chain. you can log or do metrics here, and invoke
// the callback when you're done. restify will automtically render the
// NotFoundError depending on the content-type header you have set in your
// response.
return cb();
});
自定义错误被送回客户端:
Copy server.get('/hello/:name', function(req, res, next) {
// some internal unrecoverable error
var err = new restify.errors.InternalServerError('oh noes!');
return next(err);
});
server.on('InternalServer', function (req, res, err, cb) {
// by default, restify will usually render the Error object as plaintext or
// JSON depending on content negotiation. the default text formatter and JSON
// formatter are pretty simple, they just call toString() and toJSON() on the
// object being passed to res.send, which in this case, is the error object.
// so to customize what it sent back to the client when this error occurs,
// you would implement as follows:
// for any response that is text/plain
err.toString = function toString() {
return 'an internal server error occurred!';
};
// for any response that is application/json
err.toJSON = function toJSON() {
return {
message: 'an internal server error occurred!',
code: 'boom!'
}
};
return cb();
});
server.on('restifyError', function (req, res, err, cb) {
// this listener will fire after both events above!
// `err` here is the same as the error that was passed to the above
// error handlers.
return cb();
});
下面是另一个InternalServerError
的例子,但这一次是用的一个自定义格式程序:
Copy const errs = require('restify-errors');
const server = restify.createServer({
formatters: {
'text/html': function(req, res, body) {
if (body instanceof Error) {
// body here is an instance of InternalServerError
return '<html><body>' + body.message + '</body></html>';
}
}
}
});
server.get('/', function(req, res, next) {
res.header('content-type', 'text/html');
return next(new errs.InternalServerError('oh noes!'));
});
补充:
在 REST API 的实现中,错误处理是很重要的一部分。在前面的示例中,我们使用 send()方法发送 Error 对象来使得 restify 产生错误响应。由于 HTTP 的状态码是标准的,restify 提供了一个专门的模块 restify-errors 来创建对应不同状态码的 Error 对象,可以直接在 send()方法中使用。restify 中产生的错误会作为事件来发送,可以使用 Node.js 标准的事件处理机制来进行处理。需要注意的是,只有使用 next()方法发送的错误会被作为事件来发送,使用响应对象的 send()方法发送的则不会。
在下面的代码中,路由/error/500 使用 send()发送了一个 InternalServerError 错误对象,而/error/400 和/error/404 使用 next()分别发送了 BadRequestError 和 NotFoundError 错误对象。可以使用 server.on()来添加对于 NotFound 错误的处理逻辑,但是对于 InternalServer 错误的处理逻辑不会被触发,因为该错误是通过 send()方法来发送的。
Copy const restify = require('restify');
const errors = require('restify-errors');
const server = restify.createServer();
server.get('/error/500', (req, res, next) => {
res.send(new errors.InternalServerError('boom!'));
return next();
});
server.get('/error/400', (req, res, next) => next(new errors.BadRequestError('bad request')));
server.get('/error/404', (req, res, next) => next(new errors.NotFoundError('not found')));
server.on('NotFound', (req, res, err, cb) => {
console.error('404 %s', req.href());
return cb();
});
server.on('InternalServer', (req, res, err, cb) => {
console.error('should not appear');
return cb();
});
server.listen(8000, () => console.log('%s listening at %s', server.name, server.url));