當工期比較緊的時候,專案開發中會經常出現移動端等待後端介面資料的情形,不但耽誤專案進度,更讓人有種無奈的絕望。所以在開發中,我們常常自己做些假資料,以方便開發和UI除錯。然而做假資料方法不同,效率和安全性都各不同,有時稍有不慎,還會產生很大的bug。因此本文擬結合我在貝聊的開發經驗,講一講我們組在iOS開發中曾經用過的做假資料的方法及其優劣。
示例專案
為方便下文的說明,本文主要以貝聊家長版app發現首頁的熱門帖子列表的實現為例。熱門帖子列表的樣式如下圖:
這是比較常見的列表,用常用的UITableView
實現即可,但需要自定義一個的UITableViewCell
的子類ExploreTableViewCell
。專案中,ExploreTableViewCell
並沒有用xib實現(因為xib日後不好修改,且程式碼複用性差),而是通過SnapKit
用純程式碼布的局,具體的佈局程式碼大致如下:
import UIKit
import SnapKit
class ExploreTableViewCell: UITableViewCell {
let thumbnailImageView: UIImageView
let titleLabel: UILabel
let avatarImageView: UIImageView
let authorNameLabel: UILabel
let viewCountLabel: UILabel
let commentCountLabel: UILabel
//...其他屬性
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
//建立view
thumbnailImageView = UIImageView()
titleLabel = UILabel()
avatarImageView = UIImageView()
authorNameLabel = UILabel()
viewCountLabel = UILabel()
commentCountLabel = UILabel()
//...其他view的建立
super.init(style: style, reuseIdentifier: reuseIdentifier)
//設定view
titleLabel.numberOfLines = 2
titleLabel.textColor = UIColor.black
titleLabel.snp.makeConstraints { (make) -> Void in
make.left.equalTo(thumbnailImageView.snp.right).offset(15)
make.right.equalTo(contentView.snp.right)
make.top.equalTo(contentView.snp.top)
}
//...其他view的設定
}
//...其他業務程式碼
}複製程式碼
原始碼中寫死
原始碼中寫死資料是最便捷的假資料做法,專案很趕時,為最快速的看到UI效果,一般都會採取這種假資料方式。比如在上述熱門帖子列表示例專案中,為檢視整個ExploreTableViewCell
的佈局效果,在titleLabel
等subview
的設定位置,直接寫死假資料。
//...其他程式碼
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
//...其他初始化程式碼
//寫死的假資料程式碼
titleLabel.text = "這是一個標題這是一個標題這是一個標題這是一個標題這"
thumbnailImageView.image = UIImage(named:"sampleImage")
avatarImageView.image = UIImage(named:"sampleImage")
authorNameLabel.text = "作者名"
viewCountLabel.text = "1000"
commentCountLabel.text = "1000"
//...其他初始化程式碼
}
//...其他程式碼複製程式碼
原始碼中寫死假資料雖然方便,但稍有不慎就容易直接上線上環境(因為測試在測試時一般都會有資料,假資料被遮蓋了),演變成一個有可能非常嚴重也有可能很輕的bug(貝聊就切實出現過這樣的bug,而且還是個影響廣泛的大bug),為安全起見,所有寫死的假資料都應該包在條件編譯巨集內。
//...其他程式碼
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
//...其他初始化程式碼
//寫死的假資料程式碼,包裹在條件編譯巨集內
#if DEBUG
titleLabel.text = "這是一個標題這是一個標題這是一個標題這是一個標題這"
thumbnailImageView.image = UIImage(named:"sampleImage")
avatarImageView.image = UIImage(named:"sampleImage")
authorNameLabel.text = "作者名"
viewCountLabel.text = "1000"
commentCountLabel.text = "1000"
#endif
//...其他初始化程式碼
}
//...其他程式碼複製程式碼
包在條件編譯巨集內,就可以保證不汙染正式環境的程式碼,從而保證安全性。
利用單元測試的網路請求stub做假資料
在原始碼中寫死假資料,有以下三個缺點,
- 汙染原始碼
假資料寫在原始碼中,即使用巨集包裹起來,只是保證了一定的安全性,但依然汙染了原始碼,如果上線前忘了把假資料程式碼移除,它一直會殘留在原始碼中,而且還會一直影響DEBUG
環境的除錯。 - 假資料散落四處,無法集中管理
本文示例程式碼的假資料雖然寫在一塊,但在實際開發中,並不是所有的UI程式碼都在一個檔案中。即使在一個檔案內,往往各個屬性的初始化和設定也不在一個方法內。程式碼一多,基本很難管理。 - 扭曲了資料的正確流通
正確的資料產生方式,應該是發一個網路請求,然後把請求回來的資料轉成model
,最後通過model
給各個UI元件填充資料。而在原始碼中寫死假資料,直接打亂了資料的正確流通,這會使得整個開發的邏輯是顛倒的,不但使開發更容易出bug,而且邏輯流的切換帶來的開發效率和開發感受都很差。
較好的假資料方式,應該儘可能的不汙染原始碼,不擾亂正常的資料流通,而且能集中管理。在研究單元測試時,我無意中發現stub某個頁面請求資料的網路請求即可達到這種完美的假資料效果。
首先按正常的流程開發整個功能,(在開發中,我總是傾下於先建立Model,而不是先寫UI)
- 建立Model
- 建立ViewController
- 建立View等UI元素
- 在ViewController中完成網路請求的發起,並完成從網路資料到Model的轉換
- 應用Model填充UI
整個功能開發按照有真實網路請求進行,但事實上並沒有網路請求,因為後臺並未搭好,沒關係,先按照後臺給出的介面和資料格式定義,建立一個本地JSON檔案。對於本文的示例(假定只有列表資料)來說,檔名暫為hotTopics.json
,內容大致如下(貝聊發現首頁實際上有很多其他元素,網路請求返回的JSON也比這個複雜的多):
{
hotTopics: [
{
"title": "這是一個標題這是一個標題這是一個標題",
"thumbnail": "https://api.beiliao.com/explore/image/fdlafjlfp34523.jpg",
"author": "小黃老師",
"authorAvatar": "https://api.beiliao.com/explore/image/fdlafjlfp34523.jpg",
"commentCount": 1000,
"viewCount": 3000
},
{
"title": "這是另外一個標題這是另外一個標題這是另外一個標題",
"thumbnail": "https://api.beiliao.com/explore/image/fdla32131fjlfp34523.jpg",
"author": "小李老師",
"authorAvatar": "https://api.beiliao.com/explore/image/fdl232afjlfp34523.jpg",
"commentCount": 1030,
"viewCount": 3400
}
]
}複製程式碼
然後在ViewController
中stub
本ViewController
中所有的網路請求,我在開發中用的是OHHTTPStubs,大致的程式碼如下:
class ExploreViewController: UITableViewController {
//...其他程式碼
override func viewDidLoad() {
super.viewDidLoad()
//...其他程式碼
#if DEBUG
stubRequests()
#endif
//...其他程式碼
}
//...其他程式碼
func stubRequests() {
stub(isPath("/explore/hotTopics")) { _ in
let stubPath = OHPathForFile("hotTopics.json", type(of: self))
return fixture(filePath: stubPath!, headers: ["Content-Type":"application/json"])
}
}
}複製程式碼
注意所建立的JSON檔案一定要加到專案目錄中。新增完上述程式碼後,path為/explore/hotTopics
的網路請求將被stub,返回的資料將是所指定JSON檔案中的資料,這樣就跟真實的網路請求沒有任何的區別了。而且利用OHHTTPStubs還可以模擬網路請求失敗、網路請求超時以及throttle
等各種網路請求狀態,從而更全面的除錯UI和整個功能。
利用stub做假資料可以真實的做到基本不汙染程式碼、集中管理和完全真實的資料流通流程,與在原始碼中寫死這種方式相比,近乎完美。然而當你真正用過一段時間後,你會發現,這種方式還是有一個致命的缺點和一個不那麼重要的缺點。
- 不適合做UI除錯
因為每改動一次資料,都需要重新編譯,而iOS編譯是很慢的,尤其是Swift。而要想做UI除錯,頻繁的改動資料,檢視各種邊界條件下的UI是必然的。 - 還是汙染了程式碼
雖然相較上一種方法,汙染非常小,但或多或少還是有汙染的,有強迫症的人是受不了的,而且有時測試說是個bug(測試包一般是BETA環境),你build一下發現資料是假資料,不是網路請求的資料,還需要找到stub網路請求的位置,然後把程式碼註釋了,也是極其的煩人的。
如果能做到每改動一下資料,然後重新整理一下就可以看到了,像網頁一樣,而且真的一點都不汙染程式碼,那就是完美的解決方案。
動態注入
如果只是想做到,每改動一下資料,然後重新整理以下就可以看到了,像網頁一樣,Xcode的動態注入是可以的,現在比較流行的是 injectionforxcode和dyci-main兩個庫。利用單元測試的網路請求stub做假資料,然後結合動態注入,理論上應該可以做到實時重新整理,但事實上injectionforxcode和dyci-main的體驗都是很糟糕的,時靈時不靈,我用過兩次後,基本就不想碰了,我寧願編譯慢一點,當然我從來沒有用動態注入去做假資料的實時重新整理,但我覺得應該是個方案。
但這個方案即使可行,也還是會汙染程式碼,並不算特別徹底的方案。真正徹底的方案,與用stub攔截網路請求的思路相同,只是要將網路請求的攔截放到整個APP外,有兩個方案可行。
本地自己搭個伺服器
第一種就是本地自己搭個伺服器,然後把開發時需要攔截的網路請求地址改為自己搭建的伺服器地址,然後返回自己自定義的JSON資料。但這種方式也有三個缺點:
- 有一定門檻
雖然搭建伺服器是很簡單的事,並不是所有人都會,也是需要一定的學習成本的。 - 還是要修改原始碼中網路請求的地址
這雖然已經把原始碼汙染降到最低了,但畢竟還是有。 - 要想模擬不同的網路狀態,還需去修改伺服器的程式碼,不方便。
綜合起來這種方案價效比並不高,但確實有一定的趣味性,畢竟自己折騰東西嘛。
網路代理
第二種就是利用現有的網路代理軟體,直接攔截對應的網路請求,然後返回本地寫好的JSON資料。我最終採用的這種方案(因為我嫌配置伺服器麻煩)。將APP中所有的網路請求都代理給網路代理,然後指定特定的網路請求返回本地JSON資料。這種方案的好處不言而喻,
- 真正的不汙染原始碼
原始碼中任何程式碼都不用動,真正做到了乾淨綠色無汙染。 - 攔截起來很方便
許多網路代理軟體,都自帶攔截甚至改寫網路請求的功能,所以啟動攔截功能很方便。 - 方便除錯
網路代理一般都有改變一個網路請求狀態的功能,可以輕鬆實現返回網路錯誤、網路超時和延遲網路請求等不同的網路請求狀態的功能,非常方便。
我常用的網路代理就是Charles,相信大家都有耳聞。Charles有個maplocal
的功能(在工具選單下),如圖:
mapLocal的設定也很簡單,在Location一欄填上所要攔截的網路請求的host、path或者完整的URL,然後在LocalPath一欄選擇對應的本地JSON檔案即可,記得勾選啟動。
這樣簡單的設定後,所指定的網路請求都會返回本地對應的JSON檔案資料。然後你將發現這種假資料之完美,簡直讓人窒息。
編譯後,如果想改變一個資料,看看對應的UI,直接去改變本地JSON檔案,然後下來重新整理一下,你會發現顯示的資料就是剛剛改動的資料,簡直要感動哭了。
但事實上這種方式還是有一個小小的缺點,即Charles與Shadowsocks不能同時開著,因為Charles不支援父代理。搞程式設計開發,為方便查閱資料,翻牆軟體會一隻開著,但這樣Charles就不能開著,想用的話,又要先退出Shadowsocks,再開啟Charles,這讓我很頭疼。最後只能在真正寫完所有的邏輯和UI後,關閉Shadowsocks,開啟Charles,集中除錯。
=============2017-03-04更新開始===============
文章發出後,不少讀者反饋,
- Charles與Shadowsocks可以共存
具體怎麼共存,我還有待研究,後續文章再做補充 - 假資料的資料應該能隨機生成
具體由json server、mock.js和Charles三者結合完成,有意思,值得研究,待後續文章詳細補充。
=============2017-03-04更新結束===============
總結
一路試下來,其實只有第一種原始碼中寫死和最後一種網路代理兩種假資料方式最常用,雖然第一種缺點最多,但方便快捷,最後一種雖無任何缺點,但啟動還是有點麻煩。
寫了這麼多,還是希望對大家有所啟發。
歡迎關注我的微博:輕墨lightink
文章同步釋出在貝聊知乎