1. 程式人生 > >APP和服務端-架構設計(二)

APP和服務端-架構設計(二)

1. App架構設計經驗談:介面的設計

App與伺服器的通訊介面如何設計得好,需要考慮的地方挺多的,在此根據我的一些經驗做一些總結分享,旨在拋磚引玉。

1.1 安全機制的設計

現在,大部分App的介面都採用RESTful架構,RESTFul最重要的一個設計原則就是,客戶端與伺服器的互動在請求之間是無狀態的,也就是說,當涉及到使用者狀態時,每次請求都要帶上身份驗證資訊。實現上,大部分都採用token的認證方式,一般流程是:

1. 使用者用密碼登入成功後,伺服器返回token給客戶端;
2. 客戶端將token儲存在本地,發起後續的相關請求時,將token發回給伺服器;
3. 伺服器檢查token的有效性,有效則返回資料,若無效,分兩種情況:

  • token錯誤,這時需要使用者重新登入,獲取正確的token
  • token過期,這時客戶端需要再發起一次認證請求,獲取新的token

然而,此種驗證方式存在一個安全性問題:當登入介面被劫持時,黑客就獲取到了使用者密碼和token,後續則可以對該使用者做任何事情了。使用者只有修改密碼才能奪回控制權。

如何優化呢?第一種解決方案是採用HTTPS。
HTTPS在HTTP的基礎上添加了SSL安全協議,自動對資料進行了壓縮加密,在一定程式可以防止監聽、防止劫持、防止重發,安全性可以提高很多。不過,SSL也不是絕對安全的,也存在被劫持的可能。另外,伺服器對HTTPS的配置相對有點複雜,還需要到CA申請證書,而且一般還是收費的。而且,HTTPS效率也比較低。一般,只有安全要求比較高的系統才會採用HTTPS,比如銀行。而大部分對安全要求沒那麼高的App還是採用HTTP的方式。

我們目前的做法是給每個介面都添加簽名。
給客戶端分配一個金鑰,每次請求介面時,將金鑰和所有引數組合成源串,根據簽名演算法生成簽名值,傳送請求時將簽名一起傳送給伺服器驗證。類似的實現可參考OAuth1.0的簽名演算法。這樣,黑客不知道金鑰,不知道簽名演算法,就算攔截到登入介面,後續請求也無法成功操作。不過,因為簽名演算法比較麻煩,而且容易出錯,只適合對內的介面。如果你們的介面屬於開放的API,則不太適合這種簽名認證的方式了,建議還是使用OAuth2.0的認證機制。
我們也給每個端分配一個appKey,比如Android、iOS、微信三端,每個端分別分配一個appKey和一個金鑰。沒有傳appKey的請求將報錯,傳錯了appKey的請求也將報錯。這樣,安全性方面又加多了一層防禦,同時也方便對不同端做一些不同的處理策略。
另外,現在越來越多App取消了密碼登入,而採用手機號+簡訊驗證碼的登入方式,我在當前的專案中也採用了這種登入方式。這種登入方式有幾種好處:

  1. 不需要註冊,不需要修改密碼,也不需要因為忘記密碼而重置密碼的操作了;
  2. 使用者不再需要記住密碼了,也不怕密碼洩露的問題了;
  3. 相對於密碼登入其安全性明顯提高了。

1.2 介面資料的設計

介面的資料一般都採用JSON格式進行傳輸,不過,需要注意的是,JSON的值只有六種資料型別:

  • Number:整數或浮點數
  • String:字串
  • Boolean:true 或 false
  • Array:陣列包含在方括號[]中
  • Object:物件包含在大括號{}中
  • Null:空型別

所以,傳輸的資料型別不能超過這六種資料型別。
我們曾經試過傳輸Date型別
它會轉為類似於"2016年1月7日 09時17分42秒 GMT+08:00"這樣的字串,這在轉換時會產生問題,不同的解析庫解析方式可能不同,有的可能會轉亂,有的可能直接異常了。要避免出錯,必須做特殊處理,自己手動去做解析。為了根除這種問題,最好的解決方案是用毫秒數表示日期。

還出現過字串的"true"和"false",或者字串的數字,甚至還出現過字串的"null",導致解析錯誤,尤其是"null",導致App奔潰
後來查了好久才查出來是該問題導致的。這都是因為服務端對資料沒處理好,導致有些資料轉為了字串。所以,在客戶端,也不能完全信任服務端傳回的資料都是對的,需要對所有異常情況都做相應處理。

伺服器返回的資料結構,一般為:

{ code:0, message: "success", data: { key1: value1, key2: value2, ... }}
  • code: 狀態碼,0表示成功,非0表示各種不同的錯誤
  • message: 描述資訊,成功時為"success",錯誤時則是錯誤資訊
  • data: 成功時返回的資料,型別為物件或陣列

不同錯誤需要定義不同的狀態碼,屬於客戶端的錯誤和服務端的錯誤也要區分,比如1XX表示客戶端的錯誤,2XX表示服務端的錯誤。這裡舉幾個例子:

  • 0:成功
  • 100:請求錯誤
  • 101:缺少appKey
  • 102:缺少簽名
  • 103:缺少引數
  • 200:伺服器出錯
  • 201:服務不可用
  • 202:伺服器正在重啟

錯誤資訊一般有兩種用途:

  1. 一是客戶端開發人員除錯時看具體是什麼錯誤;
  2. 二是作為App錯誤提示直接展示給使用者看。
    主要還是作為App錯誤提示,直接展示給使用者看的。所以,大部分都是簡短的提示資訊。

data欄位只在請求成功時才會有資料返回的。
資料型別限定為物件或陣列,當請求需要的資料為單個物件時則傳回物件,當請求需要的資料是列表時,則為某個物件的陣列。這裡需要注意的就是,不要將data傳入字串或數字,即使請求需要的資料只有一個,比如token,那返回的data應該為:

// 正確
data: { token: 123456 }
// 錯誤
data: 123456

1.3 介面版本的設計

介面不可能一成不變,在不停迭代中,總會發生變化。介面的變化一般會有幾種:

  • 資料的變化,比如增加了舊版本不支援的資料型別
  • 引數的變化,比如新增了引數
  • 介面的廢棄,不再使用該介面了

為了適應這些變化,必須得做介面版本的設計。實現上,一般有兩種做法:

  • 每個介面有各自的版本,一般為介面添加個version的引數。

大部分情況下會採用第一種方式,當某一個介面有變動時,在這個介面上疊加版本號,併兼容舊版本。App的新版本開發傳參時則將傳入新版本的version。
如果整個介面系統的根基都發生變動的話,比如微博API,從OAuth1.0升級到OAuth2.0,整個API都進行了升級。
有時候,一個介面的變動還會影響到其他介面,但做的時候不一定能發現。因此,最好還要有一套完善的測試機制保證每次介面變更都能測試到所有相關層面。

2. App架構設計經驗談:技術選型

當你做架構設計時,必然會面臨技術選型的抉擇,不同的技術方案,架構也可能完全不同。有哪些技術選型需要做決策呢?比如,App是純原生開發,還是Web App,抑或Hybrid App?iOS開發,語言上是選擇Objective-C還是Swift?架構模式用MVC,還是MVP,或者MVVM?下面根據我的一些經驗對某些方面做點總結分享。

2.1 原生/H5

關於用原生好,還是用H5好的爭論從沒間斷過。但我覺得,脫離了實際場景來討論孰好孰壞意義不大。就說我們目前正在做的專案,先說明下背景:

  1. 不止要做Android和iOS App,也要做微信公眾號;
  2. H5人員缺乏,只有一兩個兼職的可用,而且不可控因素很高;
  3. 我們對原生比較熟;
  4. 開發時間只有半個月。

首先,需求上來說,大部分頁面用H5實現,可以減少很多工作量。但因為不可控因素太高,而時間又短,風險太大。而我們對原生比較熟,開發效率比較高,很多東西我也控制得了,風險相對比較低。而且,我們的主推產品是App,微信屬於輔助性產品,所以,微信要求也沒那麼高。因此,我決定以原生為主,H5為輔,App大部分頁面用原生完成,小部分用WebView載入H5。

另外,WebView載入H5也有兩種模式,一種是載入伺服器的H5頁面,一種是載入本地的H5頁面。載入伺服器的H5頁面比較簡單,WebView只要load一下URL就可以了。載入本地的H5頁面,則需要將H5檔案存放在本地,包括關聯的CSS和JS檔案。這種方式相對比較複雜,不過,載入速度會比第一種快很多。我們當前專案基於上面考慮,只能選擇第一種方案。

如果人員和時間資源充足的話,那又如何選型呢?毫無疑問,我會以H5為主,微信和App都有的頁面統一用H5,App專有的部分,比如導航欄、標題欄、登入等,才用原生實現。另外,WebView裡的H5有點選事件時,也許是URL連結,也許是呼叫JS的,都不會讓它直接在該WebView裡做跳轉,需要攔截下來做些原生處理後跳轉到一個新的原生頁面,原生頁面也許嵌入另一個WebView,用來展示新的H5頁面。這是簡單的例子,關於Hybrid App詳細的設計,以後再講。另外,關於H5,絕對是大趨勢,強烈建議所有App開發人員都去學習。

2.2 Objective-C/Swift

我在專案中選擇了Swift,主要基於三個原因:

  1. Swift真的很簡潔,生產效率很高;
  2. Swift取代Objective-C是必然的趨勢;
  3. 目前iOS只有我一個人開發,不需要顧慮到團隊裡沒人懂Swift。

如果你的團隊裡沒人懂Swift,那還是乖乖用Objective-C吧;如果有一兩個懂Swift的,那可以混合開發,並讓不懂的人儘快學會Swift;如果都懂了,不用想了,直接上Swift吧。

當語言上選擇了Swift,相應的一些第三方庫也面臨著選型。比如,依賴庫管理,Objective-C時代大部分用CocoaPods,Swift時代,我更喜歡Carthage。Carhage是用Swift寫的,和CocoaPods相比,輕耦合,也更靈活。我個人也不太喜歡CocoaPods,使用起來比較麻煩,耦合性也較高,我使用過程中也經常出問題,而且還總是不知道該怎麼解決,要移除時也是非常麻煩。

再推薦幾個關於Swift的第三方庫:

  1. Alamofire:Swift版本的網路基礎庫,和AFNetworking是同一個作者
  2. AlamofireImage:基於Alamofire的圖片載入庫
  3. ObjectMapper:Swift版本的Json和Model轉換庫
  4. AlamofireObjectMapper:Alamofire的擴充套件庫,結合了ObjectMapper,自動將JSON的Response資料轉換為了Swift物件

2.3 MVC/MVP/MVVM

先分別簡單介紹下這三個架構模式吧:

  • MVC:Model-View-Controller,經典模式,很容易理解,主要缺點有兩個:

    1. View對Model的依賴,會導致View也包含了業務邏輯;
    2. Controller會變得很厚很複雜。
  • MVP:Model-View-Presenter,MVC的一個演變模式,將Controller換成了Presenter,主要為了解決上述第一個缺點,將View和Model解耦,不過第二個缺點依然沒有解決。

  • MVVM:Model-View-ViewModel,是對MVP的一個優化模式,採用了雙向繫結:View的變動,自動反映在ViewModel,反之亦然。

架構模式上,我不會推崇說哪種模式好,每種模式都各有優點,也各有極限性。越高階的模式複雜性越高,實現起來也越難。最近火熱的微服務架構,比起MVC,複雜度不知增加了多少倍。

我在實際專案中思考架構時,也不會想著要用哪種模式,我只思考現階段,以現有的人力資源和時間資源,如何才能更快更好地完成需求,適當考慮下如何為後期擴充套件或重構做準備。就說我前段時間分享的Android專案重構之路系列中講的那個架構,確切地說,都不屬於上面三種架構模式之一。

寫在最後

技術選型,決策關鍵不在於每種技術方案的優劣如何,而在於你團隊的水平、資源的多寡,要根據實際情況選擇最適合你們當前階段的架構方案。當團隊拓展了,資源也充足了,肯定也是需要再重構的,到時再思考其他更合適更優秀的方案。

3. App架構設計經驗談:資料層的設計

一個App,從根本上來說,就是對資料的處理,包括資料從哪裡來、資料如何組織、資料怎麼展示,從職責上劃分就是:資料管理、資料加工、資料展示。相對應的也就有了三層架構:資料層、業務層、展示層。本文就先講講資料層的設計。

資料層,是三層架構中的最底層,負責資料的管理。它主要的任務就是:

  1. 呼叫網路API,獲取資料;
  2. 將資料快取到本地;
  3. 將資料交付給上一層。

根據這三個任務,資料層可以再拆分為三層:

  • 網路層
  • 本地資料層
  • 交付層

3.1 網路層

網路層主要就是對網路API的封裝。關於API的設計,該系列的第一篇文章介面的設計已經講過一些。關於如何封裝,可以參考Android專案重構之路系列的架構篇實現篇,其中介面層和本文的網路層是一樣的。

還有一些在前面的文章中沒有提及到的,在此做一些補充。

首先是不同網路狀態的處理。當網路不可用時,則不應該再去呼叫API;當網路可用,但不是WIFI時,有些比較耗流量的操作也應該禁止,比如上傳和下載大檔案;當網路狀態不同時,還可以採用不同的網路策略,比如,當網路為WIFI時,當前API可以返回更多更全面的資料,還可以預先載入相關聯的其他API。

其次,為了節省流量,介面的設計上可以對資料進行簡化。例如,對於一些列表類的介面,可以這麼設計:只返回更新的部分,比如,上一次請求返回了10條按時間排序的資料,第一條資料為最新的,id為101,當發起下一次請求時,將101的id作為引數呼叫API,API查到該id,發現該id之後又新增了兩條資料,API則只返回新增的這兩條資料。

另外,為了保證程式的健壯性,呼叫API時,對入參的合法性檢查也是很有必要的。而且,也應該定義好本地的錯誤碼和錯誤資訊,保證每個錯誤都能正常解析。

3.2 本地資料層

本地資料層主要就是做快取處理,這需要設計好一套快取策略。設計快取策略時,有幾個問題需要考慮清楚:

  • 哪些需要快取?哪些不需要快取?
  • 快取在哪裡?資料庫?檔案?還是記憶體?
  • 快取時間多長?

哪些需要快取?
將所有資料都快取是不明智的,不同的資料應該有不同的快取策略,比如一個電商App,首頁的商品列表資料應該快取,而且快取時間應該比較長,而每個商品的詳情資料就沒必要快取或快取時間很短。對於一份資料需不需要快取,判斷標準可以是:使用者檢視該資料的頻率高不高?首頁商品列表是使用者每次啟動都會看到的,而每個商品的詳情使用者最多隻看幾次。

快取在哪裡?
從記憶體讀取資料是最快的,但記憶體非常有限。因此,記憶體一般只用來快取使用頻率非常高的資料。
檔案快取主要就是圖片、音訊、視訊了。
資料庫可以儲存大量資料,主要就是用於儲存商品列表、聊天記錄之類的關係型資料。
然而,不管快取在哪裡,都需要限定好快取的容量,要定期清理,不然會越積越多。

快取時間多長?
首先,每份快取資料都應該設定一個快取的有效時間,有效期的起始時間以最後一次被呼叫的時間為準,當該資料長時間沒有再被呼叫到時,就應該從快取中清理掉。

快取的有效時間應該設多長呢?可以短至一分鐘,長至一星期甚至一個月,具體因資料而異。一般記憶體的快取時間不宜太長,程式退出基本就要全部清理了。檔案快取可以設定保留一天或一個星期,可以每隔一天清理一次。資料庫快取再久一些也無所謂,但最好還是不要超過一個月。

3.3 交付層

交付層其實就是一個向上層開放的互動介面層,是上層向資料層獲取資料的入口。上層向資料層請求資料,它是不關心資料層的資料是從快取獲取還是從網路獲取的,它只關心結果,資料層能給到它想要的資料結果就OK了。因此,交付層主要就是定義一堆開放的介面或協議。

如果介面或協議非常多,那麼,將介面或協議按照模組劃分也是有必要的。比如微信,按模組劃分有:IM、公眾號、朋友圈、錢包、購物、遊戲等等。模組之間應該儘量相對獨立、鬆耦合。

4. App架構設計經驗談:業務層的設計

業務層其實並不複雜,但是大部分開發人員對其職責並沒有理解清楚,從而使其淪落為一個數據中轉站。我之前分享過的Android專案重構之路系列中提到的核心層,其實就是這裡所講的業務層。但有不少讀者反映,他們在實際專案中就只是做一下引數檢查,然後直接呼叫API,與展示層對接的介面基本也與API的介面一致的。這樣,業務層無疑就已經變為了一個數據中轉站。

4.1 業務層的職責

所以,設計業務層之前,對業務層的職責要先真正理解清楚。這裡,我舉兩個栗子說明一下。

第一個是新使用者註冊的例子。
註冊時,介面上一般都會要求使用者輸入手機號、驗證碼、密碼和確認密碼。但是,API介面一般只會有三個引數:手機號、驗證碼和密碼,不會有確認密碼。因此,呼叫介面之前,密碼和確認密碼的一致性檢查是必須的。同時,也要檢查這些資料是否為空、手機號是否符合規範、驗證碼是否有效、密碼有沒有包含了特殊字元等。正確姿勢就是當所有檢查都通過了之後,才呼叫API介面。最後,呼叫註冊介面成功後,可能還要再呼叫一次登入介面,並可能將使用者登入資訊快取起來,方便使用者下次啟動應用時自動登入。所有這些都屬於業務邏輯處理,也就是業務層的工作。

第二個是涉及使用者驗證的例子。
比如,在一個電商App,當用戶瀏覽某個商品,點選購買時,App首先會判斷使用者是否已經登入,如未登入,則會跳轉到登入頁面讓使用者先登入。如果已經登入,但token已經過期,那需要先去獲取新的token,之後才能進行下一步的購物操作。這些邏輯處理,也是業務層的工作。

因此,業務層就是處理業務邏輯,包括資料的檢查、業務分支的處理等。
比如上面第二個例子,可能很多人就會將使用者是否已經登入的判斷直接在介面上做處理,當確認登入後,token也是有效的之後,才呼叫業務層做購買商品的操作,這就是導致業務層淪落為API的資料中轉站的直接表現。

4.2 業務層的互動

只有真正理解了業務層的職責之後,才能有效地設計業務層與外層的互動介面。

業務層向下,與資料層互動;向上,與展示層互動。

與資料層互動只是呼叫資料層的介面獲取資料,而與展示層互動則需要提供介面給展示層呼叫。因為業務處理一般屬於比較耗時的操作,主要在於底層的網路請求比較耗時,所以提供給展示層的介面資料結果應該以非同步的方式提供,因此,介面上就需要提供個回撥引數,返回業務處理之後的結果。我之前分享過的Android專案重構之路:實現篇有講到一種實現方式,可參考。

5. App架構設計經驗談:展示層的設計

展示層是三層架構中最複雜的一層了,需要考慮的包括但不限於介面佈局、螢幕適配、文字大小、顏色、圖片資源、提示資訊、動畫等等。展示層也是變化最頻繁的一個層面,每天改得最多的就是介面了。因此,展示層也是最容易變得混亂不堪的一個層面。一個良好的展示層,應該有較好的可讀性、健壯性、維護性、擴充套件性。

5.1 三原則

我在Android專案重構之路:介面篇中提到過三個原則,要設計好展示層,至少需要遵循好這三條基本的原則:

  1. 保持規範性:定義好開發規範,包括書寫規範、命名規範、註釋規範等,並按照規範嚴格執行;
  2. 保持單一性:佈局就只做佈局,內容就只做內容,各自分離好,每個方法、每個類,也只做一件事情;
  3. 保持簡潔性:保持程式碼和結構的簡潔,每個方法,每個類,每個包,每個檔案,都不要塞太多程式碼或資源,感覺多了就應該拆分。

關於這三個原則詳細的解說,介面篇已經講過的,我這裡就不再重複。在此,我只做些補充。

關於規範,Android方面,我已經分享過一套Android技術積累:開發規範,主要分為書寫規範、命名規範、註釋規範三部分。iOS方面,蘋果已經有一套Coding Guidelines,主要屬於命名方面的規範。當我們制定自己的開發規範時,首先就要遵守蘋果的這份規範,在此基礎上再加上自己的規範。

最重要的不是開發規範的制定,而是開發規範的執行。如果沒有按照開發規範去執行,那開發規範就等於形同虛設,那程式碼混亂的問題依然得不到解決。

另外,Android系統本身已經對資源進行了很好的分離,字串、顏色值、尺寸大小、圖片、動畫等等都用不同的xml檔案定義。而iOS系統在這方面就遜色很多,只能自己實現,其中一種實現方案就是通過plist檔案的方式實現和Android一樣的機制。

5.2 工程結構

工程結構其實就是模組的劃分,無非分為兩類:按業務劃分或按元件劃分。
比如一個電商App,可能會有首頁、附近、分類、我的四大模組,工程結構也根據這四大模組進行劃分,Android可能就分為了四個模組包:

  • com.domain.home 首頁
  • com.domain.nearby 附近
  • com.domain.category 分類
  • com.domain.user 我的

同樣的,iOS則分為四個分組:home、nearby、category、user。

之後,每個模組下相應的頁面就放入相應的模組。那麼,問題來了,商品詳情頁應該屬於哪個模組呢?首頁會跳轉到商品詳情頁,附近也會跳轉到商品詳情頁,分類也會跳轉到商品詳情頁,使用者檢視訂單時也能跳轉到商品詳情頁。有些頁面,並不能很明顯的區分出屬於哪個模組的。我接手過的,按業務劃分的二手專案中(即不是由我搭建的專案),我要找一個頁面時,我認為應該屬於A模組的,但在A模組卻找不到,問了同事才知道在B模組。類似的情況出現過很多次,而且不止出現在我身上,對業務不熟悉的開發人員都會出現這個問題。而且,對業務不熟悉的開發人員開發新的頁面或功能時,如果對業務理解不深,劃分出錯,那也將成為問題,其他人員要找該頁面時更難找到了。
因此,我更喜歡按元件劃分的工程結構,因為元件每個人都懂,不管對業務熟不熟悉,查詢起來都明顯方便很多。Android按元件劃分大致如下:

  • com.domain.activities 存放所有的Activity
  • com.domain.fragments 存放所有的Fragment
  • com.domain.adapters 存放所有的Adapter
  • com.domain.services 存放所有的Service
  • com.domain.views 存放所有的自定義View
  • com.domain.utils 存放所有的工具類

iOS的分組則大致如下:

  • controllers 存放所有ViewController
  • cells 存放所有Cell,包括TableViewCell和CollectionViewCell
  • views 存放所有自定義控制元件或對系統控制元件的擴充套件
  • utils 存放所有的工具類

5.3 基類的定義

Android的Activity、Fragment、Adapter,iOS的ViewController,分別定義一個基類,將大部分通用的變數和方法定義和封裝好,將減少很多工作量,而且有了統一的設定,也會減少程式碼的混亂。比如我在Android專案重構之路:實現篇中提到的KBaseActivity和KBaseAdapter的實現就是例子,當然還可以抽離出更多變數和方法。
每個Activity的onCreate()方法,一般分為三步:

  1. 變數的初始化;
  2. View的初始化;
  3. 載入資料。

因此,其實可以將onCreate()方法拆分成三個方法:

  1. initVariables()
  2. initViews()
  3. loadData()

在基類中將這三個方法定義為抽象方法,由子類去實現,這樣,子類就不需要實現onCreate()方法了,只要實現更細化的上述三個方法即可。

iOS的ViewController也是同樣的方式,這裡就不重複了。

5.4 寫在最後

自此,該系列的文章暫時就完結了,方法論比較多,很少涉及到具體的實現。因為具體實現的方案很多,而且還要結合實際專案,無法說哪個方案好哪個方案差。但方法論大部分是想通的,所以,本系列主要講方法論。