在Swift中编写网络层:面向协议的方法

在本指南中,我们将研究如何在没有任何第三方库的情况下在纯Swift中实现网络层。 让我们直接跳到它! 阅读指南后,我们的代码应为:

  • 面向协议
  • 易于使用
  • 易于实施
  • 输入安全
  • 使用枚举来配置端点。

以下是我们最终将通过网络层实现的示例:

只需输入router.request(。 借助枚举的力量,我们可以看到所有可用的端点以及该请求所需的参数。

首先,一些结构

当创建任何东西时,拥有结构总是很重要的,因此以后找东西很容易。 我坚信文件夹结构是软件体系结构的关键因素。 为了使文件井井有条,让我们事先创建所有组,然后记下每个文件的存放位置。 这是项目结构的概述。 ( 请注意,名称仅是建议,您可以根据自己的喜好命名课程和组。

EndPointType协议

我们需要的第一件事是定义我们的EndPointType协议。 该协议将包含配置端点的所有信息。 什么是端点? 好吧,从本质上讲,它是一个URLRequest及其所有组成部分,例如标头,查询参数和主体参数。 EndPointType协议是我们网络层实现的基石。 继续,创建一个文件并将其命名为EndPointType 。 将此文件放在“ 服务”组中。 (不是EndPoint组,我们将继续进行说明)。

HTTP协议

我们的EndPointType具有构建整个端点所需的许多HTTP协议。 让我们探究这些协议的含义。

HTTP方法

创建一个名为HTTPMethod的文件,然后 将其放在“ 服务”组中。 该枚举将用于设置请求的HTTP方法。

HTTP任务

创建一个名为HTTPTask的文件,然后 将其放入“ 服务”组中。 HTTPTask负责为特定端点配置参数。 您可以添加适用于您的网络层要求的任意多个案例。 我将提出请求,所以我只有三种情况。

下一节将讨论参数以及如何处理编码参数。

HTTP头

HTTPHeaders仅仅是字典的别名。 您可以在HTTPTask文件的顶部创建此类型别名

 公共typealias HTTPHeaders = [String:String] 

参数和编码

创建一个名为ParameterEncoding的文件,然后 将其放在“ 编码”组中。 我们定义的第一件事是参数类型别名。 我们使用typealias使代码更简洁。

 公共typealias参数= [String:Any] 

接下来,使用一个静态函数编码定义协议ParameterEncoder encode方法采用两个参数: inout URLRequestParameters 。 (为避免歧义,我将函数参数称为参数。) INOUT是一个Swift关键字,它将参数定义为参考参数。 通常,变量作为值类型传递给函数。 通过将inout放在参数的前面,我们将其定义为引用类型。 要了解有关inout参数的更多信息,请访问此处。 ParameterEncoder协议将由我们的JSONParameterEncoderURLPameterEncoder实现

 公用协议ParameterEncoder { 
静态函数编码(urlRequest:inout URLRequest,with parameters:Parameters)抛出
}

ParameterEncoder执行一项功能,即对参数进行编码。 此方法可能会失败,因此会引发错误,我们需要处理。

抛出自定义错误而不是标准错误可能很有价值。 我总是很难理解Xcode给出的一些错误。 通过具有自定义错误,您可以定义自己的错误消息并确切知道错误的来源。 为此,我只需创建一个继承自Error的枚举。

URLParameterEncoder

创建一个名为URLParameterEncoder的文件,并将其放在Encoding组中。

上面的代码采用参数,并使其可以安全地作为URL参数传递。 如您所知,URL中禁止使用某些字符。 参数也由’&’符号分隔,因此我们需要满足所有这些要求。 如果未设置请求的适当标题,我们还将为其添加适当的标题。

此代码示例是我们应该考虑使用单元测试进行测试的代码。 正确构建URL是至关重要的,因为我们可能会遇到许多不必要的错误。 如果您使用的是开放API,则您不希望您的请求配额被大量失败的测试所用。 如果您想了解有关单元测试的更多信息,可以通过阅读STHuang的这篇文章开始。

JSONParameterEncoder

创建一个名为JSONParameterEncoder的文件,并将其放置在Encoding组中。

URLParameter编码器类似,但是在这里我们将参数编码为JSON并再次添加适当的标头。

网络路由器

创建一个名为NetworkRouter的文件,并将其放在Service组中。 我们从定义完成类型别名开始。

 公共类型别名NetworkRouterCompletion =(_数据:数据?,_响应:URLResponse?,_错误:错误?)->() 

接下来,我们定义一个协议NetworkRouter

NetworkRouter具有一个EndPoint ,可用于发出请求,一旦发出请求,它将响应传递给完成。 我添加了cancel函数,因为它具有额外的优点,但不要使用它。 可以在请求的生命周期中的任何时间调用此函数并将其取消。 如果您的应用程序具有上载或下载任务,那么这可能被证明非常有价值。 我们在这里使用associatedtype ,因为我们希望我们的路由器能够处理任何EndPointType 。 如果不使用associatedtype,则路由器必须具有具体的EndPointType 。 有关associatedtypes的更多信息,建议您查看NatashaTheRobot的这篇文章。

路由器

创建一个名为Router的文件,并将其放在Service组中。 我们声明一个URLSessionTask类型的私有变量任务。 本质上,这项任务将完成所有工作。 我们将变量保持私有,因为我们不希望此类之外的任何人修改我们的任务。

请求

在这里,我们使用共享会话创建URLSession。 这是创建URLSession的最简单方法。 但是请记住,这不是唯一的方法。 可以使用可以更改会话行为的配置来实现URLSession的更复杂的配置。 有关更多信息,我建议花一些时间阅读这篇文章。

在这里,我们通过调用buildRequest并为其指定一个路由作为EndPoint来创建请求。 由于我们的编码器可能会引发错误,因此此调用将其作为buildRequest包装在do-try-catch块中。 我们只需将所有响应,数据和错误传递给完成即可。

建立要求

Router内部创建一个名为buildRequest的私有函数 此功能负责我们网络层中的所有重要工作。 本质上是将EndPointType转换为URLRequest。 一旦EndPoint成为请求,我们就可以将其传递给会话。 这里做了很多工作,因此我们将分别研究每种方法。 让我们分解一下buildRequest方法:

  1. 我们实例化一个URLRequest类型的变量请求。 给它我们的基本URL,并附加我们将要使用的特定路径。
  2. 我们将请求的httpMethod设置为与EndPoint相等。
  3. 由于编码器抛出错误,因此我们创建了一个do-try-catch块。 通过创建一个大的try-catch块,我们不必为每次尝试创建一个单独的块。
  4. 开启route.task
  5. 根据任务,调用适当的编码器。

配置参数

Router内创建一个名为configureParameters的函数。

这个函数负责编码我们的参数。 由于我们的API期望所有bodyParameters作为JSON和URLParameters进行URL编码,因此我们只需将适当的参数传递给其指定的编码器即可。 如果您要处理具有多种编码样式的API,建议您修改HTTPTask以使用编码器Enum。 该枚举应具有所需的所有不同样式的编码器。 然后在configureParameters内​​部添加编码器Enum的附加参数。 打开枚举并适当地编码参数。

添加其他标题

Router内创建一个名为addAdditionalHeaders的函数。

只需将所有其他标头添加为请求标头的一部分即可。

取消

取消功能的实现将是这样的:

在实践中

现在,在一个实际示例中使用我们构建的网络层。 我们将利用TheMovieDB🍿将一些电影数据导入我们的应用程序。

电影终点

MovieEndPoint与“ Moya入门”中的“目标类型”非常相似(如果尚未阅读,请检查出来)。 现在,我们无需实现Moya的 TargetType,而只需实现自己的EndPointType 。 将此文件放在EndPoint组中。

网络响应

NetworkManager中创建一个名为NetworkResponse的枚举。

我们将利用该枚举来处理来自API的响应并显示适当的消息。

结果

NetworkManager中创建一个枚举结果

结果Enum非常强大,可以用于许多不同的事情。 我们将使用Result来确定对API的调用是成功还是失败。 如果失败,我们将返回一条错误消息并说明原因。 有关面向结果的编程的更多信息,您可以观看或阅读此演讲。

处理网络响应

创建一个名为handleNetworkResponse的函数,该函数接受一个参数HTTPResponse并返回Result 。

在这里,我们打开HTTPResponse的statusCode。 statusCode是一个HTTP协议,它告诉我们响应的状态。 通常,200到299之间的任何值都是成功的。 有关statusCodes的更多信息,请阅读此内容。

拨打电话

因此,现在我们为网络层奠定了坚实的基础。 现在该打电话了!

我们将从API中获取新电影的列表。 创建一个名为getNewMovies的函数。

让我们分解一下此方法的每个步骤:

  1. 我们使用两个参数定义getNewMovies方法:页码和完成返回可选的Movie数组或可选的错误消息。
  2. 我们称我们的路由器。 传递页码并在闭包内处理完成。
  3. 如果没有网络或由于某种原因无法进行API调用, URLSession将返回错误。 请注意,这不是API错误。 这样的故障是在客户端,可能是由于互联网连接不良。
  4. 我们需要将响应 强制转换为HTTPURLResponse,因为我们需要访问statusCode属性。
  5. 我们声明一个从handleNetworkResponse方法获得的结果 。 然后,我们在switch-case块中检查结果。
  6. 成功意味着我们能够与API成功通信并获得适当的响应。 然后,我们检查响应是否返回数据。 如果没有日期,我们只需使用return语句退出该方法。
  7. 如果响应返回数据,则需要将数据解码为模型。 然后,我们将解码的电影传递给完成。
  8. 失败的情况下,我们只需将错误传递给完成即可。

并做了! 那就是我们纯Swift中没有Cocoapods或第三方库的网络层。 要发出测试api请求以获取电影,请使用网络管理器创建一个viewController,然后在该管理器上调用getNewMovies。

NetworkRouterCompletion是我们实现的。 即使我们实现了它,有时也很难确切记住该类型是什么以及我们应该如何使用它。 我们心爱的Xcode可以解救! 只需双击占位符,其余的将由Xcode完成。

结论

现在,我们有了一个可以自定义的易于使用的面向协议的网络层。 我们对它的功能拥有完全的控制权,并对它的原理有完全的了解。 通过进行这项练习,我可以真实地说我自己已经学到了一些新东西。 因此,与仅安装一个库的工作相比,我为此感到自豪。 希望,这篇文章证明在Swift中创建自己的网络层确实并不难。 😜只是不要这样做:

您可以在我的GitHub上找到源代码。 谢谢阅读!