关注程序异常流

2,586 阅读8分钟

想分享这个主题很久了,前些天和好朋友聊天的时候也提到过,我觉得新手最像新手的地方就在于新手考虑事情总是不够全面,这体现在很多方面,比如设计上、技术选型上等等。今天我想分享给大家的是其中一个方面:异常处理。

正好前些时间在手机上看到这么一则小故事:

故事很好笑,可能大家看完都会觉得这群程序员也太蠢了,连这种状况都没有考虑到。其实在我们工作中,也常常会出现只关注于正常流程处理的开发,而忽略一些非正常的流程的情况。

其实讨论程序异常处理的话题比较大,我大概分成三个方面来说,一个是程序的容错,一个是业务上的容错,另一个是安全方面。

程序的容错

程序的容错是我们写代码过程中应该首要考虑的地方,比如当你要编写一个模块给别人使用时,如果没有考虑到一些特殊情况的输入,可能模块就无法正常使用了。

需要考虑容错其实是相当常见的场景,比如这里我写了一个好玩的“睡排序”,假设要给别人使用。

代码比较简单,大家可能也能看出里面常见的几个可能抛异常的地方。

首先是 data.whatever.inner.array 嵌套太深了,很可能读取不到。这里我们常常使用缺省参数。如 ES6 中的写法:

第二个可能有问题的地方是 done 函数的调用,done 很可能外部没有传递。这里我们常常使用短路运算的方式避免:

最后还有一个可能比较隐晦的地方,这里是对数字进行排序,所以我们应该考虑数组中每一项的类型是否合法,也就是类型校验。类型校验又是另外一个话题,这里只提一下对数字的校验方法。

大家用的比较多的应该是 isNaN 方法了,这是挂载在 window 上的方法。其实 ES6 也提供了一个 isNaN 方法,并挂载在了 Number 上,也就是 Number.isNaN。这两者的区别在于 window.isNaN 传入 undefined、非空字符串等等其实并不是 NaN 的参数时也返回 true,而 Number.isNaN 会首先判断参数是否为 number,所以我们更推荐使用 Number.isNaN。

上面是一个比较简单的例子,我们针对性地提出了三点改进。有了这些改进,我们的程序不会报错,但在我们平常的开发过程中其实有些异常是不可避免的,我们必须要针对可能出现的异常进行异常捕获,说到这里,大家可能最先想到的就是 try...catch 语句了。

try...catch

try catch 是大家用的比较多的,这里就不详细展开说了,只提一下大家可能忽略的地方,就是 try catch 其实有三种形式:

  • try...catch
  • try...finally
  • try...catch...finally

finally 块是不管异常与否都会进入的地方,我们常常在这里面做一些清理工作,在前端可能是把一些变量置为 null 避免内存泄露,后端可能是关闭数据库连接等等。

另外需要注意的是在 finally 中 return 的值将作为 try catch 语句的整体返回值,不管在 try 或者 catch 中是否已经 return 了。

Promise

Promise 的异常捕获是我在面试中比较常问的问题。Promise 是异步的,所以对其 try catch 是没有作用的。

我们一般使用 catch 方法去处理 Promise 的 reject 状态,这里需要知道的是 Promise 的 catch 方法只是一种特殊的 then 方法,catch 方法等同于调用 then 方法但把第一个参数置为 undefined。

另外提一个新手可能会犯的错误,在 then 方法中的 onResolved 回调中抛出的异常只会让返回的新 Promise 的状态置为 reject,而不会让同一 then 方法中的 onRejected 方法执行。

async/await

async/await 是比 Promise 使用起来更方便也更容易理解的语法,它让异步的执行有了同步的写法。对于 async/await 的异常捕获,我觉得只要理解两个地方就行:

  • async 函数的返回值是 Promise 对象
  • await 命令就是该 Promise 内部 then 命令的语法糖

async 函数可以当做普通函数调用,也可以使用 await 表达式调用。当作为普通函数调用时,如上所述,该函数返回一个 Promise,我们使用 Promise 的异常捕获即可:

当使用 await 表达式调用时,会使 async 函数暂停执行,等待表达式中的 Promise 解析完成后继续执行 async 函数并返回解决结果。所以我们可以使用 try catch 进行捕获:

window.onerror

其实以上说到的都算针对性的异常捕获,但在实际开发中,我们总会碰到我们没考虑到的程序异常,这里可以使用一个全局的异常捕获方法进行处理。

在浏览器里,提供了 window.onerror 事件,它会捕获程序中出现的未被捕获到的同步或异步的错误。

在事件处理函数中,能获取到错误信息、当前 URL、代码行数、列数等,非常详细。

nodejs

篇幅原因,nodejs 的异常捕获就不展开说了,在 process 上有三个事件:

  • uncaughtException 捕捉全局未捕获异常,一般用作使进程优雅退出
  • unhandledRejection 当有 Promise rejected 但没有 onRejected 函数进行处理时触发
  • rejectionHandled 当有 Promise rejected 但被 onRejected 函数进行处理时触发

业务上的容错

说完程序里的容错,我们接着说一下业务上的容错。其实在本文开头提到的例子就是一个典型的业务上没有进行容错的例子。其实业务上的容错更多的是产品需要考虑的问题,但作为开发,我们也需要去理解业务,并能敏锐地发现一些业务中可能碰到的问题,避免开发到一半需要推翻重来。

代码上抛出异常只会让你的程序不可用,说白了用户体验就不会好。但一旦业务上发生问题,那么影响的可能就是产品的业务线了。

举个例子,我们之前做了个项目,因为订单的状态太多,用户会有很多不同的操作来使订单转移到不同的状态,不同的状态又会反应出不同的操作。最后就在不同状态的切换中乱套了,导致上线之后收到了很多用户状态错误的反馈。

这跟开发往往没有什么太大的联系,而是对业务的理解。

安全

最后跟大家聊一下安全。我们说了这么多,其实侧重的都是正常用户的使用,但在实际生产环境中,我们必须还要考虑一些恶意的输入。

篇幅原因也不展开说了。。简单提一下这几种攻击。

CSRF 攻击

面试的时候我也会尝试性问一下对方在安全方面考虑的问题。一般会从跨域开始问,然后自然而然提到 jsonp,jsonp 可以让我们进行跨域请求,但其中是否会有安全问题?毕竟别人也能通过 jsonp 访问你的网站了。

另外,当用户登录了 a.com 之后,打开了一个 evil.com,在 evil.com 里向 a.com 发送请求,是会携带 a.com 的 cookie 的,包括 jsonp 请求,这里的安全问题又要如何避免?

一般我们会使用添加 token 的方式来防止 CSRF 攻击,token 可以隐藏在表单中,或者在请求头里携带,作为一个合法请求的校验。

XSS 攻击

这个可能是前端接触比较多的攻击,一般分为反射型的 XSS 攻击和存储型的 XSS 攻击。区别在于存储型的 XSS 攻击会使恶意代码存放在服务端,导致所有能看到这段代码的用户都受影响。

XSS 攻击的诱导常见于一些恶意链接,当用于访问一些奇怪的网址(比如邮箱里的垃圾邮件、广告链接),可能会跳转到具有 XSS 漏洞的网站,从而引发安全问题。

一般我们需要考虑对用户的输入进行合法校验,包括前端和后端。

SQL 注入

SQL 注入是后端同学需要着重考虑的问题,对于一些小白代码,特别常见于一些学校的管理后台,很可能有 SQL 注入的漏洞,举个最简单的例子:

connection.query(`SELECT username from user where username = "${username}" and password = "${password}"`, function () {
	// ...
})

如果后端对用户输入没有进行任何过滤,直接是这么校验用户登录的话,那么只要用户猜出用户名,如常见的 admin,然后使用 " or "1" = "1,就能正常登录了。

总结

说了这么多,其实今天的主题就一个,那就是希望大家在以后的开发中,一定要多考虑异常的情况,不要想当然地以为用户一定会按照正常的流程走进来。当然,这也需要产品同学考虑各种可能出现的业务情况、测试同学进行各种 case 的测试。在大家共同的努力下,才能保证整个产品不会出现大的漏洞。

如果发现以上内容有任何不正确的地方,或者想一起探讨的,欢迎在评论区留言~