About Moya
Moya is a network abstraction layer that encapsulates Alamofire at the bottom and provides a simpler interface for developers to call. In objective-C in the past, most developers would use AFNetwork for network requests. When the business was more complicated, they would encapsulate AFNetwork twice and write a network abstraction layer suitable for their own projects. In Objective-C, the famous YTKNetwork encapsulates AFNetworking as an abstract parent class, and then writes different subclasses according to different network requests. The subclasses inherit the parent class to realize request services. Moya’s position at the project level is somewhat similar to YTKNetwork. You can see the comparison below
Protocol Oriented Programming (POP)
If you have some knowledge of POP before reading the Moya source code, you will get twice the result with half the effort. There are protocols in Objective-C as well, and typically you extend a class by having an object comply with the protocol and then implementing the methods that the protocol specifies. POP actually reinforces this idea. Many times things have diverse qualities that can’t be inherited from a single class. To address this pain point, C++ has multiple inheritance, which means that a subclass can inherit from more than one parent class, and these inherited parents are not necessarily related to each other. However, there are other problems with this. For example, if a subclass inherits from its parent class, it does not need to use all of the methods and properties of the parent class, which means that the subclass has some useless properties and methods. If the parent class is modified, it is difficult to avoid affecting the child class. C++ multiple inheritance also introduces a diamond defect. What is a diamond defect? I’ll put two links at the bottom of this section for your convenience. Swift introduces protocol-oriented programming, which dictates the implementation of things through protocols. By following a different protocol, to a class or structure or enumeration customization, it only need to implement the properties or methods can be stipulated in the agreement, is similar to building blocks, in each block has a demand module, undertakes assorted, relative to the OOP, lower the coupling, also provide the possibility of more code to maintain and expand. The idea of POP is generally like this. Here are two articles about POP by Wang Wei, which are worth reading. Protocol-oriented Programming meets Cocoa (Part 1) Protocol-oriented Programming meets Cocoa (Part 2)
Module composition of Moya
Since Moya is a web abstraction layer designed using POP, its overall logical structure has no obvious inheritance relationship. The core code of Moya can be divided into the following modules
Provider
Provider is a provider that provides network request services. After some initial configuration, the provider can be used externally to initiate requests directly.
Request
When using Moya for network requests, the first step needs to be configured to generate a Request. First, follow the official documentation to create an enumeration that complies with the TargetType protocol and implements the properties specified by the protocol. Why create enumerations to comply with the protocol, as opposed to creating classes to comply with the protocol as objective-C does? In fact, it is possible to use classes or structures. The reason for using enumerations is that Swift enumerations are much more powerful than Objective-C. Enumerations combine with switch statements to make the API easier to manage. The following figure shows the process of generating a Request
baseURL
path
method
sampleData
task
headers
Copy the code
Once you have provided the “basic materials” for these network requests, you can further configure them to generate the required requests. Looking at the first arrow in the figure above, an endPoint is generated through an EndpointClosure. EndPoit is an object that wraps the properties and methods required for network requests. The EndPoint class has the following properties:
public typealias SampleResponseClosure = () -> EndpointSampleResponse
open let url: String
open let sampleResponseClosure: SampleResponseClosure
open let method: Moya.Method
open let task: Task
open let httpHeaderFields: [String: String]?
Copy the code
As you can see intuitively, the EndPoint properties correspond to the variables configured with TargetTpye above. So what does this process do in the code? In the MoyaProvider class, you have the following declaration
/// Closure that defines the endpoints for the provider.
public typealias EndpointClosure = (Target) -> Endpoint<Target>
open let endpointClosure: EndpointClosure
Copy the code
Declares a closure with a Target, which is a generic, and returns an EndPoint. The endPoint is a class that wraps the parameters and actions of the request and is described in more detail below, starting with what endpointClosure does.
endpointClosure: @escaping EndpointClosure = MoyaProvider.defaultEndpointMapping
Copy the code
The MoyaProvider initialization method calls its extended class method defaultEndpointMapping with the input Target as an argument and returns an endPoint object.
public final class func defaultEndpointMapping(for target: Target) -> Endpoint<Target> {
return Endpoint(
url: URL(target: target).absoluteString,
sampleResponseClosure: { .networkResponse(200, target.sampleData) },
method: target.method,
task: target.task,
httpHeaderFields: target.headers
)
}
Copy the code
Target is the enumeration that is configured at the beginning. The variable of Target is retrieved by point syntax and the endPoint is initialized. There might be some confusion here about urls and sampleResponseClosure. Url initialization can be viewed in URL+ moya. swift, which extends the NSURL class constructor to enable initialization based on Moya’s TargetType.
/// Initialize URL from Moya's `TargetType`. init
(target: T) { // When a TargetType'
s path is empty, URL.appendingPathComponent may introduce trailing /, which may not be wanted in some cases
if target.path.isEmpty {
self = target.baseURL
} else {
self = target.baseURL.appendingPathComponent(target.path)
}
}
Copy the code
SampleResponseClosure is a closure associated with network requests that return bogus data. It can be ignored here to understand how Moya generates requests. . We know the MoyaProvider defaultEndpointMapping can return to the endPoint object, after back over it again
endpointClosure: @escaping EndpointClosure = MoyaProvider.defaultEndpointMapping
Copy the code
With @escaping to declare endpointClosure as a escaping closure, we can
EndpointClosure = MoyaProvider.defaultEndpointMapping
Copy the code
convert
(Target) -> Endpoint<Target> = func defaultEndpointMapping(for target: Target) -> Endpoint<Target>
Copy the code
With further transformation, the left-hand side of the equals sign can be written as a regular closure expression
{(Target)->Endpoint<Target> in
return Endpoint(
url: URL(target: target).absoluteString,
sampleResponseClosure: { .networkResponse(200, target.sampleData) },
method: target.method,
task: target.task,
httpHeaderFields: target.headers
)
}
Copy the code
EndpointClosure is a closure that takes Target as an argument and returns an endPoint object. How do I get the endPoint object returned by the closure? MoyaProvider provides such a method
/// Returns an `Endpoint` based on the token, method, and parameters by invoking the `endpointClosure`.
open func endpoint(_ token: Target) -> Endpoint<Target> {
return endpointClosure(token)
}
Copy the code
That’s how TargetType is converted to an endPoint via endpointClosure.
The next step is to use requestClosure, pass it to the endPoint, and generate the Request. The request generation process is very similar to the endPoint.
Declare in MoyaProvider:
/// Closure that decides if and what request should be performed
public typealias RequestResultClosure = (Result<URLRequest, MoyaError>) -> Void
open let requestClosure: RequestClosure
Copy the code
And there’s a very similar line in the MoyaProvider initialization method
requestClosure: @escaping RequestClosure = MoyaProvider.defaultRequestMapping,
Copy the code
Go to view the defaultRequestMapping method
public final class func defaultRequestMapping(for endpoint: Endpoint<Target>, closure: RequestResultClosure) {
do {
let urlRequest = try endpoint.urlRequest()
closure(.success(urlRequest))
} catch MoyaError.requestMapping(let url) {
closure(.failure(MoyaError.requestMapping(url)))
} catch MoyaError.parameterEncoding(let error) {
closure(.failure(MoyaError.parameterEncoding(error)))
} catch {
closure(.failure(MoyaError.underlying(error, nil)))
}
}
Copy the code
Similar to endpointClosure, we can convert to a requestClosure expression
{(endpoint:Endpoint<Target>, closure:RequestResultClosure) in
do {
let urlRequest = try endpoint.urlRequest()
closure(.success(urlRequest))
} catch MoyaError.requestMapping(let url) {
closure(.failure(MoyaError.requestMapping(url)))
} catch MoyaError.parameterEncoding(let error) {
closure(.failure(MoyaError.parameterEncoding(error)))
} catch {
closure(.failure(MoyaError.underlying(error, nil)))
}
}
Copy the code
The whole thing is to initialize an urlRequest using a do catch statement, passing different parameters to the closure depending on the result. We start with a try call to endpoint.urlRequest() and switch to a catch statement if an error is thrown. UrlRequest () is a long method, so if you are interested, you can check it in Moya’s endpoint.swift. What it does is simply initialize an NSURLRequest object based on the attributes of the endpoint described earlier.
This is the final Request generated from TargetType in the figure above. A lot of people are wondering, why bother? Just pass the necessary parameters and generate a Request. Why add another endPoint? Based on some of the methods provided by the Endpoint class, I think it’s more flexible to configure network requests to accommodate more diverse business needs. The Endpoint class also has several methods
/// Convenience method for creating a new `Endpoint` with the same properties as the receiver, but with added HTTP header fields.
open func adding(newHTTPHeaderFields: [String: String]) -> Endpoint<Target>
/// Convenience method for creating a new `Endpoint` with the same properties as the receiver, but with replaced `task` parameter.
open func replacing(task: Task) -> Endpoint<Target>
Copy the code
Using these methods, you can add headers to some network requests in endpointClosure, replacing the request parameters and making the configuration of those requests more flexible.
How does a Request generated by requestClosure get external access? This is the next step in the Provider sending request implementation process. You’ll see how to use this Request in the next section.
Provider sends requests
Let’s take a look at the official documentation for the basic steps to use Moya
- Create an enumeration that complies with the TargetType protocol and implements the specified properties.
- Initialize the
provider = MoyaProvider<Myservice>()
- Call provider.request to process the result of the request in a closure.
The first of these steps is covered above, and initialization of MoyaProvider is covered only in a small part. I’m not going to cover the rest of the initialization method in one go, which is a lot to cover and a bit cumbersome to understand. Later in the code interpretation, if any related attributes are involved, go back to the initialization method one breakthrough at a time.
open func request(_ target: Target,
callbackQueue: DispatchQueue? = .none,
progress: ProgressBlock? = .none,
completion: @escaping Completion) -> Cancellable {
let callbackQueue = callbackQueue ?? self.callbackQueue
return requestNormal(target, callbackQueue: callbackQueue, progress: progress, completion: completion)
}
Copy the code
If you look at requestNormal directly from here, this method is a bit long, and some of the plugin-related code and the code for the test stub are not going to be a barrier to understanding provider.Request. They are optional. It’s not necessary.
let endpoint = self.endpoint(target)
Copy the code
The endPoint object is generated, which is easy to understand, as explained earlier. View the performNetworking closure
if cancellableToken.isCancelled {
self.cancelCompletion(pluginsWithCompletion, target: target)
return
}
Copy the code
If the request is cancelled, the cancellation completed callback is called and return is not executed in the closure below. RequestResult: Result
= requestResult
Result uses an enumeration to provide some of the results of running the process, as follows, and it’s easy to see what it means.
switch requestResult {
case .success(let urlRequest):
request = urlRequest
case .failure(let error):
pluginsWithCompletion(.failure(error))
return
}
Copy the code
If the request succeeds, the URLRequest is retrieved, and if it fails, the plugin is used to handle the failure callback.
// Allow plugins to modify request
let preparedRequest = self.plugins.reduce(request) { The $1.prepare($0, target: target) }
Copy the code
Use plug-ins to refine the request
cancellableToken.innerCancellable = self.performRequest(target, request: preparedRequest, callbackQueue: callbackQueue, progress: progress, completion: networkCompletion, endpoint: endpoint, stubBehavior: stubBehavior)
Copy the code
Here self. PerformRequest is the actual network request, more internal code, but the idea is very simple, using Alamofire’s SessionManager to send the request. After the configuration is complete, you can call requestClosure(Endpoint, performNetworking) and execute this closure to retrieve the Request described above to execute the specific network Request.
Response
When a request is sent using Alamofire, closures are defined to handle the response to the request. Response This class provides some processing methods for the request results, such as data to JSON, image conversion, etc.
Plugins
Moya provides a plug-in protocol called PluginType, which specifies several methods that specify the application area of the plug-in.
/// Called to modify a request before sending func prepare(_ request: URLRequest, target: TargetType) -> URLRequest /// Called immediately before a request is sent over the network (or stubbed). func willSend(_ request: RequestType, target: TargetType) /// Called after a response has been received, but before the MoyaProvider has invoked its completion handler. func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType) /// Called to modify a result before completion func process(_ result: Result<Moya.Response, MoyaError>, target: TargetType) -> Result<Moya.Response, MoyaError>Copy the code
prepare
The request can be modified before it is requested.willSend
Called immediately before the request is sent, this can be used to add Toast that circles while the request is being sentdidReceive
Called when the request response is received and before the MoyaProvider Completion Handler.process
Called before the Completion handler to modify the result of the request, you can visually understand plug-in call timing by looking at the following figure
The use of plug-ins, so that the code only maintain the trunk logic, users according to business needs to add plug-ins to configure their own network business layer, which is more flexible, low coupling. Moya offers four plugins
- AccessTokenPlugin OAuth Token authentication
- CredentialsPlugin certificate
- NetworkActivityPlugin Network request status
- NetworkLoggerPlugin You can write your own plug-in as required. Select NetworkActivityPlugin to view the internal structure of the plug-in.
public final class NetworkActivityPlugin: PluginType {
public typealias NetworkActivityClosure = (_ change: NetworkActivityChangeType, _ target: TargetType) -> Void
let networkActivityClosure: NetworkActivityClosure
public init(networkActivityClosure: @escaping NetworkActivityClosure) {
self.networkActivityClosure = networkActivityClosure
}
public func willSend(_ request: RequestType, target: TargetType) {
networkActivityClosure(.began, target)
}
public func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType) {
networkActivityClosure(.ended, target)
}
}
Copy the code
Plug-in internal structure is very simple, in addition to some self-defined variables, is to comply with the PluginType protocol, to implement the method specified in the protocol, in a specific method to do what you need to do. Because PluginType already has a protocol extension that implements all of the default methods, it is not necessary to implement all of the protocol methods within a specific plug-in, just the specific methods as needed. Plugins: [PluginType] = [] add plugins to the array for network requests.
conclusion
Moya can be said to be a very Swift framework, the biggest advantage is the use of protocol-oriented thinking, so that users can configure their own network abstraction layer in a building-block manner. The plug-in mechanism is provided to allow developers to customize their own plug-ins according to their business needs and join in the process of network requests at appropriate locations under the premise of maintaining the backbone network request logic.