像老板一样处理 Axios 和错误 😎
介绍
我非常喜欢这种“问题/解决方案”的方法。我们先看到一些问题,然后找到一个非常好的解决方案。但就这次演讲而言,我觉得我们也需要一些介绍。
开发 Web 应用程序时,通常需要将前端和后端分离。为此,你需要一些东西来让它们之间进行沟通。
例如,你可以使用原生 HTML、CSS 和 JavaScript 构建前端(通常称为 GUI 或用户界面),或者,通常使用Vue、React 等各种框架,以及网上其他许多可用的框架。我选择 Vue 是因为这是我的个人偏好。
为什么?我对其他框架的研究确实不深,所以不能保证 Vue 就是最好的,但我喜欢它的工作方式、语法等等。这就像你喜欢一个人一样,是个人选择。
但是,除此之外,您使用的任何框架,您都将面临同样的问题:_如何与后端通信_(可以用很多种语言编写,我不敢提及一些。我现在迷恋的?Python 和Flask)。
一种解决方案是使用AJAX(什么是 AJAX?异步 JavaScript 和 XML)。您可以直接使用XMLHttpRequest向后端发出请求并获取所需数据,但缺点是代码冗长。您可以使用Fetch API,它会在 之上进行抽象XMLHttpRequest
,并提供一套强大的工具。另一个重大改进是它将Fetch API
使用Promises,从而避免回调XMLHttpRequest
(避免回调地狱)。
另外,我们有一个名为Axios的很棒的库,它具有不错的 API(出于好奇心,它在底层使用XMLHttpRequest
,提供非常广泛的浏览器支持)。Axios API 将 包装成XMLHttpRequest
,Promises
不同于Fetch API
。除此之外,现在Fetch API
可用的浏览器引擎都很好地支持 ,并且为旧版浏览器提供了 polyfill。我不会讨论哪一个更好,因为我真的认为这是个人喜好,就像任何其他库或框架一样。如果您没有意见,我建议您寻找一些比较并深入研究的文章。有一篇由Faraz Kelhini撰写的很棒的文章,我会向您推荐。
我个人的选择是Axios
因为它拥有良好的 API,响应超时、自动 JSON 转换和拦截器(我们将在提案解决方案中使用它们)等等。没有什么是无法实现的Fetch API
,只是有其他方法。
问题
说到Axios
,可以使用以下代码行发出一个简单的 GET HTTP 请求:
import axios from 'axios'
//here we have an generic interface with basic structure of a api response:
interface HttpResponse<T> {
data: T[]
}
// the user interface, that represents a user in the system
interface User {
id: number
email: string
name: string
}
//the http call to Axios
axios.get<HttpResponse<User>>('/users').then((response) => {
const userList = response.data
console.log(userList)
})
我们使用了 Typescript(接口和泛型)、ES6 模块、Promises、Axios 和箭头函数。我们不会深入讲解它们,并假设你已经了解它们。
因此,在上面的代码中,如果一切顺利,也就是说:服务器在线,网络运行正常,那么当你运行这段代码时,你将在控制台上看到用户列表。然而,现实生活并不总是完美的。
我们开发人员有一个使命:
让用户的生活变得简单!
因此,当出现问题时,我们需要尽一切努力自己解决问题,甚至在用户不知情的情况下,当没有其他办法时,我们有义务向他们显示一条非常好的消息,解释出了什么问题,以减轻他们的负担。
Axios
likeFetch API
用于Promises
处理异步调用,避免我们之前提到的回调。Promises
是一个非常好的API,而且理解起来也不难。我们可以将操作 ( then
) 和错误处理程序 ( ) 串联起来,API 会按顺序调用它们。如果Promisecatch
中发生错误,则会查找并执行最近的错误。catch
因此,上面带有基本错误处理程序的代码将变成:
import axios from 'axios'
//..here go the types, equal above sample.
//here we call axios and passes generic get with HttpResponse<User>.
axios
.get<HttpResponse<User>>('/users')
.then((response) => {
const userList = response.data
console.log(userList)
})
.catch((error) => {
//try to fix the error or
//notify the users about somenthing went wrong
console.log(error.message)
})
好的,那么问题是什么呢?嗯,我们有上百个错误,每次 API 调用,解决方案/消息都一样。出于好奇,Axios 给我们展示了一个小列表:ERR_FR_TOO_MANY_REDIRECTS, ERR_BAD_OPTION_VALUE, ERR_BAD_OPTION, ERR_NETWORK, ERR_DEPRECATED, ERR_BAD_RESPONSE, ERR_BAD_REQUEST, ERR_CANCELED, ECONNABORTED, ETIMEDOUT
。我们有HTTP 状态码,其中发现了很多错误,比如404
(页面未找到)等等。你懂的。我们遇到的常见错误太多了,以至于无法在每个 API 请求中优雅地处理它们。
非常丑陋的解决方案
我们能想到的一个非常丑陋的解决方案是编写一个庞大的函数,每次发现新错误时都会递增。尽管这种方法丑陋,但如果您和您的团队记得在每次 API 请求中调用该函数,它还是可行的。
function httpErrorHandler(error) {
if (error === null) throw new Error('Unrecoverable error!! Error is null!')
if (axios.isAxiosError(error)) {
//here we have a type guard check, error inside this if will be treated as AxiosError
const response = error?.response
const request = error?.request
const config = error?.config //here we have access the config used to make the api call (we can make a retry using this conf)
if (error.code === 'ERR_NETWORK') {
console.log('connection problems..')
} else if (error.code === 'ERR_CANCELED') {
console.log('connection canceled..')
}
if (response) {
//The request was made and the server responded with a status code that falls out of the range of 2xx the http status code mentioned above
const statusCode = response?.status
if (statusCode === 404) {
console.log('The requested resource does not exist or has been deleted')
} else if (statusCode === 401) {
console.log('Please login to access this resource')
//redirect user to login
}
} else if (request) {
//The request was made but no response was received, `error.request` is an instance of XMLHttpRequest in the browser and an instance of http.ClientRequest in Node.js
}
}
//Something happened in setting up the request and triggered an Error
console.log(error.message)
}
有了我们的神奇函数,我们可以像这样使用它:
import axios from 'axios'
axios
.get('/users')
.then((response) => {
const userList = response.data
console.log(userList)
})
.catch(httpErrorHandler)
我们必须记住在每个 API 调用中添加这一点,并且,对于我们可以优雅处理的每个新错误,我们都需要用更多的代码和丑陋catch
来增加我们的讨厌之处。httpErrorHandler
if's
除了丑陋和缺乏可维护性之外,这种方法的另一个问题是,如果在一个 API 调用中,我希望处理与全局方法不同的方法,但我无法做到。
随着问题的累积,函数会呈指数级增长。这个解决方案无法正确扩展!
优雅且值得推荐的解决方案
当我们团队合作时,让他们记住每个软件的精妙之处非常困难。团队成员来来去去,我不知道有哪份文档足够好,能够解决这个问题。
另一方面,如果代码本身能够以通用的方式处理这些问题,那就去做吧!开发人员什么都不用做,就不会犯错!
在我们深入研究代码之前(这是我们对本文的期望),我需要讲一些内容来让您了解代码的作用。
Axios 允许我们使用一个叫做 axios 的函数Interceptors
,它会在每个请求中执行。这是一种非常棒的方法,可以用来检查权限、添加一些需要存在的 header(例如 token)以及预处理响应,从而减少样板代码的数量。
我们有两种类型Interceptors
。AJAX调用之前(请求)和之后(响应) 。
它的使用非常简单:
//Intercept before request is made, usually used to add some header, like an auth
const axiosDefaults = {}
const http = axios.create(axiosDefaults)
//register interceptor like this
http.interceptors.request.use(
function (config) {
// Do something before request is sent
const token = window.localStorage.getItem('token') //do not store token on localstorage!!!
config.headers.Authorization = token
return config
},
function (error) {
// Do something with request error
return Promise.reject(error)
}
)
但是,在本文中,我们将使用响应拦截器,因为我们需要在这里处理错误。您可以扩展该解决方案以处理请求错误。
响应拦截器的一个简单用法是调用我们的大而丑陋的函数来处理所有类型的错误。
与所有形式的自动处理程序一样,我们需要一种在需要时绕过(禁用)此操作的方法。我们将扩展AxiosRequestConfig
接口并添加两个可选选项raw
和silent
。如果raw
设置为true
,我们将不执行任何操作。silent
在处理全局错误时,是否可以静音显示的通知?
declare module 'axios' {
export interface AxiosRequestConfig {
raw?: boolean
silent?: boolean
}
}
下一步是创建一个Error
类,每次我们想要通知错误处理程序假设问题时,我们都会抛出这个类。
export class HttpError extends Error {
constructor(message?: string) {
super(message) // 'Error' breaks prototype chain here
this.name = 'HttpError'
Object.setPrototypeOf(this, new.target.prototype) // restore prototype chain
}
}
现在,让我们编写拦截器:
// this interceptor is used to handle all success ajax request
// we use this to check if status code is 200 (success), if not, we throw an HttpError
// to our error handler take place.
function responseHandler(response: AxiosResponse<any>) {
const config = response?.config
if (config.raw) {
return response
}
if (response.status == 200) {
const data = response?.data
if (!data) {
throw new HttpError('API Error. No data!')
}
return data
}
throw new HttpError('API Error! Invalid status code!')
}
function responseErrorHandler(response) {
const config = response?.config
if (config.raw) {
return response
}
// the code of this function was written in above section.
return httpErrorHandler(response)
}
//Intercept after response, usually to deal with result data or handle ajax call errors
const axiosDefaults = {}
const http = axios.create(axiosDefaults)
//register interceptor like this
http.interceptors.response.use(responseHandler, responseErrorHandler)
好吧,我们不需要每次 Ajax 调用时都记住这个神奇的函数。而且,我们可以随时禁用它,只需将它传递raw
给请求配置即可。
import axios from 'axios'
// automagically handle error
axios
.get('/users')
.then((response) => {
const userList = response.data
console.log(userList)
})
//.catch(httpErrorHandler) this is not needed anymore
// to disable this automatic error handler, pass raw
axios
.get('/users', {raw: true})
.then((response) => {
const userList = response.data
console.log(userList)
}).catch(() {
console.log("Manually handle error")
})
好吧,这的确是个不错的解决方案,但是,这个丑陋的函数会变得越来越庞大,以至于我们看不到尽头。它会变得非常庞大,以至于任何人都不想维护它。
我们还能再进步吗?哦,是的。
改进的优雅解决方案
我们将Registry
使用注册表设计模式开发一个类。该类允许您通过一个键(稍后我们将深入探讨)和一个操作(可以是字符串(消息)、一个对象(可以执行一些恶意操作)或一个函数)来注册错误处理,当错误与键匹配时将执行该操作。该注册表将具有父级,您可以将其放置在其中,以便您覆盖键以自定义处理方案。
以下是我们将在代码中使用的一些类型:
// this interface is the default response data from ours api
interface HttpData {
code: string
description?: string
status: number
}
// this is all errrors allowed to receive
type THttpError = Error | AxiosError | null
// object that can be passed to our registy
interface ErrorHandlerObject {
after?(error?: THttpError, options?: ErrorHandlerObject): void
before?(error?: THttpError, options?: ErrorHandlerObject): void
message?: string
notify?: QNotifyOptions
}
//signature of error function that can be passed to ours registry
type ErrorHandlerFunction = (error?: THttpError) => ErrorHandlerObject | boolean | undefined
//type that our registry accepts
type ErrorHandler = ErrorHandlerFunction | ErrorHandlerObject | string
//interface for register many handlers once (object where key will be presented as search key for error handling
interface ErrorHandlerMany {
[key: string]: ErrorHandler
}
// type guard to identify that is an ErrorHandlerObject
function isErrorHandlerObject(value: any): value is ErrorHandlerObject {
if (typeof value === 'object') {
return ['message', 'after', 'before', 'notify'].some((k) => k in value)
}
return false
}
好了,类型定义好了,我们来看看类的实现。我们将使用一个 Map 来存储对象/键和一个父类,如果在当前类中找不到对应的键,我们将查找父类。如果父类为 null,则搜索结束。在构造函数中,我们可以传递一个父类,以及可选的 实例ErrorHandlerMany
,来注册一些处理程序。
class ErrorHandlerRegistry {
private handlers = new Map<string, ErrorHandler>()
private parent: ErrorHandlerRegistry | null = null
constructor(parent: ErrorHandlerRegistry = undefined, input?: ErrorHandlerMany) {
if (typeof parent !== 'undefined') this.parent = parent
if (typeof input !== 'undefined') this.registerMany(input)
}
// allow to register an handler
register(key: string, handler: ErrorHandler) {
this.handlers.set(key, handler)
return this
}
// unregister a handler
unregister(key: string) {
this.handlers.delete(key)
return this
}
// search a valid handler by key
find(seek: string): ErrorHandler | undefined {
const handler = this.handlers.get(seek)
if (handler) return handler
return this.parent?.find(seek)
}
// pass an object and register all keys/value pairs as handler.
registerMany(input: ErrorHandlerMany) {
for (const [key, value] of Object.entries(input)) {
this.register(key, value)
}
return this
}
// handle error seeking for key
handleError(
this: ErrorHandlerRegistry,
seek: (string | undefined)[] | string,
error: THttpError
): boolean {
if (Array.isArray(seek)) {
return seek.some((key) => {
if (key !== undefined) return this.handleError(String(key), error)
})
}
const handler = this.find(String(seek))
if (!handler) {
return false
} else if (typeof handler === 'string') {
return this.handleErrorObject(error, { message: handler })
} else if (typeof handler === 'function') {
const result = handler(error)
if (isErrorHandlerObject(result)) return this.handleErrorObject(error, result)
return !!result
} else if (isErrorHandlerObject(handler)) {
return this.handleErrorObject(error, handler)
}
return false
}
// if the error is an ErrorHandlerObject, handle here
handleErrorObject(error: THttpError, options: ErrorHandlerObject = {}) {
options?.before?.(error, options)
showToastError(options.message ?? 'Unknown Error!!', options, 'error')
return true
}
// this is the function that will be registered in interceptor.
resposeErrorHandler(this: ErrorHandlerRegistry, error: THttpError, direct?: boolean) {
if (error === null) throw new Error('Unrecoverrable error!! Error is null!')
if (axios.isAxiosError(error)) {
const response = error?.response
const config = error?.config
const data = response?.data as HttpData
if (!direct && config?.raw) throw error
const seekers = [
data?.code,
error.code,
error?.name,
String(data?.status),
String(response?.status),
]
const result = this.handleError(seekers, error)
if (!result) {
if (data?.code && data?.description) {
return this.handleErrorObject(error, {
message: data?.description,
})
}
}
} else if (error instanceof Error) {
return this.handleError(error.name, error)
}
//if nothings works, throw away
throw error
}
}
// create ours globalHandlers object
const globalHandlers = new ErrorHandlerRegistry()
让我们深入研究一下resposeErrorHandler
代码。我们选择使用key
作为标识符来选择最佳的错误处理程序。查看代码时,您会看到在注册表中搜索的顺序key
。规则是从最具体到最通用的搜索。
const seekers = [
data?.code, //Our api can send an error code to you personalize the error messsage.
error.code, //The AxiosError has an error code too (ERR_BAD_REQUEST is one).
error?.name, //Error has a name (class name). Example: HttpError, etc..
String(data?.status), //Our api can send an status code as well.
String(response?.status), //respose status code. Both based on Http Status codes.
]
这是 API 发送的错误示例:
{
"code": "email_required",
"description": "An e-mail is required",
"error": true,
"errors": [],
"status": 400
}
还有其他例子:
{
"code": "no_input_data",
"description": "You doesnt fill input fields!",
"error": true,
"errors": [],
"status": 400
}
因此,作为示例,我们现在可以注册我们的通用错误处理:
globalHandlers.registerMany({
//this key is sent by api when login is required
login_required: {
message: 'Login required!',
//the after function will be called when the message hides.
after: () => console.log('redirect user to /login'),
},
no_input_data: 'You must fill form values here!',
//this key is sent by api on login error.
invalid_login: {
message: 'Invalid credentials!',
},
'404': { message: 'API Page Not Found!' },
ERR_FR_TOO_MANY_REDIRECTS: 'Too many redirects.',
})
// you can registre only one:
globalHandlers.register('HttpError', (error) => {
//send email to developer that api return an 500 server internal console.error
return { message: 'Internal server errror! We already notify developers!' }
//when we return an valid ErrorHandlerObject, will be processed as whell.
//this allow we to perform custom behavior like sending email and default one,
//like showing an message to user.
})
我们可以在任何我们喜欢的地方注册错误处理程序,将最通用的错误处理程序分组放在一个 TypeScript 文件中,并将特定的错误处理程序内联保存。您可以自行选择。但是,要实现这一点,我们需要将其附加到我们的http
Axios 实例上。操作如下:
function createHttpInstance() {
const instance = axios.create({})
const responseError = (error: any) => globalHandlers.resposeErrorHandler(error)
instance.interceptors.response.use(responseHandler, responseError)
return instance
}
export const http: AxiosInstance = createHttpInstance()
现在,我们可以发出 ajax 请求,错误处理程序将按预期工作:
import http from '/src/modules/http'
// automagically handle error
http.get('/path/that/dont/exist').then((response) => {
const userList = response.data
console.log(userList)
})
上面的代码将在用户屏幕上显示一个通知气球,因为将触发404
我们之前注册的错误状态代码。
针对一个 http 调用进行自定义
解决方案不止于此。假设只有一个 http 请求,你想404
以不同的方式处理,但仅此而已404
。为此,我们创建dealsWith
以下函数:
export function dealWith(solutions: ErrorHandlerMany, ignoreGlobal?: boolean) {
let global
if (ignoreGlobal === false) global = globalHandlers
const localHandlers = new ErrorHandlerRegistry(global, solutions)
return (error: any) => localHandlers.resposeErrorHandler(error, true)
}
此函数使用ErrorHandlerRegistry
父级来个性化一个键,但对于所有其他键,使用全局处理程序(如果您想要这样做,ignoreGlobal
则可以强制不这样做)。
因此,我们可以编写如下代码:
import http from '/src/modules/http'
// this call will show the message 'API Page Not Found!'
http.get('/path/that/dont/exist')
// this will show custom message: 'Custom 404 handler for this call only'
// the raw is necessary because we need to turn off the global handler.
http.get('/path/that/dont/exist', { raw: true }).catch(
dealsWith({
404: { message: 'Custom 404 handler for this call only' },
})
)
// we can turn off global, and handle ourselves
// if is not the error we want, let the global error take place.
http
.get('/path/that/dont/exist', { raw: true })
.catch((e) => {
//custom code handling
if (e.name == 'CustomErrorClass') {
console.log('go to somewhere')
} else {
throw e
}
})
.catch(
dealsWith({
404: { message: 'Custom 404 handler for this call only' },
})
)
最后的想法
所有这些解释都很棒,但代码,嗯,代码本身,更棒。所以,我创建了一个 GitHub 仓库,里面整理了本文的所有代码,方便大家尝试、改进和自定义。
脚注:
- 这篇文章比第一次意识到的要长得多,但我喜欢分享我的想法。
- 如果您对代码有任何改进,请在评论中告诉我。
- 如果您发现任何错误,请修复我!