当前位置: 首页 > 科技观察

如何在现代JavaScript中安全地获取网络数据

时间:2023-03-21 13:24:04 科技观察

Fetch-错误的获取方式在JavaScript中非常棒。但是,您的代码中可能充斥着这样的内容:constres=awaitfetch('/user')constuser=awaitres.json()这段代码虽然简单易用,但存在很多问题。你可以说“哦,是的,错误处理”,然后像这样重写它:当然,这是一种改进,但仍然存在问题。在这里,我们假设user实际上是一个User对象……但这是假设我们收到200响应。但fetch不会针对200以外的状态抛出错误,因此您实际上可能会收到400(错误请求)、401(未授权)、404(未找到)、500(内部服务器错误)或各种其他问题。A一种更安全但更丑陋的方式所以我们可以进行另一个更新:401:/*Handle*/breakcase404:/*Handle*/breakcase500:/*Handle*/break}}//User是用户constuser=awaitres.json()}catch(err){//错误处理}现在,我们终于可以很好地使用fetch了。但它可能有点笨拙,因为每次都必须记住它,而且你必须希望你团队中的每个人每次都能处理这些情况。它在控制流方面也不是最优雅的。在可读性方面,我个人更喜欢本文开头提到的代码。它读起来很清晰——获取用户,解析为json,用用户对象做一些事情。但是在这种格式中,我们获取用户,处理一堆错误情况,解析json,处理其他错误情况等。这有点不协调,尤其是在我们在业务逻辑之上和之下进行错误处理的这一点上,而不是在一个地方。一种不那么丑陋的方法如果请求有问题,一个更优雅的解决方案可能是抛出异常而不是在多个地方处理错误:try{constres=awaitfetch('/user')if(!res.ok){thrownewError('Badfetchresponse')}constuser=awaitres.json()}catch(err){//错误处理}但我们还有最后一个问题——当我们需要处理错误时,我们输了很多有用的上下文。我们实际上无法访问catch块内的res,因此在处理错误时我们实际上不知道响应的状态代码或主体是什么。这将使我们很难知道要采取的最佳行动方案,并给我们留下非常无用的日志。这里一个改进的解决方案可能是创建您自己的自定义错误类,您可以在其中转发响应详细信息:fetch('/user')if(!res.ok){thrownewResponseError('Badfetchresponse',res)}constuser=awaitres.json()}catch(err){//使用完全访问权限处理错误状态和文字开关(err.response.status){case400:/*Handle*/breakcase401:/*Handle*/breakcase404:/*Handle*/breakcase500:/*Handle*/break}当我们保留状态代码时,我们现在可以更智能地处理错误。例如,我们可以通过500提醒用户我们遇到了问题,并可能重试或联系我们的支持人员。或者如果状态为401,他们当前未被授权,可能需要重新登录等。创建包装器对于我们最新最好的解决方案,我还有最后一个问题——它仍然需要开发人员每次都编写一些像样的样板文件。在整个项目范围内进行更改或强制执行此结构仍然是一个挑战。这是我们可以包装fetch以根据需要处理事情的地方:..options)if(!res.ok){thrownewResponseError('Badfetchresponse',res)}returnres}然后我们可以如下使用它:try{constres=awaitmyFetch('/user')constuser=awaitres.json()}catch(err){//通过error.response处理问题。*}在我们的最后一个例子中,最好确保我们有一个统一的方法来处理错误。这可能包括对用户的警报、日志记录等。探索开源解决方案很有趣,但重要的是要记住,您不必总是为事物创建自己的包装器。以下是一些可能值得使用的流行的现有选项,包括一些小于1kb的选项:Axiosaxios是一个非常流行的JS抓取选项,它可以自动为我们处理上面的几个场景。try{const{data}=awaitaxios.get('/user')}catch(err){//基于error.response的错误处理。*}我对Axios的唯一批评是它是一个简单的数据获取包装器大的。因此,如果大小是您的首要任务(我认为通常应该这样做以保持您的性能一流),您可能需要查看以下两个选项之一:Redaxios如果您喜欢Axios,但不喜欢它提供的软件包添加11kb的大小,Redaxios是一个很好的选择,它使用与Axios相同的API但小于1kb。importaxiosfrom'redaxios'//像往常一样使用Wretch一个更新的选项是Wretch,它是Fetch的一个非常薄的包装器,就像Redaxios。Wretch的独特之处在于它在很大程度上仍然感觉像fetch,但为您提供了处理常见状态的有用方法,这些状态很好地链接在一起:constuser=awaitwretch("/user").get()//以更易读的方式处理错误情况方式。notFound(error{/*...*/}).unauthorized(error{/*...*/}).error(418,error{/*...*/}).res(response/*...*/).catch(error{/*othererrors*/})也不要忘记安全地写入数据最后但同样重要的是,我们不要忘记直接使用fetch当通过POST、PUT或PATCH发送数据你能发现这段代码中的错误吗?//这里至少有一个错误,你能发现吗?constres=awaitfetch('/user',{method:'POST',body:{name:'SteveSewell',company:'Builder.io'}})至少有一个,但可能有两个。首先,如果我们发送JSON,body属性必须是一个JSON序列化的字符串:constres=awaitfetch('/user',{method:'POST',//?我们必须JSON序列化这个bodybody:JSON.stringify({name:'SteveSewell',company:'Builder.io'})})这很容易忘记,但如果我们使用TypeScript,这至少会自动提示我们。TypeScript不会为我们捕获的另一个错误是我们没有在此处指定Content-Type标头。许多后端要求您指定它,否则它们将无法正确处理文字。constres=awaitfetch('/user',{headers:{//?如果我们发送序列化的JSON,我们应该设置Content-Type:'Content-Type':'application/json'},method:'POST',body:JSON.stringify({name:'SteveSewell',company:'Builder.io'})})现在,我们有了一个相对健壮和安全的解决方案。(可选)向我们的包装器添加自动JSON支持我们还可能决定在包装器中为这些常见情况添加一些安全措施。例如使用下面的代码:constisPlainObject=valuevalue?.constructor===ObjectexportasyncfunctionmyFetch(...options){letinitOptions=options[1]//如果我们为fetch指定一个RequestInitif(initOptions?.body){//如果我们传递一个body属性,它是一个普通对象或数组if(Array.isArray(initOptions.body)||isPlainObject(initOptions.body)){//创建一个新的选项对象来序列化body并确保我们有一个内容类型标头initOptions={...initOptions,body:JSON.stringify(initOptions.body),headers:{'Content-Type':'application/json',...initOptions.headers}}}}constres=awaitfetch(...initOptions)if(!res.ok){thrownewResponseError('Badfetchresponse',res)}returnres}现在我们可以像这样使用我们的包装器:constres=awaitmyFetch('/user',{method:'POST',body:{name:'SteveSewell',company:'Builder.io'}})简单安全。我喜欢。开源解决方案虽然定义我们自己的抽象很有趣,但让我们指出几个流行的开源项目如何自动为我们处理这些情况:"代码实际上按预期工作:constres=awaitaxios.post('/user',{name:'SteveSewell',company:'Builder.io'})Wretch同样,对于Wretch,大多数基本示例也可以作为expected:constres=awaitwretch('/user').post({name:'SteveSewell',company:'Builder.io'})(可选)使我们的包装类型Safety最后但并非最不重要,如果你想在fetch周围实现你自己的包装器,如果你正在使用它,我们至少要确保它是类型安全的TypeScript。这是我们的最终代码,包括类型定义:constisPlainObject=(value:unknown)=>value?.constructor===ObjectclassResponseErrorextendsError{response:Responseconstructor(message:string,res:Response){super(message)this.response=res}}导出异步函数myFetch(input:RequestInfo|URL,init?:RequestInit):Promise{letinitOptions=initif(initOptions?.body){if(Array.isArray(initOptions.body))||isPlainObject(initOptions.body)){initOptions={...initOptions,body:JSON.stringify(initOptions.body),headers:{"Content-Type":"application/json",...initOptions.headers,},}}}constres=awaitfetch(input,initOptions)if(!res.ok){thrownewResponseError("Badresponse",res)}returnres}使用我们的新类型安全时的最后一个陷阱你当您提取包装器时,会遇到最后一个问题。在typescript的catch块中,默认错误是任意类型(any)===500)...}你可以说,哦!我只是输入错误:try{constres=awaitmyFetch}catch(err:ResponseError){//TSerror1196:Catch子句变量类型注释必须是'any'或'unknown'ifspecified}嗯,是的,我们可以'TypeScript中的拼写错误。这是因为从技术上讲,您可以在任何地方将任何内容放入TypeScript。以下是理论上可以存在于任何try块中的所有有效JavaScript/TypeScript例如网络错误,例如没有可用的连接。我们也可能不小心在我们的fetch包装器中有一个合法的错误,它会抛出其他错误,比如TypeError所以这个包装器的最终、干净和类型安全的用法是这样的:}catch(err:unknown){if(errinstanceofResponseError){switch(err.response.status){...}}else{thrownewError('获取用户时发生未知错误',{cause:err})}这里我们可以使用instanceof检查err是否是ResponseError实例,并在错误响应的条件块中获得完整的类型安全。然后,如果出现意外错误,我们也可以重新抛出错误,并使用JavaScript中新的cause属性转发原始错误详细信息,以便更好地调试。可重用的错误处理最后,最好不要总是为每个HTTP调用的每个可能的错误状态设置自定义开关。将我们的错误处理封装到一个可重用的函数中会好得多,我们可以在处理任何我们知道需要特殊逻辑的一次性情况后将其用作回退,因为该调用对该调用是唯一的。例如,我们可能有一种常见的方式想要通过“糟糕,抱歉,请联系技术支持”消息来提醒用户500问题,或者如果没有更具体的方法来处理401问题的状态这个特殊的要求。使用“请重新登录”消息。在实践中,它可能看起来像这样:status===404){//这个调用的特殊逻辑,我们想处理这个状态,就像在404上,我们似乎没有这个用户返回}}//??处理我们不需要特殊的任何其他东西逻辑,只需要我们默认的句柄handleError(err)return}我们可以这样实现:exportfunctionhandleError(err:unknown){//保存到我们选择的日志服务saveToALoggingService(err);if(errinstanceofResponseError){switch(err.response.status){case401://提示用户重新登录showUnauthorizedDialog()break;case500://向用户显示一个对话框,我们有一个错误,然后重试,如果没有,请联系技术支持showErrorDialog()break;default://ShowthrownewError('Unhandledfetchresponse',{cause:err})}}thrownewError('Unknownfetcherror',{cause:err})}usingWretch这是我认为Wretch的亮点之一,因为上面的代码看起来像这样:res.body()}catch(err){//使用默认处理程序来捕获其他一切handleError(err);返回;}使用Axios/Redaxios使用Axios或Redaxios,看起来类似于我们原来的例子{if(err.response.status===404){//未找到逻辑return}}//使用默认处理程序handleError(err)return捕获所有其他内容}结论就是这样!如果不清楚,我个人建议使用现成的包装器来实现抓取,因为它们可以非常小(1-2kb),通常有更多的文档、测试和社区,并且已经被其他人证明和验证是一个有效的解决方案但是,无论您选择手动使用fetch、编写自己的包装器,还是使用开源包装器,一切都已经说完了-对于您的用户和您的团队,请确保您获得正确的数据。