前言
前些天幫公司做了網(wǎng)絡(luò)層的重構(gòu),當(dāng)時就想做好了就分享給大家,后來接著做了新版本的需求,現(xiàn)在才有時間整理一下。
之前的網(wǎng)絡(luò)層使用的是直接拖拽導(dǎo)入項目的方式導(dǎo)入了AF,然后還修改了大量的源碼,時隔2年,AF已經(jīng)更新?lián)Q代很多次了,導(dǎo)致整個重構(gòu)遷移非常的麻煩。不過看著前輩寫的代碼,肯定也是一個高人,許多思路和我的一樣,但是實現(xiàn)方式又不同,給我很好的參考。
在做網(wǎng)絡(luò)層架構(gòu)的時候也參考了Casa大神的架構(gòu)思想,但是還是有所不同。
本文沒有太多的理論,沒有太多的專業(yè)術(shù)語,一來是方便大家閱讀,二來我的基礎(chǔ)也沒那么好,沒有太多華麗的詞匯,對于架構(gòu)來說主要是思路,有思路在,具體的實現(xiàn)就沒有問題了。
本文主要介紹以下幾點(diǎn):
1.網(wǎng)絡(luò)接口規(guī)范
2.多服務(wù)器多環(huán)境設(shè)置
3.網(wǎng)絡(luò)層數(shù)據(jù)傳遞(請求和返回)
4.業(yè)務(wù)層對接方式
5.網(wǎng)絡(luò)請求怎么自動取消
6.網(wǎng)絡(luò)層錯誤處理
網(wǎng)絡(luò)接口規(guī)范
demo里面的請求示例是在網(wǎng)上找的,不符合我說的這套規(guī)范,僅作示例用。
規(guī)范很重要,有合理的規(guī)范就可以精簡很多代碼邏輯,特別是接口的兼容,是最底層最基礎(chǔ)的設(shè)計,把接口規(guī)范放在前面來說,在做這次重構(gòu)時,我提出了一些規(guī)范點(diǎn),可以給大家參考。
1.兩層三部分?jǐn)?shù)據(jù)結(jié)構(gòu)
接口返回數(shù)據(jù)第一次為字典,分為兩層三部分:code、msg、data。
code:錯誤碼,可以記錄下來快速定位接口錯誤原因,可以定義一套錯誤碼,比如200正常,1重新登錄...
msg:接口文案提示,包括錯誤提示,用來直接顯示給用戶,所以這一套錯誤提示就不能是什么一串英文錯誤了。
data:需要返回的數(shù)據(jù),可以是字典,可以是數(shù)組。
接口幫我們定義了code和msg,是不是我們就不需要做錯誤處理了?當(dāng)然不是,服務(wù)端的錯誤邏輯畢竟是簡單的,具體到data里面的數(shù)據(jù)處理可能還有錯誤,所以錯誤的處理是必不可少的,下面會單獨(dú)對錯誤處理做介紹。
2.網(wǎng)絡(luò)請求參數(shù)上傳方式統(tǒng)一
這里一般都能做到,也有額外的,比如我們的一個服務(wù)器接口做的比較早,當(dāng)時POST接口使用的就不規(guī)范,普通的應(yīng)用信息channelID、device_id使用的是拼接在字符串后面的方式,而真正的請求參數(shù)則需要轉(zhuǎn)成json放在一個字段里面?zhèn)鬟f,就是接口GET、POST并存的方式,造成網(wǎng)絡(luò)層需要做特殊處理。
所以說標(biāo)準(zhǔn)的GET、POST請求方式是很有必要的。
3.關(guān)于Null類型
大家都知道Null類型在iOS里面是很特殊的,我的建議是放在客戶端來做,原因有很多:
1)接口的規(guī)范定義并不是每個公司都是從一開始就能定義好的,老接口如果要把Null字段去掉的改動非常大。
2)客戶端用過一個接口過濾也可以解決,一勞永逸,不用再擔(dān)心因為某天接口的問題出現(xiàn)崩潰,而且通過一些Model的第三方庫也可以很好的解決這個問題。這里不得說下swift的類型檢測真是太方便了,之前一個項目用swift寫的,代碼規(guī)范一點(diǎn),根本不會出現(xiàn)因為參數(shù)類型問題引起崩潰。
多服務(wù)器多環(huán)境設(shè)置
這部分基本上是照搬casa大神的設(shè)計,這里我延伸了一個多環(huán)境的設(shè)計,小的項目一般都是一個服務(wù)器,但是像淘寶之類的項目一個服務(wù)器顯然是不可能的,多個服務(wù)器的設(shè)計還是非常普遍的。根據(jù)一個枚舉變量通過ServerFactory單例生成獲取對應(yīng)的服務(wù)器配置。
1.服務(wù)器環(huán)境
標(biāo)準(zhǔn)的APP是有4個環(huán)境的,開發(fā)、測試、預(yù)發(fā)、正式,特別是服務(wù)器的代碼,不能說所有的代碼更改都在正式環(huán)境下,應(yīng)該從開發(fā)->測試->預(yù)發(fā)->正式做代碼的更新,開發(fā)就是新需求和優(yōu)化的時候的更改,測試就是提交給測試人員后的更改,這個時候更改是在一個新的分支上,完成后要和合并到測試分支上并合并到開發(fā)分支上,預(yù)發(fā)這時候的變動就比較小了,一般會在測試人員完成后發(fā)布給全公司的人來測試,有問題了才會更改,更改后同樣合并到開發(fā)分支,正式則是線上發(fā)布版本的緊急BUG修復(fù),修改完后同樣合并到開發(fā)分支上。所以開發(fā)分支是一直都是最新的。在此基礎(chǔ)上可能會有其他的環(huán)境,比如hotfix環(huán)境,自定義的h5/后臺本地調(diào)試的環(huán)境。
客戶端同樣存在這些環(huán)境,并且要提供切換的入口。
在我的demo中提供了兩套設(shè)置,一套是第一次安裝應(yīng)用的初始化環(huán)境(宏定義),另外是手動切換環(huán)境的設(shè)置(枚舉EnvironmentType)。這里有一個比較繞的邏輯,宏定義的正式環(huán)境設(shè)置高于手動切換環(huán)境設(shè)置,手動切換環(huán)境設(shè)置高于宏定義其他環(huán)境。
所以當(dāng)宏定義正式環(huán)境存在的時候是不能手動切換環(huán)境的,用于普通用戶的發(fā)布版本,但是其他宏定義環(huán)境時是可以切換到正式環(huán)境的。
另外手動切換自定義的環(huán)境是在基類中實現(xiàn)的,而其他的環(huán)境配置是在協(xié)議中實現(xiàn)的,這就和其他環(huán)境地址的配置不統(tǒng)一了。
可以這樣理解,這里的基類是為了提供已返回值,協(xié)議是為了返回值的靈活,既然自定義環(huán)境的地址配置不需要靈活性,自然是放在基類好。思路是大方向,實現(xiàn)是靈活的,如果非要放在協(xié)議中實現(xiàn)也無不可以,無非是賦值粘貼幾次一樣的代碼,但是一模一樣的代碼是我最不喜歡看到的,所以就放在基類了。如果有更好的解決方案歡迎提供。
2.擴(kuò)展性
model提供的是高擴(kuò)展性,針對不同的不服務(wù)器添加更多的配置,比如加密方法,比如數(shù)據(jù)解析方法...前面提到了,統(tǒng)一的規(guī)范有的時候不是一時半會就能做好的,兼容就成了需求,這個時候不同服務(wù)器的個性化設(shè)置就可以在協(xié)議中聲明并實現(xiàn)了,基類提供返回值就好。
網(wǎng)絡(luò)層數(shù)據(jù)傳遞(請求和返回)
網(wǎng)絡(luò)層數(shù)據(jù)傳遞
Client、BaseEngine/DataEngine、RequestDataModel數(shù)據(jù)傳遞
網(wǎng)絡(luò)請求的發(fā)生在我理解中分兩步,一步是數(shù)據(jù)的整理,一步是生成Request并發(fā)起請求,基于這個思想我拆分出了Client和Engine,然后又把URLRequestGenerator從Client中拆分出來,Engine拆分出了下層的BaseEngine和面向不同業(yè)務(wù)的DataEngine。
而從BaseEngine到Client,再到URLRequestGenerator是要做數(shù)據(jù)傳遞的,請求參數(shù)和返回參數(shù),所以又有了RequestDataModel。
RequestDataModel
可以看出來RequestDataModel屬性都是網(wǎng)絡(luò)請求發(fā)起和返回的必要參數(shù),這樣做的好處真的是太大了,不知道大家有沒有這樣的場景:因為請求參數(shù)的不同做了好多方法接口暴露出去,最后調(diào)起的還是同一個方法,而且一旦方法寫的多了,最后連應(yīng)該調(diào)用哪個方法都不知道了。我就遇到過,所以現(xiàn)在我的網(wǎng)絡(luò)請求調(diào)起是這樣的:
生成NSURLRequest是這樣的:
可以看到我的demo里面的YAAPIClient類和YAAPIURLRequestGenerator類方法至少,方法少就意味著邏輯簡單明了,方便閱讀,兩個類的代碼行數(shù)都是120行,120行實現(xiàn)了網(wǎng)絡(luò)請求的發(fā)起和著陸,你能想象嗎?
另外RequestDataModel帶來的另外一個好處就是高擴(kuò)展性,你有沒有遇到網(wǎng)絡(luò)層需要添加刪除一個參數(shù)導(dǎo)致調(diào)用方法修改了,然后很多地方都要修改方法?用RequestDataModel只需要添加刪除參數(shù)就行了,只需要改方法體,這個改方法體和同時改方法名方法體是完全兩個工作量。哈哈,有點(diǎn)賣虎皮膏藥的感覺。這個的確是我的得意創(chuàng)新點(diǎn)。
Client
Client做兩個操作,一個是生成NSURLRequest,一個是生成NSURLSessionDataTask并發(fā)起,另外還要暴露取消操作給Engine。
URLRequestGenerator是生成NSURLRequest,URLRequestGenerator會對dataModel進(jìn)行加工解析,生成對應(yīng)服務(wù)器的NSURLRequest。
然后Client通過NSURLRequest生成NSURLSessionDataTask。
Client和URLRequestGenerator都是單例。
取消接口參考了casa大神的設(shè)計,使用NSNumber *requestID來做task的綁定,就不多做介紹了。
BaseEngine/DataEngine
Engine或者說是APIManager在我的設(shè)計中既不是離散的也不是集約的。
casa大神的理論
集約型API調(diào)用其實就是所有API的調(diào)用只有一個類,然后這個類接收API名字,API參數(shù),以及回調(diào)著陸點(diǎn)(可以是target-action,或者block,或者delegate等各種模式的著陸點(diǎn))作為參數(shù)。然后執(zhí)行類似startRequest這樣的方法,它就會去根據(jù)這些參數(shù)起飛去調(diào)用API了,然后獲得API數(shù)據(jù)之后再根據(jù)指定的著陸點(diǎn)去著陸。比如這樣:
離散型API調(diào)用是這樣的,一個API對應(yīng)于一個APIManager,然后這個APIManager只需要提供參數(shù)就能起飛,API名字、著陸方式都已經(jīng)集成入APIManager中。比如這樣:
各自的優(yōu)點(diǎn)就不說了,但是由此延伸出幾個問題:
1.參數(shù)的傳遞使用字典對于網(wǎng)絡(luò)層來說是不可知的,而且業(yè)務(wù)層需要去關(guān)注接口字段的變化,其實是沒有必要的。
2.離散型API會造成Manager大爆炸。
3.集約型會造成取消操作不方便。
4.取消操作并不是每個接口必須的,如果寫成部分離散的部分集約的,代碼的整體結(jié)構(gòu)...我是個有強(qiáng)迫癥的人,看不得這樣的代碼。
所以我的設(shè)計主要就解決了上面的這些問題:
1.面向業(yè)務(wù)層的DataEngine只傳遞必要的參數(shù)進(jìn)來,不使用字典,比如:
control暫時先不管,是做自動取消的,后面再介紹。
searchKey就是搜索的關(guān)鍵字
在調(diào)用的時候就是這樣
2.我按業(yè)務(wù)層來劃分DataEngine,比如BBSDataEngine、ShopDataEngine、UserInforDataEngine...每個DataEngine里面包含各自業(yè)務(wù)的所有網(wǎng)絡(luò)請求接口,這樣就不會出現(xiàn)DataEngine大爆炸,像我們的項目有300多個接口,拆分后有十幾個DataEngine,如果使用離散型API設(shè)計,那畫面太美我不敢看?
3.BaseEngine提供取消操作
每個接口生成一個BaseEngine實例,持有Client返回的requestID,所以就可以做取消操作,簡單的使用場景。
4.返回的YABaseDataEngine實例ViewController不是必須持有的,當(dāng)有需要取消操作的時候再去持有就行了。
這樣的設(shè)計就集成了集約型和離散型的有點(diǎn),又解決了集約型和離散型的缺點(diǎn)。
網(wǎng)絡(luò)請求怎么自動取消
當(dāng)一個頁面的請求正在天上飛的時候,用戶等了好久不耐煩了,小手點(diǎn)了個back,然后ViewController被pop被回收。此時請求的著陸點(diǎn)就沒了。這是很危險的情況,著陸點(diǎn)要是沒了,就很容易crash的。
casa大神說在BaseDataEngine的dealloc里面做取消網(wǎng)絡(luò)請求操作,我也是這樣想的,但是casa大神說要把BaseDataEngine綁定給ViewController,當(dāng)ViewController銷毀時BaseDataEngine也就跟著銷毀了,這樣我也是同意的,但是要讓我不管什么情況都要給ViewController添加BaseDataEngine變量來保存BaseDataEngine這是我萬萬不能接受的,而且有的ViewController會發(fā)起兩三種網(wǎng)絡(luò)請求,難道要我添加兩三個變量?代碼入侵太大,所以這里偷偷使用了一個巧,使用了runtime,給ViewController添加一個字典,來保存requestID和BaseDataEngine,這樣對于ViewController來說就不是必須要寫變量來持有BaseDataEngine了,所以就出現(xiàn)了上面的DataEngine里面要把control傳遞進(jìn)來的樣子。
在發(fā)起請求的時候進(jìn)行綁定
在請求完成的時候進(jìn)行刪除
公司上個大神的做法
雖然使用control的做法很方便,但是如果說要把現(xiàn)在已有的接口都添加一個control字段的工作量也是很大的,如果已有的接口是使用字典傳遞給DataEngine的,這里給大家一個公司上個大神的做法,使用內(nèi)存地址,將內(nèi)存地址添加到字典中去,用內(nèi)存地址做key綁定,也是可以的。如果是像我這樣直接把關(guān)鍵參數(shù)傳遞過來的,不是用的字典,就不行了。
網(wǎng)絡(luò)層錯誤處理
說實話,錯誤處理該放在按個地方我也是糾結(jié)了好久,也和公司同事討論了好久,最終定下來了一套方案,僅供大家參考。
我們將錯誤處理分為兩個步驟,一個是錯誤解析,一個是錯誤的UI展示。
大家可以看到我設(shè)計的接口返回數(shù)據(jù)是標(biāo)準(zhǔn)的id data, NSError *error,所以我的想法是Client就把error處理好,不管你是網(wǎng)絡(luò)超時錯誤也好,或者是數(shù)據(jù)格式不正確也好,都error解析完整,把code錯誤碼定義好,上層根據(jù)需要通過code來做具體的UI展示,因為有的界面的錯誤需要用戶的點(diǎn)擊確認(rèn),有的頁面的錯誤只是一閃而過的提示框,把error交給BaseEngine或者DataEngine來處理errorUI,所以我定義了一套errorUI的枚舉,當(dāng)BaseEngine拿到error的時候就去做錯誤的展示。
總結(jié)
架構(gòu)的設(shè)計更多的是思路,我希望的是大家能通過我們提供的思路取其精華去其糟粕,總會設(shè)計會最適合你的項目的架構(gòu)的。
另外我的這套設(shè)計存在的爭議的點(diǎn)可能會有很多,有一部分我已經(jīng)在文中提到了,如果大家有什么其他的想法我們再討論。
1.關(guān)于block
對于block和delegate的選擇,我更傾向于block,只有一個原因,因為block的結(jié)構(gòu)更方便閱讀,這一個優(yōu)點(diǎn)我覺得足以秒殺他所有的缺點(diǎn),可以這樣說,我現(xiàn)在的項目基本上很少用到delegate了。
什么時候自定義delegate?就是當(dāng)你的不同時期的回調(diào)超過2次的時候(不包含2次),3次回調(diào)就看情況了,如果要處理的邏輯比較少就使用block,多的話就使用delegat,一旦超過3次,基本上就不會考慮block,希望大家也不要對block存在偏見,延遲生命周期什么的都是可以解決的,一個宏定義就解決了,順便給出strongSelf,如果這么方便的宏都不愿意使用,那是真的不適合用block了,誰也救不了你。
2.交付什么樣的數(shù)據(jù)給ViewController?是model還是data
這個有什么好爭議的嗎?有DataEngine在,交付什么樣的數(shù)據(jù)還不是你說了算。
底層的BaseEngine和Client當(dāng)然還是data比較合適,到了DataEngine層,你想交付什么樣的數(shù)據(jù)就交什么樣的數(shù)據(jù),可以看業(yè)務(wù)層的需求,有的接口根本就不包含model,你非要統(tǒng)一所有的接口都返回model這不是扯淡嗎,所以我的建議是根據(jù)接口的實際情況來,統(tǒng)一規(guī)范,我們的設(shè)計因為有些接口是不需要model的,以后就統(tǒng)一返回data。
3.優(yōu)化
我的這套設(shè)計只是基本思路,還有很多優(yōu)化的點(diǎn),我知道。
這部分就是各顯神通的地方了,不是我藏私,而是現(xiàn)在的項目對于網(wǎng)絡(luò)層沒有太多的優(yōu)化點(diǎn),所以我也沒做太多,做的部分敏感代碼太多,實在是沒辦法拆出來,不過可以告訴大家一個小的優(yōu)化點(diǎn),errorUI的處理可以考慮做成隊列,比如需要用戶點(diǎn)擊確定的彈出框,而且內(nèi)容都是一樣的,放在隊列里面只顯示一次就好。
4.為什么業(yè)務(wù)層沒有使用RequestDataModel
model就是對象,下層主要是用來做數(shù)據(jù)傳遞的,用model沒有問題;而向上到業(yè)務(wù)層的時候,更多的理念是方法的調(diào)用,而且方法的定義更有針對性,這個時候用model就不合適了。就好像超市一樣,進(jìn)貨的時候是使用集裝箱拉貨的,所有的東西都裝在一起,當(dāng)?shù)焦衽_的時候就會一個個的分類擺好。
核心關(guān)注:拓步ERP系統(tǒng)平臺是覆蓋了眾多的業(yè)務(wù)領(lǐng)域、行業(yè)應(yīng)用,蘊(yùn)涵了豐富的ERP管理思想,集成了ERP軟件業(yè)務(wù)管理理念,功能涉及供應(yīng)鏈、成本、制造、CRM、HR等眾多業(yè)務(wù)領(lǐng)域的管理,全面涵蓋了企業(yè)關(guān)注ERP管理系統(tǒng)的核心領(lǐng)域,是眾多中小企業(yè)信息化建設(shè)首選的ERP管理軟件信賴品牌。
轉(zhuǎn)載請注明出處:拓步ERP資訊網(wǎng)http://www.ezxoed.cn/
本文標(biāo)題:iOS網(wǎng)絡(luò)層架構(gòu)設(shè)計分享
本文網(wǎng)址:http://www.ezxoed.cn/html/support/11121519305.html