NodeJS-REST-API-开发高级教程-四-

38 阅读6分钟

NodeJS REST API 开发高级教程(四)

原文:Pro REST API Development with Node.js

协议:CC BY-NC-SA 4.0

八、故障排除

就是这里。你进入了最后一章。您亲身体验了在 Node 中编写 RESTful API 的代价。你已经复习过理论了。您了解了 REST 实际代表什么,以及如何使用它来开发一个好的、有用的 API。

在这一章中,我将介绍在这个过程中可能会出错的一些事情,以及您必须考虑的一些事项,例如:

  • 异步编程。我将最后一次尝试这个主题,解释它是如何在我们的代码中使用的。
  • 关于 Swagger UI 配置的次要细节。有时文档是不够的。
  • 潜在的 CORS 问题。我将回顾一下 CORS 的基本知识,以帮助你理解如何利用它。
  • 数据类型。关于我们的代码,我将讨论的最后一个主题是如何从 JSON 模式数据类型转换到 Mongoose 类型。

异步编程

对于非 JavaScript 开发人员,甚至是非 Node.js 开发人员来说,异步编程的概念一开始可能很难理解。我说“可能”是因为这不是 JavaScript/Node.js 独有的概念;其他编程语言,如 Earlang、Python,甚至最近的 Go 都有这种能力。

也就是说,Node.js 是少数几个 web 开发人员被迫处理这个概念或者无法正确开发的环境之一。

当您开始处理外部资源时,异步编程成为任何使用 Node.js 的中型项目的必备工具,这主要是因为这意味着您将使用已经在利用这种编程技术的第三方库;所以你要么接受它,要么转换语言。

你已经介绍了这个特性是如何提高应用的性能的,你甚至看到了几个利用它的有用的设计模式,所以现在让我们讨论一下如果不能理解这个概念会如何影响你对第七章中介绍的代码的理解。

无论您是否注意到,在我们的 API 代码中,有几个地方发生了异步编程。让我们来看看其中的一些。

控制器动作的代码

每个控制器上的每个动作都有一段数据库查询形式的异步编程。这可能是最显而易见的一点,但是仔细阅读以正确理解它是很重要的。

我们不做这类事情的原因是:

var authors = lib.db.model('Author')

.find(criteria).exec()

if(!authors) return next(controller.RESTError('InternalServerError', authors))

controller.writeHAL(res, authors)

相反,我们设置了一个回调函数,就像这样:

lib.db.model('Author')

.find(criteria)

.exec(function(err, authors) {

if(err) return next(controller.RESTError('InternalServerError', err))

controller.writeHAL(res, authors)

})

这是因为,正如我已经说过的,Node.js 中的 I/O 操作是异步的,这意味着查询数据库需要像这样完成,并设置一个回调函数来处理到达的响应。Node.js 确实提供了其 I/O 功能的同步版本(比如读写文件),但它们主要是为了简化转换;不鼓励你使用它们,像 Mongoose 这样的第三方库也没有兴趣遵循这种模式。

在手动测试应用时捕捉这种类型的错误可能有点令人头疼,因为最终的行为可能并不总是相同的。当代码足够复杂时,异步函数返回响应所需的时间和代码使用该值所需的时间就成了一场竞赛。

此外,因为 Node.js 解释器不会在您错过方法/函数调用中的一些参数时抛出错误,所以您可能会像这样结束:

function libraryMethod(attr1, callback) {

asyncCall(attr1, function(response){

if(callback) callback(response)

})

}

var returnValue = libraryMethod('hello world')

前面的代码永远不会抛出错误。在你的returnValue中,你将永远得不到定义。如果您无法访问libraryMethod函数的代码,可能很难理解哪里出了问题。例如,您有这样一个代码:

var myResponseValue = ''

asyncCall('hello', function(response) {

myResponseValue = response

})

///some other code taking 30ms to execute

console.log(myResponseValue)

前面的代码显示了使用异步调用时的另一个常见错误:您正确地设置了回调,但是在回调之外使用了返回值。

在前面的例子中,如果asyncCall得到响应的时间少于 30 毫秒,那么它会工作,但是直到发生了一些事情(比如代码进入生产环境)时,您才会意识到自己的错误。突然,asyncCall执行需要 31 毫秒,现在“undefined”一直打印到控制台。但是你当然不知道为什么。解决这个问题的简单方法是在回调函数中添加任何处理响应值的代码。

中间件功能

乍看之下,这可能并不明显,但是整个中间件链都遵循着第三章 3 中提到的串行流程机制。你怎么知道?因为有next功能;当函数结束并准备将控制权交给下一个中间件时,您需要调用它。

多亏了next,函数中可以有更多的异步代码,并且仍然能够调用下一个函数。在某些地方,这并不明显,比如在设置queryParserbodyParser中间件时:

server.use(restify.queryParser())

server.use(restify.bodyParser())

但是这些方法实际上是返回一个新函数,该函数又接收三个神奇的参数:请求对象、响应对象和下一个函数。

创建定制中间件时的一个常见问题是忘记在代码的一个可能的执行分支中调用next函数(如果您碰巧拥有它们的话)。这种情况的症状是您的 API 似乎挂起了,您从未从服务器得到响应,并且您在控制台上看不到任何错误。这是因为执行流被中断了。突然它无法找到继续下去的方法。并且您没有发回响应(使用 response 对象)。这是一个棘手的问题,因为没有任何错误消息来清楚地说明问题。

function middleware(req, res, next){

if(req.params.q == '1') {

next()

} else {

if(req.params.q2 == '1') {

next()

}

}

//if no 'q' or 'q2' parameters are sent, or if they don't have the right values, then this middleware is breaking the serial flow and no response is ever getting back to the client.

}

项目中还使用了另一种类型的中间件:Mongoose 中间件,它是可以附加到模型上的钩子,可以在一组特定动作之前或之后执行。我们的特例在clientreview模型上使用了 post save 挂钩:

modelDef.schema.post('save', function(doc, next) {

db.model('Book').update({_id: doc.book}, {$addToSet: {reviews: this.id}}, function(err) {

next(err)

})

})

这段代码清楚地显示了在中间件内部与异步调用结合使用的next函数。如果您忘记调用next,那么执行将在这个回调时被中断(并停止)。

配置 Swagger UI 的问题

设置 Swagger UI 是一项既需要修改 UI 本身又需要在后端编写一些特殊代码的任务。这不是特别容易理解,因为文档阅读起来并不简单。

一方面,我们使用 swagger-node-restify 模块来生成 UI 所需的后端端点;这通过以下几行实现:

swagger.addModels(lib.schemas)   swagger.setAppHandler(server)   lib.helpers.setupRoutes(server, swagger, lib)   swagger.configureSwaggerPaths("", "/api-docs", "")   swagger.configure(' http://localhost:9000 ', '0.1')  

第 1 行设置了模型,以便当端点将它们指定为响应类时,Swagger 可以返回它们。第 2 行基本上是告诉模块我们使用哪个 web 服务器来获取文档。我们可能会配置两个不同的服务器:一个用于文档,一个用于实际的 API。

第 3 行实际上是我们的一个,但是它需要 Swagger,因为我们调用它提供的addGETaddPOSTaddDELETEaddPUT方法(这是由BaseController代码在其setUpActions方法中完成的)。

第 4 行没有说太多,但是它很有用,原因有几个:

  • 最明显的一点是我们正在为文档设置路径:/api-docs
  • 我们还说,我们不想通过扩展(例如,.json)来指定格式。默认情况下,我们需要在路径中定义一个由.json自动替换的{format}部分。有了这一行,我们就不再需要它,并简化了路径格式。

最后,第 5 行设置了整个文档 API 的基本 URL。

第七章,前端代码不得不改;我提到了具体位置。显然需要取消 API 键代码的注释和主机 URL 的更改,但是不需要资源路径的更改。由于我们在初始化阶段配置静态路径的方式,我们需要对此进行更改。

server.get(/^\/swagger-ui(\/.*)?/, restify.serveStatic({

directory: __dirname + '/',

default: 'index.html'

}))

前面的代码确保只有swagger-ui文件夹下的任何内容作为静态内容(基本上是 Swagger UI 需要的所有内容),但是 HTML 文件中的默认路径指向根文件夹,这在我们的例子中不够好。

CORS:又名跨原产地资源共享

任何从事过一段时间的 web 开发人员都见过这个可怕的错误消息:

XMLHttpRequest cannot loadhttp://domain.example. Originhttp://domain1.exampleis not allowed by Access-Control-Allow-Origin

对于在公共 API 的 web 客户端上工作的开发人员,浏览器检查跨源资源共享(CORS)以确保请求是安全的,这意味着浏览器检查了请求的端点,因为它没有找到任何 CORS 标头,或者标头没有指定我们的域为有效,所以它出于安全原因取消了请求。

对于 API 设计者来说,这是一个非常相关的错误,因为需要考虑 CORS,要么手动允许它,要么拒绝它。如果您正在设计一个公共 API,您需要确保在响应头中指定任何域都可以发出请求。这是所有可能设置中最宽松的。另一方面,如果您正在定义一个私有 API,那么 CORS 头有助于定义唯一可以实际请求任何类型的端点资源的域。

通常,web 客户端会对每个 CORS 请求执行一系列步骤:

First, the client will ask the API server if the desired request is possible (Can the client query the wanted resource using the needed method from the current origin?). This is done by sending a “pre-flight”1 request with the Access-Control-Request-Header header (with the headers the client needs to access) and the Access-Control-Request-Method header (with the method needed).   Then the server will answer with what is authorized, using these headers: Access-Control-Allow-Origin with the allowed origin (or * for anything), Access-Control-Allowed-Methods with the valid methods, and Access-Control-Allow-Headers with a list of valid headers to be sent.   Finally, the client can do the “normal” request.  

如果在飞行前请求过程中有任何东西验证失败(请求的方法或需要的头),那么响应将不是 200 OK 响应。

对于我们的例子,根据第七章中的代码,我们将采用公共 API 方法,因为我们允许任何域使用以下代码向我们的端点发出请求:

restify.defaultResponseHeaders = function(data) {

this.header('Access-Control-Allow-Origin', '*')

}

数据类型

尽管我们没有在整个 API 的 JavaScript 代码中直接处理和指定变量的类型,但是有两个非常特殊的地方需要数据类型:为我们的资源定义的 JSON 模式和定义的 Mongoose 模型。

现在,由于getModelFromSchema函数和translateTypeToJs函数中的代码,您可以从 JSON 模式类型转换到 Mongoose 类型,因为我们的模式中定义的大多数基本类型几乎都可以直接转换成 JavaScript 类型。

对于更复杂的类型,比如数组,由于整个定义是不同的,需要添加额外的代码,这就是getModelFromSchema代码的用武之地。

从第七章中的代码转换而来的类型仅限于当时需要的,但是你可以很容易地扩展它来实现更多的功能,比如让required属性同时为模式验证器和 Mongoose 验证器工作(这些确保你不会保存任何无效的东西)。让我们快速看一下如何添加对required属性的支持。

一个对象类型由一系列属性组成,但也包括一系列必需的属性,这些属性定义在与properties属性相同的级别:

module.exports = {

"id": "Author",

"properties": {

"name": {

"type": "string",

"description": "The full name of the author"

},

"description": {

"type": "string",

"description": "A small bio of the author"

},

"books": {

"type": "array",

"description": "The list of books published on at least one of the stores by this author",

"items": {

"$ref": "Book"

}

},

"website": {

"type": "string",

"description": "The Website url of the author"

},

"avatar": {

"type": "string",

"description": "The url for the avatar of this author"

},

"address": {

"type": "object",

"properties": {

"street": {

"type": "string"

},

"house_number": {

"type": "integer"

}

}

}

},

"required": ["name", "website"]

}

要获得这个新属性的内容,只需在getModelFromSchema函数中添加几行代码,简单地检查属性名;如果它在所需的数组中,您可以根据需要设置它:

function getModelFromSchema(schema) {

var data = {

name: schema.id,

schema: {}

}

var newSchema = {}

var tmp = null

var requiredProperties = schema.required

_.each(schema.properties, function(v, propName) {

if( requiredProperties``&&

v.required = true

}

if(v['$ref'] != null) {

tmp = {

type: Schema.ObjectId,

ref: v['$ref']

}

} else {

tmp = translateComplexType(v)

}

newSchema[propName] = tmp

})

data.schema = new Schema(newSchema)

return data

摘要

就是这里。你做到了。你设法看完了整本书!你已经从 REST 的基础学到了一个成熟的 RESTful API,最后,在这一章中,你学习了在开发过程中会引起麻烦的主要东西,比如异步编程,配置 Swagger UI,CORS,以及从 JSON 模式类型转移到 Mongoose 类型。

感谢您的阅读,希望您能喜欢这本书。

Footnotes 1

期权申请。