使用JavaScript处理意外数据





动态类型语言的主要问题之一是,您不能始终保证数据流正确,因为您不能强制将参数或变量设置为非null的值。在这种情况下,我们倾向于使用简单的代码:



function foo (mustExist) {
  if (!mustExist) throw new Error('Parameter cannot be null')
  return ...
}


这种方法的问题是代码污染,因为您必须在所有位置测试变量,并且无法保证所有开发人员都会始终运行该测试,尤其是在变量或参数不能为null的情况下。通常,我们甚至都不知道该参数的值可以是undefined或null-在不同的专家处理客户端和服务器部分时,即在大多数情况下,通常会发生这种情况。



为了稍微优化这种情况,我开始寻找如何以及采用哪种策略来最大程度地减少意外因素。那是我见到Eric Elliott的一篇很棒的文章的时候... 这项工作的目的不是要完全驳斥他的文章,而是要添加一些有趣的信息,这些信息由于我在JavaScript开发领域的经验而能够随着时间的推移而发现。



在开始之前,我想仔细阅读本文中介绍的一些要点,并表达我对服务器组件开发人员的看法,因为另一篇文章更面向客户。



一切如何开始



数据处理问题可能是由于多种因素引起的。当然,主要原因是用户输入。但是,除了另一篇文章中提到的那些以外,还有其他格式错误的数据源:



  • 数据库记录
  • 隐式返回空数据的函数
  • 外部API


在所考虑的所有情况下,都将采用不同的解决方案,稍后我们将详细分析它们中的每一个,并牢记它们都不是万能药。大多数问题是由人为错误引起的:在许多情况下,语言准备使用null或未定义的数据(null或未定义),但是在转换此数据的过程中,可能会失去处理数据的能力。



用户输入的数据



在这种情况下,我们机会很少。如果问题出在用户输入上,则可以通过所谓的水合解决(换句话说,我们必须获取用户发送给我们的原始输入(例如,作为API有效负载的一部分))并将其转换为我们可以毫无错误地工作)。



在服务器端,当使用Web服务器(例如Express)时,我们可以使用标准工具(例如JSON模式 或Joi)在客户端通过用户输入执行所有操作



下面是使用Express或AJV可以完成的操作的示例:



const Ajv = require('ajv')
const Express = require('express')
const bodyParser = require('body-parser')
 
const app = Express()
const ajv = new Ajv()
 
app.use(bodyParser.json())
 
app.get('/foo', (req, res) => {
  const schema = {
    type: 'object',
    properties: {
      name: { type: 'string' },
      password: { type: 'string' },
      email: { type: 'string', format: 'email' }
    },
    additionalProperties: false
    required: ['name', 'password', 'email']
  }
 
  const valid = ajv.validate(schema, req.body)
    if (!valid) return res.status(422).json(ajv.errors)
    // ...
})
 
app.listen(3000)


看:我们正在检查路线的主要部分。默认情况下,这是我们从body-parser包中获得的对象,作为负载的一部分。在这种情况下,我们将其通过JSON模式传递,因此如果这些属性之一具有不同的类型或格式(对于电子邮件),则将对其进行验证。



重要!请注意,我们将为未处理的对象返回HTTP 422 。许多人将查询错误(例如无效的正文或查询字符串)解释为错误400 无效的查询 -这部分是正确的,但是在这种情况下,问题不在于请求本身,而在于用户随其发送的数据。因此,对用户的最佳响应将是错误422:这意味着该请求是正确的,但由于其内容不是预期的格式,因此无法进行处理。



另一个选择(使用AJV除外)是使用我用Roz创建的库。我们称它为Expresso,它是一组库,使使用Express开发API变得容易一些。这样的工具之一就是 @ expresso / validator,它基本上可以完成我们上面演示的内容,但是可以作为中间件来使用。



具有默认值的其他参数



除了之前检查过的内容之外,我们还发现了在未在可选字段中发送空值的情况下,可以将空值传递给我们的应用程序的可能性。例如,想象一下,我们有一个分页路由,该分页路由使用两个参数page和size作为查询字符串。但是,它们是可选的,如果未收到,则应默认为默认。



理想情况下,我们的控制器应具有执行以下操作的函数:



function searchSomething (filter, page = 1, size = 10) {
  // ...
}


注意。就像我们响应分页请求而返回的422错误一样,返回正确的错误代码206不完整的内容也很重要,每当我们响应请求(返回的数据量是整体的一部分)时,我们都会返回206.当用户到达最后一页并且没有更多数据时,我们可以返回代码200,而当用户尝试查找总页面范围之外的页面时,我们返回代码204 No content



当我们得到两个空值时,这将解决问题,但这通常是JavaScript中一个非常有争议的方面。可选参数仅在值为空时才采用默认值,但是此规则不适用于null值,因此,如果执行以下操作:



function foo (a = 10) {
  console.log(a)
}
 
foo(undefined) // 10
foo(20) // 20
foo(null) // null


并且我们需要将该信息视为null,因此我们不能仅依赖于可选参数。因此,在这种情况下,我们有两种方法:



1.在控制器中使用If语句



function searchSomething (filter, page = 1, size = 10) {
  if (!page) page = 1
  if (!size) size = 10
  // ...
}


看起来不太好,很不方便。



2. 直接在路由上使用JSON模式



再次,我们可以使用AJV或@ expresso / validator验证此数据:



app.get('/foo', (req, res) => {
  const schema = {
    type: 'object',
    properties: {
      page: { type: 'number', default: 1 },
      size: { type: 'number', default: 10 },
    },
    additionalProperties: false
  }
 
<a href=""></a>  const valid = ajv.validate(schema, req.params)
    if (!valid) return res.status(422).json(ajv.errors)
    // ...
})


使用空值和不确定值



我个人不满意在JavaScript中同时使用null和undefined来证明值是空的想法,原因有很多。除了将这些概念带入抽象层次的困难外,还不应忘记可选参数。如果您仍然对这些概念有疑问,让我从实践中为您提供一个很好的例子:







现在我们了解了定义,可以说2020年JavaScript中将有两个主要功能:合并运算符可选链接。因为我已经写了一篇有关此的文章,所以我现在不再赘述  (它是葡萄牙语),但是请注意,这两个创新将极大地简化我们的任务,因为我们可以使用适当的运算符(??)来专注于null和undefined这两个概念,而不是使用逻辑负数(例如!那是犯错的沃土。



隐式返回null的函数



由于其隐式性质,此问题很难解决。某些功能在假定将始终提供数据的情况下处理数据,但在某些情况下并非如此。让我们考虑一个标准的例子:



function foo (num) {
  return 23*num
}


如果num为null,则此函数的结果将为0,这不是预期的。在这种情况下,我们别无选择,只能测试代码。可以执行两种类型的测试。第一种是使用简单的if语句:



function foo (num) {
  if (!num) throw new Error('Error')
  return 23*num
}


第二种方法是使用Either monad ,在我提到的文章中有详细介绍。这是处理模棱两可的数据(即可能为空或不为空的数据)的好方法。这是因为JavaScript已经具有一个内置函数,该函数支持两种操作流Promise:



function exists (value) {
  return x != null ? Promise.resolve(value) : Promise.reject(`Invalid value: ${value}`)
}
 
async function foo (num) {
  return exists(num).then(v => 23 * v)
}


这是将catch语句从存在委托给名为foo的函数的方法:



function init (n) {
  foo(n)
    .then(console.log)
    .catch(console.error)
}
 
init(12) // 276
init(null) // Invalid value: null


外部API和数据库记录



这是一个非常常见的情况,尤其是当某些系统是根据先前创建或填充的数据库开发的。例如,一个新产品使用与其成功的前身相同的数据库,从而集成了不同系统的用户,依此类推。



最大的问题不是数据库是未知的-实际上,这就是原因,因为我们不知道在数据库级别执行了什么操作,并且我们无法确认是否将接收值为null或undefined的数据。 ...当数据库没有正确记录时,我们不得不说质量差的记录,并且我们面临与以前相同的问题。



实际上,我们在这里无能为力,我个人更喜欢检查数据状态以确保可以使用它。但是,您无法验证所有数据,因为许多返回的对象可能只是太大了。因此,在执行任何操作之前,建议先检查函数操作所涉及的数据,例如映射或过滤器,以确保其是否未定义。



产生错误



 对数据库和外部API 使用断言函数是一种好习惯。本质上,这些函数返回数据(如果有),否则将产生错误。这种类型的函数最常见的用例是当我们有一个API时,例如通过标识符(众所周知的findById)搜索特定的数据类型:



async function findById (id) {
  if (!id) throw new InvalidIDError(id)
 
  const result = await entityRepository.findById(id)
  if (!result) throw new EntityNotFoundError(id)
  return result
}


将Entity替换为您的实体名称,例如UserNotFoundError。



很好,因为我们可以在同一个控制器中具有通过ID查找用户的功能,以及使用该用户查找其他数据的另一个功能,例如该用户在另一个数据库集合中的配置文件。在调用配置文件查找功能时,我们使用一个断言来确保用户确实存在于我们的数据库中。否则,该功能甚至不会执行,您可以直接在路线上搜索错误:



async function findUser (id) {
  if (!id) throw new InvalidIDError(id)
 
  const result = await userRepository.findById(id)
  if (!result) throw new UserNotFoundError(id)
  return result
}
 
async function findUserProfiles (userId) {
  const user = await findUser(userId)
 
  const profile = await profileRepository.findById(user.profileId)
  if (!profile) throw new ProfileNotFoundError(user.profileId)
  return profile
}


请注意,如果用户不存在,我们将不会进行数据库调用,因为第一个功能可确保用户存在。现在我们可以在路由中执行以下操作:



app.get('/users/{id}/profiles', handler)
 
// --- //
 
async function handler (req, res) {
  try {
    const userId = req.params.id
    const profile = await userService.getProfile(userId)
    return res.status(200).json(profile)
  } catch (e) {
    if (e instanceof UserNotFoundError || e instanceof ProfileNotFoundError) return res.status(404).json(e.message)
    if (e instanceof InvalidIDError) return res.status(400).json(e.message)
  }
}


我们可以通过简单地检查现有错误类的实例名称来找出返回的错误类型。



结论



有多种处理数据的方法,以确保信息的连续和可预测。您还知道其他提示吗?离开他们的意见。



像材料做的?!想提出建议,发表意见或只是打个招呼?在社交媒体上找到我的方法如下:








本文最初Lucas Santos发表在dev.to上如果您对本文的主题有任何疑问或意见,请将其张贴在dev.to上的原始文章下



All Articles