在这篇文章中,我们将看到如何在不依赖任何第三方库的情况下用纯Swift编写网络层。我们去看看吧。相信我们的代码看了之后都能做到:面向协议,简单易用,易于实现类型安全。使用枚举(enums)来配置终端(endPoints)。下面是我们网络层的最后一个例子。这个项目的最终目标是进入router.request(。借助枚举的力量,我们可以看到所有有效的终端和我们请求的参数)首先,一些结构在创建任何东西之前,有一个结构非常重要,以便我们以后可以轻松找到我们需要的东西。我坚信文件夹结构对软件架构至关重要。为了让我们的文件井井有条,让我们提前创建所有的组,我会标记每个文件应该放在哪里。这是项目结构的概述。(请注意,这里的名称只是建议,您可以随意命名您的类和组)项目文件夹结构EndPointType协议我们要做的第一件事是定义我们的EndPointType协议。该协议应包含用于配置终端的所有信息。什么是终端?本质上它是一个URL请求(URLRequest),包含了headers、queryparameters和bodyparameters等各种组件。端点类型协议是我们网络层实现的基石。我们创建一个文件,命名为EndPointType,放在服务组中(不是终端组,后面会区分)。TerminalTypeProtocolHTTPProtocol为了创建一个完整的终端,我的终端类型协议中有很多HTTP协议。让我们看看这些协议需要什么。HTTP方法创建一个名为HTTPMethod的文件并将其放入服务组中。此枚举将用于设置我们请求的HTTP方法。HTTPMethod枚举HTTP任务创建一个名为HTTPTask的文件并将其放入服务组中。HTTPTask用于为特定端点配置参数,您可以将适当数量的案例(cases)添加到您的网络层请求中。我将构建我的请求如下所示,它只包含3例HTTPTask枚举在下一章中我们将讨论参数以及如何处理参数的编码。HTTP头文件HTTPHeaders是一个别名(typealias)字典。您可以在HTTPTask文件的开头创建它。publictypealiasHTTPHeaders=[String:String]ParameterandEncoding创建一个名为ParameterEncoding的文件并将其放入编码组中。我们首先需要定义一个参数别名,通过它我们可以让代码更加干净简洁。publictypealiasParameters=[String:Any]然后定义一个协议参数编码器(ParameterEncoder),使用静态函数编码。这种编码方法包含2个参数,inoutURLRequest和Parameters。(为了防止混淆,后面我将函数参数统称为参数)。INOUT是一个swift关键字,用于将参数定义为引用参数。通常变量作为值类型传递给函数。通过在参数前加上inout前缀,我们将其定义为引用类型。要了解有关双向参数的更多信息,您可以单击此处。参数编码器协议将通过JSONParameterEncoder和URLParameterEncoder实现。publicprotocolParameterEncoder{staticfuncencode(urlRequest:inoutURLRequest,withparameters:Parameters)throws}parameterencoder执行的是对参数进行编码的函数,这个方法会失败并报错,所以我们需要处理一下。能够返回自定义错误消息比标准错误消息更有价值。我总是花很多时间分析Xcode给出的一些错误提示。通过自定义错误信息,您可以定义自己的错误信息,您可以清楚地知道错误来自哪里。为此,我创建了一个继承自Error的枚举。NetworkError枚举URL参数编码器创建一个名为URLParameterEncoder的文件并将其放入编码组中。URLParameterEncoderCode上面的代码包含一些参数,并将它们转换为URL参数以便安全传递。您需要知道URL中禁止使用某些字符。参数也由“&”标记分隔,我们需要考虑所有这些。如果之前未设置,我们还会将适当的标头添加到请求中。此示例代码是使用单元测试时应考虑的内容。如果URL没有正确建立,我们就会有很多不必要的错误。如果您使用的是开放API,您不希望您的请求配额被一堆有问题的测试用完。如果你想了解更多关于单元测试的知识,你可以阅读S.T.Huang的这篇文章。JSON参数编码器创建一个名为JSONParameterEncoder的文件,并将其放入编码组中。JSON参数编码器代码与URL参数编码器类似,但这里是针对JSON对参数进行编码,还需要添加相应的头文件。网络路由器创建一个名为NetworkRouter的文件并将其放在服务组中。我们首先为完成定义一个别名。publictypealiasNetworkRouterCompletion=(_data:Data?,_response:URLResponse?,_error:Error?)->()之后我们定义了一个协议网络路由器NetworkRouter代码一个网络路由器有一个产生请求的终端,一旦产生请求,它就会将响应传递给已完成的部分。我加了一个取消功能,有当然好,但没必要用。可以在请求的生命周期中的任何时候调用此函数以取消它。如果您的应用程序有上传或下载任务,这将很有用。为了让我们的路由器处理任何端点类型,我们在这里使用关联类型。如果没有关联类型,路由器将必须具有具体的终端类型。要了解有关关联类型的更多信息,我建议阅读NatashaTheRobot撰写的这篇文章。路由器创建一个名为Router的文件并将其放入服务组中。我们声明了一个URLSessionTask类型的私有变量任务。这个任务本质上是整个工作要做的。我们将这个变量设为私有,因为我们不想让这个类之外的任何东西来调整我们的任务。Routermethodstubrequest这里我们使用共享会话管理(session)来创建URLSession,这是创建URLSession最简单的方法,但是请记住这不是最好的方法。要实现更复杂的URLSession配置,请使用更改会话管理行为的配置。要了解更多信息,我建议阅读这篇文章。在这里,我们通过调用buildRequest并给它一个终端作为路径来构建我们的请求。对buildRequest的调用仅限于do-try-catch块,因为我们的编码器可能会抛出错误。我们只是将所有响应、数据和错误传递给完成部分。Request方法代码构建一个请求,在Router中创建一个名为buildRequest的私有函数,它负责我们网络层的所有重要工作。本质上,它将EndPointType转换为URLRequest。一旦我们的端点生成请求,我们就可以将其传递给会话管理器。这里有很多事情要做,所以我们将分别查看每种方法。让我们分解一下buildRequest方法:我们以URLRequest类型的变量请求为例。给它我们的基本URL,以及我们将要使用的路径。我们将此请求的httpMethod设置为与我们的端点相同。考虑到我们的编码器会报告错误,我们创建了一个do-try-catch块。只需创建一个大的do-try-catch块,我们就不需要为每次尝试构建一个单独的块。打开route.task,根据任务调用合适的encoder。buildRequest方法代码。配置参数在Router中创建一个名为configureParameters的函数configureParameters方法的实现这个函数负责对我们的参数进行编码。因为我们的API要求所有bodyParameters都是JSON,并且URLParameters是URL编码的,所以我们将适当的参数传递给设计的编码器。如果您使用具有多种编码的API,我建议修改HTTPTask以使用编码器枚举。该枚举需要包含您需要的所有不同类型的编码器。然后将有关编码枚举的附加参数添加到configureParameters。打开这个枚举,适当地编码参数。添加附加标头在Router中创建一个名为addAdditionalHeaders的函数。addAdditionalHeaders方法的实现添加了所有附加标头,使它们成为请求标头的一部分。取消函数的实现如下:cancel方法的实现实践下面让我们用一个实际的例子来看看我们搭建的网络层。我们将从TheMovieDB获取一些电影数据到我们的应用程序。MovieEndPointMovieEndPoint和我们在Moya入门中提到的目标类型非常相似。这里我们实现了自己的终端类型,而不是在Moya中实现目标类型。将此文件放在终端组中。importFoundationenumNetworkEnvironment{caseqacaseproductioncasetaging}publicenumMovieApi{caserecommended(id:Int)casepopular(page:Int)casenewMovies(page:Int)casevideo(id:Int)}extensionMovieApi:EndPointType{varenvironmentBaseURL:String{switchNetworkManager.environment{case.production:return"https//api.themoviedb.org/3/movie/"case.qa:return"https://qa.themoviedb.org/3/movie/"case.staging:return"https://staging.themoviedb.org/3/movie/"}}varbaseURL:URL{guardleturl=URL(string:environmentBaseURL)else{fatalError("baseURLcouldnotbeconfigured.")}returnurl}varpath:String{switchself{case.recommended(letid):return"\(id)/recommendations"case.popular:return"popular"case.newMovies:return"now_playing"case.video(letid):return"\(id)/videos"}}varhttpMethod:HTTPMethod{return.get}vartask:HTTPTask{switchself{case.newMovies(letpage):return.requestParameters(bodyParameters:nil,urlParameters:["page":page,"api_key":NetworkManager.MovieAPIKey])default:return.request}}varheaders:HTTPHeaders?{returnil}}终端类型MovieModel(MovieModel)因为TheMovieDB的响应也是JSON,所以我们的Movie模式也不会改变我们如何使用可解码协议将JSON转换为我们的模式。将此文件放在模式组中。importFoundationstructMovieApiResponse{letpage:IntletnumberOfResults:IntletnumberOfPages:Intletmovies:[电影]}extensionMovieApiResponse:Decodable{privateenumMovieApiResponseCodingKeys:String,CodingKey{casepagecasenumberOfResults="total_results"casenumberOfPages="total_pages"casemovies:result(letconcode)}trydecoder.container(keyedBy:MovieApiResponseCodingKeys.self)page=trycontainer.decode(Int.self,forKey:.page)numberOfResults=trycontainer.decode(Int.self,forKey:.numberOfResults)numberOfPages=trycontainer.decode(Int.self,forKey:.numberOfPages)movies=trycontainer.decode([Movie].self,forKey:.movi??es)}}structMovie{letid:IntletposterPath:Stringletbackdrop:Stringlettitle:StringletreleaseDate:Stringletrating:Doubleletoverview:String}extensionMovie:Decodable{enumMovieCodingKeys:String,CodingKey{caseidcaseposterPath="poster_path"casebackdrop="backdrop_path"casetitlecasereleaseDate="release_date"caserating="vote_average"caseoverview}init(fromdecoder:Decoder)throws{letmovieContainer=trydecoder.container(keyedBy:MovieCodingKeys.self)id=trymovieContainer.decode(Int.self,forKey:.id)posterPath=trymovieContainer复制代码.decode(String.self,forKey:.posterPath)backdrop=trymovieContainer.decode(String.self,forKey:.backdrop)title=trymovieContainer.decode(String.self,forKey:.title)releaseDate=trymovieContainer.decode(String.self,forKey:.releaseDate)rating=trymovieContainer.decode(Double.self,forKey:.rating)overview=trymovieContainer.decode(String.self,forKey:.overview)}}电影模式网络管理员创建名为NetworkManager文件的网络管理器并将它放在管理员组中,从现在开始我们的网络管理员将只有2个静态属性:您的API密码和网络环境(参考MovieEndPoint)。网络管理员还有一个MovieApi类型的Router。NetworkManager代码NetworkResponse在NetworkManager中创建一个名为NetworkResponse的枚举。NetworkResponse枚举我们将使用此枚举来处理来自API的响应并显示相应的信息。Result在NetworkManager中创建一个枚举Result。结果枚举结果枚举可用于许多不同的事情并且非常有用。我们根据结果确定对API的调用是成功还是失败。如果失败,我们将返回一条错误消息和原因。要了解有关面向结果的编程的更多信息,您可以阅读此对话。处理网络响应创建一个名为handleNetworkResponse的函数。此函数有一个参数HTTPResponse,并返回一个结果。这里我们启用HTTPResponse的状态码,这是一个HTTP协议,可以告诉我们响应的状态。基本上200-299之间都是成功的。打电话现在我们的网络层有了坚实的基础。是时候开始打电话了。我们将从API中获取新电影列表。创建一个名为getNewMovies的函数。getNewMovies方法的实现让我们分解该方法的每个步骤。我们将getNewMovies方法定义为采用2个参数:一个页码和一个可以返回电影数组或错误消息的完成。我们调用路由器,输入页码并在闭包内处理完成。如果没有网络或由于某种原因无法调用API,URLSession将返回错误。请注意,这不是API的故障。这种故障多出在客服这边,很有可能是因为网络不好。我们需要将响应转换为HTTPURLResponse,因为我们需要访问状态代码属性。我们从handleNetworkResponse方法声明一个结果,然后在switch-case块中检查这个结果。成功意味着我们成功联系了API并得到了适当的响应。然后我们检查这个响应是否携带数据。如果没有数据,我们使用返回语句退出方法。如果携带数据,我们需要将数据编码到我们的模式中,然后将编码的电影传递给完成部分。如果结果失败,我们将错误传递给完成部分。就是这样,这是我们的纯Swift网络层,不依赖于Cocoapods和第三方库。要测试api请求是否可以获取电影,请使用网络管理器创建一个viewController并在管理器上调用getNewMovies。classMainViewController:UIViewController{varnetworkManager:NetworkManager!init(networkManager:NetworkManager){super.init(nibName:nil,bundle:nil)self.networkManager=networkManager}requiredinit?(coderaDecoder:NSCoder){fatalError("init(coder:)hasnotbeenimplemented")}overridefuncviewDidLoad(){super.viewDidLoad()view.backgroundColor=.greennetworkManager.getNewMovies(page:1){movies,errorinifleterror=error{print(error)}ifletmovies=movies{print(movies)}}}}MainViewControllerDETOUR-NETWORK记录器示例我最喜欢的Moya功能之一是网络记录器。它使调试更容易,并且可以通过记录所有网络流量来查看请求和响应发生了什么。当我决定实现这个网络层时,我想拥有这个特性。创建一个名为NetworkLogger的文件并将其放在服务组中。我已经实现了将请求记录到控制台的代码。我不会展示我们应该把代码放在代码层的什么地方。这是对您的挑战,创建一个记录控制台响应的函数,并找到将它们放入我们的体系结构中的正确位置。提示:静态函数记录(response:URLResponse)tips你在Xcode中遇到过看不懂的占位符吗?比如,让我们看看刚才写的实现Router的代码NetworkRouterCompletion是我们实现的。即使我们实现了它,有时也很难记住它是哪种类型以及我们如何使用它。我们喜欢Xcode有一个解决方法。只需双击占位符,Xcode就会告诉您。结论我们有一个简单、易于使用、面向协议和可定制的网络层。我们可以完全控制它的功能,也可以完全理解它的机制。通过做这个练习,我可以说我自己学到了很多新东西。所以比起那些仅仅安装一个库就能完成的工作,我更为这项工作感到自豪。希望这篇文章已经表明在Swift中创建自己的网络层并不那么困难。只是不要做这样的事情:你可以在我的GitHub上找到源代码,感谢阅读。
