從零開始打造一個(gè) Swift 網(wǎng)絡(luò)框架

說起網(wǎng)絡(luò)框架,大家第一時(shí)間就會(huì)想到 AFNetworking、Alamofire 這些業(yè)內(nèi)響當(dāng)當(dāng)?shù)淖髌?,有的老鳥也會(huì)適當(dāng)傷感一下曾經(jīng)用的 ASI 。這些框架都有一個(gè)共同點(diǎn)——功能都很復(fù)雜,很齊全,而我們往往只能用到很小很小的一個(gè)部分。

事實(shí)上,咱們做 App 的時(shí)候,絕大多數(shù)時(shí)候?qū)W(wǎng)絡(luò)的需求都是收發(fā) GET/POST 請(qǐng)求。就這樣來看,根據(jù)需求來造個(gè)屬于自己的輪子,似乎也是個(gè)不錯(cuò)的選擇。尤其是現(xiàn)在蘋果提供的 NSURLSession 已經(jīng)非常強(qiáng)大,基于原生的 SDK 來做一個(gè)自己的框架,其實(shí)是很容易的。

根據(jù)這個(gè)思想,我之前擼了一個(gè)簡單的網(wǎng)絡(luò)庫 AaHTTP,在工作的項(xiàng)目里重度用了一段時(shí)間也沒有遇到什么特別的問題。

現(xiàn)在我們就來一步步看看如何做一個(gè)屬于自己的簡單的網(wǎng)絡(luò)框架。

發(fā)送請(qǐng)求的步驟分析

要發(fā)送一個(gè)請(qǐng)求,分為如下步驟:

  1. 如果攜帶的參數(shù)是 GET 類型,則將參數(shù)進(jìn)行 URL encode(轉(zhuǎn)化為 y1=x1&y2=x2的形式),追加到原始 url 的后面。如果參數(shù)是 POST 類型,則 URL 不變。
  2. 用最新的 URL 生成一個(gè) NSMutableURLRequest 的對(duì)象
  3. 如果參數(shù)是 POST 的情況,設(shè)置 Content-Typeapplication/x-www-form-urlencoded, 并將參數(shù)進(jìn)行 URL encode,并添加到 body 中。
  4. 使用 NSURLSession 發(fā)送該請(qǐng)求

URL encode時(shí),需要對(duì)特殊字符進(jìn)行轉(zhuǎn)義。

定義發(fā)送請(qǐng)求的接口

根據(jù)上面的步驟,我們不難一步到位的實(shí)現(xiàn)發(fā)送請(qǐng)求,新建一個(gè) AaNet.swift (名字您隨意),并聲明我們的類方法:

class AaNet: NSObject {
    class func request( method : String = "GET",url : String ,form : Dictionary<String,AnyObject> = [:],success : (data : NSData?)->Void,fail:(error : NSError?)->Void){


    }

    func buildParams(parameters: [String: AnyObject]) -> String {
       return ""
    }
}

我們首先聲明了兩個(gè)函數(shù),request 函數(shù)接受的參數(shù)依次是:

  • method: 請(qǐng)求類別
  • url: 目標(biāo)地址
  • form: 參數(shù)表
  • success: 成功的回調(diào), 類型為(data:NSData?) -> Void
  • fail: 失敗的回調(diào),類型為(error : NSError?) -> Void

第二個(gè)函數(shù) buildParams, 輸入一個(gè)字典,返回一個(gè)字符串。很容易想到就是我們用來做 url encode 的函數(shù)。

建議大家寫代碼前,都先寫出主要函數(shù)的聲明和對(duì)應(yīng)的參數(shù)、返回值的類型。這其實(shí)就是一種最基本的架構(gòu)工作

實(shí)現(xiàn)發(fā)送請(qǐng)求

現(xiàn)在按照之前的分析,我們來實(shí)現(xiàn)請(qǐng)求發(fā)送的邏輯:

    class func request( method : String = "GET",url : String ,form : Dictionary<String,AnyObject> = [:],success : (data : NSData?)->Void,fail:(error : NSError?)->Void){

        var innerUrl = url
        
        if method == "GET"{
            innerUrl += "?" + AaNet().buildParams(form)
        }
        
        let req = NSMutableURLRequest(URL: NSURL(string: innerUrl)!)
                            
        req.HTTPMethod = method
        
        if method == "POST" {
            req.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
            print("POST PARAMS \(form)")
            req.HTTPBody = AaNet().buildParams(form).dataUsingEncoding(NSUTF8StringEncoding)
        }
        
        let session = NSURLSession.sharedSession()
        
        print(req.description)
        
        let task = session.dataTaskWithRequest(req) { (data, response, error) -> Void in
            if error != nil{
                fail(error: error)
                print(response)
            }else{
                if (response as! NSHTTPURLResponse).statusCode  == 200{
                    success(data : data)
                }else{
                    fail(error: error)
                    print(response)
                }
            }
        }
        task.resume()
        
    }

整個(gè)流程很直觀,雖然 GET 參數(shù)和 POST 參數(shù)處理的位置不同,但都是用我們的 url encode 函數(shù) buildParams 來操作的。區(qū)別是 GET 請(qǐng)求的話,處理完后直接 append 到 url 后面,而 POST 需要用 UTF8 encode 一下,放在 request 的 body 里。

然后用 NSURLSession 的默認(rèn) session: sharedSession() 來發(fā)送請(qǐng)求,并在回調(diào)里判斷 statusCode 以及 error 對(duì)象是否為 nil 來判斷請(qǐng)求是否為空,來分別調(diào)用我們的 success 回調(diào)或 fail 回調(diào)。

實(shí)現(xiàn) URL encode

現(xiàn)在我們來實(shí)現(xiàn) buildParams,大體的步驟為:

encode:

  1. 把輸入字典轉(zhuǎn)換為鍵值對(duì)的數(shù)組。[ (Key,Value) ]
  2. 對(duì)于每一個(gè) (key,value),執(zhí)行:
    2.1 對(duì) key 進(jìn)行轉(zhuǎn)義,得到 key'
    2.2 檢查 value 的類型,如果是簡單的值,則對(duì)其進(jìn)行轉(zhuǎn)義,得到 value'。并將 (key' , value') 輸出到結(jié)果數(shù)組中。
    2.3 如果 value 是數(shù)組,則用當(dāng)前的 keyvalue 中的每一個(gè)元素組成 tuple: [(key,subValue)], 遞歸執(zhí)行步驟2。
    2.4 如果 value 是字典,也先把 value 對(duì)應(yīng)的字段轉(zhuǎn)化為鍵值對(duì)數(shù)組,但是 key 的形式為 key[subKey], 前面是 key 是當(dāng)前的 key,subKey 代表 value 對(duì)應(yīng)的字典中的 key。得到鍵值對(duì)數(shù)組后,遞歸執(zhí)行步驟2。
  3. 步驟2執(zhí)行完畢后,我們會(huì)得到一個(gè)一維的、并且 key 和 value 都被轉(zhuǎn)義過的鍵值對(duì)數(shù)組 [ (key,value) ],然后我們將其轉(zhuǎn)換為 key1=value1&key2=value2&...keyN=valueN 的形式返回。

仔細(xì)感受一下,步驟2是不是有一個(gè) flat 的過程。

我們先實(shí)現(xiàn)轉(zhuǎn)義:

    func escape(string: String) -> String {
        let legalURLCharactersToBeEscaped: CFStringRef = ":&=;+!@#$()',*"
        return CFURLCreateStringByAddingPercentEscapes(nil, string, nil, legalURLCharactersToBeEscaped, CFStringBuiltInEncodings.UTF8.rawValue) as String
    }

沒啥技術(shù)含量,可直接抄去用。然后根據(jù)我們上面的分析,實(shí)現(xiàn) URL encode:

    func buildParams(parameters: [String: AnyObject]) -> String {
        var components: [(String, String)] = []
        for key in Array(parameters.keys).sort() {
            let value: AnyObject! = parameters[key]
            components += self.queryComponents(key, value)
        }
        
        return (components.map{"\($0)=\($1)"} as [String]).joinWithSeparator("&")
    }
        
        
    func queryComponents(key: String, _ value: AnyObject) -> [(String, String)] {
        var components: [(String, String)] = []
        if let dictionary = value as? [String: AnyObject] {
            for (nestedKey, value) in dictionary {
                components += queryComponents("\(key)[\(nestedKey)]", value)
            }
        } else if let array = value as? [AnyObject] {
            for value in array {
                components += queryComponents("\(key)", value)
            }
        } else {
            components.appendContentsOf([(escape(key), escape("\(value)"))])
        }
        
        return components
    }

我們用了一個(gè)輔助函數(shù) queryComponent 來表達(dá)步驟2這個(gè)遞歸過程。

至此,我們就完成了請(qǐng)求的封裝,這個(gè)部分完整的代碼在這里

現(xiàn)在我們就可以用它來發(fā)送請(qǐng)求了,比如我們想通過 bing 網(wǎng)頁詞典來查詢 joepardize 這個(gè)單詞的意思:

        AaNet.request("GET", url: "http://cn.bing.com/dict/", form: ["q":"jeopardize"], success: { (data) in
            print(String(data: data!, encoding: NSUTF8StringEncoding))
            }) { (error) in
        }

返回:(這里沒有對(duì)結(jié)果進(jìn)行 parse, 這個(gè)不屬于本文的內(nèi)容

**Optional("<!DOCTYPE html><html lang=\"en\" xml:lang=\"en\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:Web=\"http://schemas.live.com/Web/\"><script type=\"text/javascript\">//<![CDATA[\r\nsi_ST=new Date;\r\n//]]></script><head><!--pc--><title>jeopardize - ****必應(yīng)**** Dictionary</title><meta name=\"title\" content=\"****必應(yīng)詞典**** - ****中國領(lǐng)先的中英文在線詞典****\"/><meta name=\"robots\" content=\"nofollow\"/><meta name=\"keywords\" content=\"jeopardize,jeopardize****是什么意思****,jeopardize****的翻譯****,jeopardize****的音標(biāo)****,jeopardize****的讀音****,jeopardize****的用法****,jeopardize****的例句****\"/><meta name=\"description\" content=\"****必應(yīng)詞典為您提供****jeopardize****的釋義,美****[\'d?ep?r.da?z]****,英****[\'d?ep?(r)da?z]****,****v. ****危害;危及;冒****…****的危險(xiǎn);損害;**** ****網(wǎng)絡(luò)釋義:**** ****損壞;使受危險(xiǎn);破壞;**** \"/><meta content=\"text/html; charset=utf-8\" http-equiv=\"content-type\"/><link href=\"/sa/simg/favicon_teal_min.ico\" rel=\"icon\"/><script type=\"text/javascript\">//<![CDATA[\n_G={ST:(si_ST?si_ST:new Date),Mkt:\"en-US\",RTL:false,Ver:\"15\",IG:\"C33762708EB443748A4535A9339C11A0\",EventID:\"71B08123D0674FC09FBEBFA1DEAD9D4B\",V:\"web\",P:\"Dictionary\",DA:\"HK2\",SUIH:\"Jiikj9TC83VvRen-Y4-a_A\",gpUrl:\"\\/fd\\/ls\\/GLinkPing.aspx?\"};_G.lsUrl=\"/fd/ls/l?IG=\"+_G.IG;curUrl=\"http:\\/\\/cn.bing.com\\/dict\\/\";function si_T(a){if(document.images){_G.GPImg=new Image;_G.GPImg.src=_G.gpUrl+\'IG=\'+_G.IG+\'&\'+a;}return true;};\n//]]></script><style type=\"text/css**

更優(yōu)雅的接口和適配器模式

顯然,目前的接口并不友好,封裝也很低級(jí)。對(duì)于移動(dòng)應(yīng)用的網(wǎng)絡(luò)開發(fā)而言,還有幾個(gè)基本的需求沒有被覆蓋:

  • 默認(rèn)的主機(jī)名: 我們的 app 一般的后臺(tái)就一個(gè)域名,如果我們每次發(fā)一個(gè)請(qǐng)求都要敲一遍域名那真的太蛋疼了。
  • 默認(rèn)的參數(shù)列表: 很多參數(shù)是基本每個(gè)請(qǐng)求都要帶的,比如 app 的版本,用戶設(shè)備的語言等等。
  • 更加簡短并讓人一看就懂得函數(shù)調(diào)用。
  • 參數(shù)可缺省
  • 錯(cuò)誤處理可缺省

要實(shí)現(xiàn)上述的需求,我們有兩條路可以走:

  • 在 AaNet 內(nèi)部加上對(duì)應(yīng)的邏輯,然后對(duì)之前的 request 做各種函數(shù)重載來實(shí)現(xiàn)。
  • 做一個(gè)新的模塊,實(shí)現(xiàn)上述功能,但底層的數(shù)據(jù)發(fā)送調(diào)用 AaNet, AaNet 代碼不變。

憑直覺來看,似乎應(yīng)該選擇第二個(gè)方案,首先上面的需求可能是多變的,但 AaNet 目前完成的功能是基本不會(huì)變的(除非 HTTP 協(xié)議的標(biāo)準(zhǔn)改變),變化的和不變的應(yīng)該分開。其次是我們?cè)趯碛锌赡苡龅?AaNet 不能滿足我們的需求,需要采用一些更加成熟的框架(e.g. AFNetworking 等)的時(shí)候,遷移的成本要最低的話,用一個(gè)中間層把我們的代碼和 AaNet 隔開是個(gè)很不錯(cuò)的選擇。

這個(gè)思想在設(shè)計(jì)模式中叫做適配器模式, 我們新開一個(gè) AaHTTP (名字任意)類來處理上述的需求,在底層調(diào)用 AaNet 來實(shí)現(xiàn)請(qǐng)求的發(fā)送。 然后在代碼里調(diào)用 AaHTTP 的方法來完成業(yè)務(wù)邏輯,這樣,即便某一天我們要需要替換網(wǎng)絡(luò)通信的框架,也只是需要在 AaHTTP 內(nèi)部的實(shí)現(xiàn)上修改 AaNet 為其他實(shí)現(xiàn)即可,不需要修改其他代碼。 這里的 AaHTTP 就是一種典型的適配器。

實(shí)現(xiàn) AaHTTP

比起 AaNet, AaHTTP 的實(shí)現(xiàn)是很簡單的,主要都是一些設(shè)計(jì)層面的東西。

方便區(qū)別 GET 和 POST, 用字符串肯定是不明智的,我們?cè)黾右粋€(gè) enum:

enum RequestMethod{
    case Post
    case Get
}

成員變量什么的就不用一一列舉了,大家可以直接查看該文件完整的源代碼。 這里看一下對(duì)外暴露的4個(gè)方法

為了實(shí)現(xiàn)鏈?zhǔn)秸{(diào)用,每個(gè)方法返回的都是自身

    func fetch(url : String) -> AaHTTP{
        setDefaultParas()
        curUrl = "\(hostName)\(url)"
        self.method = .Get
        return self
    }
    
    func post(url : String) -> AaHTTP{
        setDefaultParas()
        curUrl = "\(hostName)\(url)"
        self.method = .Post
        return self
    }
    
    func paras(p : [String:AnyObject]) -> AaHTTP{
        _ = p.reduce("") { (str, p) -> String in
            parameters[p.0] = p.1
            return ""
        }
        return self
    }
    
    func go(success : String -> Void, failure : NSError?->Void){
        var smethod = ""
        if method == .Get{
            smethod = "GET"
        }else{
            smethod = "POST"
        }
        
        AaNet.request(smethod, url: curUrl, form: parameters, success: { (data) -> Void in
            print("request successed in \(self.curUrl)")
            let result = String(data: data!, encoding: NSUTF8StringEncoding)
            success(result!)
            }) { (error) -> Void in
                print("request failed in \(self.curUrl)")
                failure(error)
        }
    }

fetchpost 分別生成 GET 和 POST 請(qǐng)求,paras 方法設(shè)置參數(shù),go 方法進(jìn)行實(shí)際請(qǐng)求操作。

現(xiàn)在,我們可以這樣來發(fā)送網(wǎng)絡(luò)請(qǐng)求:

aht.shareInstance.fetch("http://yahoo.com").go({ (result) in print(result) }) { (error) in print(error) }

如果有參數(shù)的話:

aht.shareInstance.fetch("http://cn.bing.com/dict/").paras(["q":"jeopardize"]).go({ (result) in print(result) }) { (error) in print(error) }

通過該類內(nèi)部的 hostname 屬性,即可實(shí)現(xiàn)缺省的主機(jī)名。

結(jié)語

至此,我們就完成了一個(gè)最簡單、但足以應(yīng)付絕大多數(shù)網(wǎng)絡(luò)請(qǐng)求的框架,或者也可以基于此走得更遠(yuǎn),比如:

  • 嘗試管理多個(gè) NSURLSession
  • 嘗試實(shí)現(xiàn)文件的下載與上傳
  • 嘗試集成常見的 restful api authentication 的功能,比如 BCE的鑒權(quán)機(jī)制
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容