注:本文內容較長且細節較多,建議先收藏再閱讀,原文將在 Github 上維護與更新。php
在 HTTP 接口開發與調試過程當中,咱們常常遇到如下相似的問題:html
- 爲何本地環境接口能夠調用成功,但放到手機上就跑不起來?
- 這個接口很複雜,內部調用了好幾個其餘接口,如何定位問題究竟出在哪一步?
- 後端開發尚未把接口提供好,前端開發任務沒法推動……
「貓哥網絡編程系列」最核心的任務即是向各位分享一個我從多年的先後端項目中總結而來的「萬能」HTTP 調試法,掌握並從網絡編程原理上理解它,能讓咱們順利定位並解決全部 HTTP 接口問題。因爲該方法主要涉及到的知識點包括 HTTP 代理(Proxy)、編輯(Edit)與數據模擬(Mock),所以我稱之爲「HTTP PEM 調試法」。前端
接下來,咱們就針對前面提出的幾個問題,詳細講解下 PEM 調試法的思路。node
如何調試線上 App 中的 H5 頁面?android
在上一期《貓哥網絡編程系列:詳解 BAT 面試題》中,咱們有介紹到 Windows 下的 Fiddler 和 Mac 下的 Charles 這兩款 HTTP 抓包工具,其實它們就是兩個 HTTP 代理服務器(HTTP Proxy Server)。因爲 HTTP 是一種符合 REST 架構風格(Representational State Transfer)的協議,具備無狀態(Stateless)與統一接口(Uniform Interface)的架構約束,所以其代理機制的實現十分的簡單。ios
打個比方,咱們能夠把 Proxy Server 理解成一個快遞中轉站,當一個包裹通過中轉站時,包裹的信息(發件人、收件人與包裹裏的貨物)一般不會作任何的改動,直接發往下一個中轉站或顧客手中。但中轉站徹底有能力修改快遞單信息、拆箱檢查貨物,甚至是私吞或調換貨物。git
當咱們須要快速定位「線上產品的接口問題」時,若是沒有源碼、數據、依賴服務和足夠的時間去搭建一個測試環境,則一般會使用 HTTP 代理服務器來進行快速抓包調試。程序員
Fiddler 默認只容許本地 IP(127.0.0.1)使用代理服務,經過設置「Tools -> Connections -> Allow remote computers to connect」能夠開啓其餘 IP(一般是同一局域網內的其餘設備)使用代理服務。github
Charles 默認開放代理服務,但陌生設備首次鏈接時須要受權確認,經過如下配置能夠設置成無需受權。面試
以上兩款軟件默認的代理端口均是 8888 ,軟件開啓以後,咱們能夠在對應的平臺終端下經過ipconfig
(Windows) 或 ifconfig
(Mac)命令查詢本機的局域網 IP,還可使用 telnet
命令檢查代理通道是否可用。(注:Win7 下如何開啓 telnet 命令請參考百度經驗。)
如下是 Windows 下 CMD 終端的使用截圖,Mac 系統下請類比參考。
接下來,咱們將手機的 Wi-Fi 代理設置爲上述的 IP 與 端口號,如下是 iOS 的設置截圖( Android 系統一般是長按已鏈接的 Wi-Fi ,在彈出的高級設置菜單中配置代理服務器)。
至此,手機上任意應用發起的 HTTP 請求都將會被代理服務器(本例中的 Fiddler/Charles 軟件)監聽到。
經過代理服務器監聽到 HTTP 請求以後,咱們能夠經過瀏覽報文的詳細信息,定位出可能的接口問題。Fiddler 與 Charles 都具備一樣強大的 HTTP 編輯(Edit)、重發(Replay/Repeat)、斷點(Breakpoints)功能。Charles 的基礎與高級用法請參考《Charles 從入門到精通》,Fiddler 教程能夠參考 OSChina 專題《HTTP調試代理 Fiddler》,如下介紹 Fiddler 的部分常見用法。
抓到手機 HTTP 請求以後,經過編輯(Unlock For Editing)和重發(Replay)操做能夠不斷地調試接口的響應是否符合預期。
經過設置自動響應規則(AutoResponder Rules)能夠將響應頭設置成常見狀態碼的返回,或將響應體映射成本地文件,經過外部編輯器修改文件內容進行調試。其中,若設置響應爲 *bpu
或 *bpafter
能夠在請求前與響應前的事件觸發時進行斷點調試,十分方便。
須要注意的是,在 Fiddler 中使用 Replay 功能重發請求時,請求由 Fiddler 代理從新發起而非手機,所以手機 App 中的 H5 不會有任何變化。只有從新刷新 App 的 H5 頁面,配合 HTTP 斷點調試(Breakpoints )的方式纔可讓修改後的 HTTP 響應體在 App中生效。這裏介紹另一種配合 Weinre 的調試用法。
Weinre 屬於知名 Hybrid 框架 Cordova 中的一款 Web App 遠程調試工具。經過在頁面中注入一段 JS 腳本,能夠在 PC 和手機端的 H5 頁面之間創建一個 Socket 雙向數據傳輸通道。原理上能夠理解爲,當咱們在 PC 端的後臺進行 debug 時,相關的操做被序列化成一組 JSON 字符串,數據經由通道傳輸給手機端中的 H5 頁面,頁面在接收到這些數據以後反序列化成相應的 JS 腳本操做,在其 window 上下文中執行,並將執行的結果回傳給通道,PC 端的 Chrome 經過監聽通道獲取到相應的數據在 debug 後臺中展示出來。
如下介紹 Weinre 的基本用法:
npm install -g weinre
weinre --boundHost 0.0.0.0 --httpPort 8081
。一般在 Node.js 的服務中綁定 IP 爲 0.0.0.0 而非 127.0.0.1(本地 IP),意味着可讓任意來源的 IP 訪問該服務ipconfig
(Mac 爲 ifconfig
)命令獲取本機 IP 後,在本機 Chrome 瀏覽器中訪問 Weinre 管理後臺:http://10.2.69.47:8081 (本例中個人 IP 爲 10.2.69.47,請注意將其替換成本身的局域網 IP)<script src="http://10.2.69.47:8081/target/target-script-min.js#anonymous"></script>
問題是,咱們「如何將 Weinre Script 自動注入到手機的 H5 頁面中」?
想必用過中國電信寬帶的同窗都有過這樣的體驗:在剛開始瀏覽網頁時,會自動跳出一些「寬帶升級優惠」、「寬帶繳費提醒」之類的頁面。這種耍流氓的方式即是寬帶運營商在 HTTP 代理層面的 Script 注入行爲。前面已經提到 HTTP 協議是一種 REST 風格的架構,而且他的頭部與主體報文爲字符串文本流(對比二機制、十六進制數據流),在不使用 HTTPS 的狀況下,很容易被中間路由或代理網關進行消息篡改。
經過 Fiddler Script 特性,咱們能夠自動對通過 Fiddler 的 HTTP 流量進行二次修改,注入任意內容(Mac 用戶若已瞭解相關知識點,請直接跳至下方的 Charles 截圖)。
打開 Fiddler 菜單「Rules -> Customize Rules… 」,若是是首次開啓會要求先下載安裝 Fiddler ScriptEditor。打開 Fiddler ScriptEditor 以後,找到如下代碼塊(或使用菜單「Go -> to OnBeforeResponse」):
static function OnBeforeResponse(oSession: Session) { if (m_Hide304s && oSession.responseCode == 304) { oSession["ui-hide"] = "true"; } }
Fiddler Script 使用的編程語言是 JScript.NET(JavaScript 和 C# 的混合語法,相似 TypeScript),OnBeforeResponse
是 HTTP Response 響應前的事件函數,咱們只須要在這裏判斷「若是開啓了 Weinre Debug 功能,那麼就在全部的 HTML 響應體中注入 Weinre Script」,如下是我修改的示例代碼,覆蓋以上代碼塊便可。
public static RulesOption("Enable Weinre Script") var m_EnableWeinreScript: boolean = true; public static var g_weinreScriptString: String = '<script src="http://127.0.0.1:8080/target/target-script-min.js#anonymous"></script>'; public static ToolsAction("Config Weinre Script") function ConfigWeinreScript(){ g_weinreScriptString = FiddlerObject.prompt("Text beblow will inject into HTML pages when 'Enable Weinre Script' rule is Enabled.", g_weinreScriptString , "Please Input the Weinre Script"); } static function OnBeforeResponse(oSession: Session) { if (m_Hide304s && oSession.responseCode == 304) { oSession["ui-hide"] = "true"; } if (m_EnableWeinreScript && oSession.oResponse.headers.ExistsAndContains("Content-Type","text/html")){ oSession.utilDecodeResponse(); if(oSession.utilFindInResponse("</html>", false)>-1){ oSession["ui-backcolor"] = "#5E30B5"; oSession["ui-color"] = "white"; oSession.utilReplaceRegexInResponse("<\/html>", g_weinreScriptString + '</html>'); } } }
修改保存後重啓 Fiddler(或使用菜單「Tools -> Reset Script」)以生效規則,接下來運行「Tools」菜單中新出現的「Config Weinre Script」,將 127.0.0.1:8080 替換成本身本機的局域網 IP 與 weinre 服務端口號,同時開啓菜單「Rules -> Enable Weinre Script」。至此,全部 HTML 頁面將會被自動注入 Weinre Script,以後咱們就能夠在 weinre 後臺中開始調試相關頁面。如下是參考截圖:
能夠看到 HTTP 響應體中已經被動態注入 Weinre Script。
在 Mac Charles 下的 Script 注入配置更加容易,只需利用其 「Rewrite」功能進行簡單的配置便可,參看下圖:
經過 Fiddler/Charles 代理工具將 JS 腳本注入成功後,咱們即可以經過前文提到的 weinre 後臺開始 debug 相應的頁面,如下是在 iPhone 模擬器中調試新浪微博界面的截圖:
使用該方法能夠調試 Android 和 iOS 中「任意 App 的 H5 頁面」,但因爲主要使用了 weinre 服務,其原理決定了該方法沒法像真正的 Chrome DevTools 同樣支持 JS 斷點調試、Profiles 性能分析等功能,具備必定的侷限性。在實際 Web App 開發過程當中,推薦使用如下工具進行調試 :
因而可知,「HTTP PEM 調試法」是一個通用的 HTTP 接口調試方案,能夠用來快速定位線上接口問題,對於開發人員來講掌握其背後的 HTTP 協議及其代理機制的原理更加劇要,接下來咱們聊聊常見的 HTTP 接口開發協做方法與 Mock 思路。
個人開發任務無法推動,由於某某的接口還沒提供給我。
但願新手程序員在看完這一章節以後,不要再向你的項目組和上級反饋這樣的說法,由於 HTTP Mock(接口數據模擬)是一項網絡編程的基礎技能,從實際項目經驗來看,大部分基於 HTTP 接口的任務均可以並行開發。
不一樣崗位(例如前端開發與後臺開發)或不一樣業務(例如訂單系統與帳戶系統)的開發人員開始並行開發任務以前,首先要作的應該是對耦合和相互依賴的任務進行邊界劃分與規則約定。具體到某個 HTTP API 接口的約定上,至少應該明確如下信息:
針對以上三條信息,我設想的「最簡」 HTTP API 包含如下幾條原則,供各位參考:
RESTful API 其實是利用 HTTP 協議的語義(提交類型、返回碼、Hypermedia Link)來將全部接口操做抽象化爲一系列資源對象。這要求 API 的設計者與調用者都具有深厚的 HTTP 協議功底、語義化與抽象化能力。
因爲 HTTP 1.0 尤爲是 HTML 的規範與應用已經深刻人心。大部分開發者可以很天然的這樣理解:「GET」 表示「讀」操做,「POST」 表示「寫」操做。這樣既能夠保證中間組件與瀏覽器很好的利用 GET 的緩存機制,又能下降接口設計的複雜度。HTTP 之父 Roy Fielding 也說過「It is okay to use POST」:
Some people think that REST suggests not to use POST for updates. Search my dissertation and you won’t find any mention of CRUD or POST. (不少人認爲 RESTful 建議不要使用 POST 用於提交更新,去翻一翻個人論文,壓根就沒提到過 POST 和其餘「增查改刪」方面的內容。)
但使用 POST 方法時尤爲要注意:「使用統一的 Content-Type」。這是一個容易被新手忽略的細節,也是接口設計中常常出錯的點。在上一期的《貓哥網絡編程系列:詳解 BAT 面試題》中有問到:
一個 POST 請求的 Content-Type 有多少種,傳輸的數據格式有何區別?
如下舉例一些常見類型的 HTTP POST Request 報文,請注意其中的 Content-Type
與 Body 的對應關係(已手動刪除無關 HTTP Header)
POST /test.php HTTP/1.1 Host: 127.0.0.1:8080 Content-Length: 54 Content-Type: application/json {"weixin_id":"imgXQB","weixin_name":"貓哥學前班"}
POST /test.php HTTP/1.1 Host: 127.0.0.1:8080 Content-Length: 74 Content-Type: application/x-www-form-urlencoded weixin_id=imgXQB&weixin_name=%E7%8C%AB%E5%93%A5%E5%AD%A6%E5%89%8D%E7%8F%AD
POST /test.php HTTP/1.1 Host: 127.0.0.1:8080 Content-Length: 259 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryl60ti7CVoBj2kxfX ------WebKitFormBoundaryl60ti7CVoBj2kxfX Content-Disposition: form-data; name="weixin_id" imgXQB ------WebKitFormBoundaryl60ti7CVoBj2kxfX Content-Disposition: form-data; name="weixin_name" 貓哥學前班 ------WebKitFormBoundaryl60ti7CVoBj2kxfX--
只有客戶端 POST 請求體的消息格式與其請求頭聲明的 Content-Type 一致時,服務端才能正確的接收與響應。由於許多後端的 Web 應用框架會遵守 HTTP 協議的內容協商原則(Content Negotiation)對響應體進行預處理,以提高開發體驗。例如,Python 的 Flask 框架 封裝了request.json、request.form、request.data 等一系列屬性用於存放不一樣類型的來源數據。
ua=1280x768||chrome
,當須要添加操做系統字段時,客戶端只需按規則追加信息到原來的參數上,如ua=1280x768||chrome||windows
。該條原則還有許多其餘的方法來實現,再也不一一舉例。基本的返回體結構,可參考如下示例代碼。
{ "code": "0", "message": "success", "data": { "id" : "1", "list" : [] } }
寥寥的幾行代碼飽含了幾部深入的血淚史:
code
表示返回碼(也能夠理解成錯誤碼),成功時返回 "0"
,出錯時按預設的錯誤碼規則返回(微信的返回碼規範設計的並很差,由於沒有內建的規律和語義);message
與 data
的設計。須要注意的是 data 只具備 Object 一種類型。無數據的時候返回一個空對象 {}
(而非 null
),有多條數據的時候將 Array 類型數據放在其內部的 list
之類的屬性中;"0"
和 "1"
。緣由是先後端對浮點數運算精度不一致,會致使商品價格的計算與展現出錯;iOS/Android 客戶端對 JSON null、布爾類型轉換的不一致會致使頻繁的 App Crash。固然,也有許多其餘的方案能夠解決上面提到的問題,但出於「最簡」的原則,這樣約定的理解成本最低。
有了最簡 API 的約定以後,實現最簡 Mock Server 就相對簡單多了。
首先,咱們按照 API 接口約定來新建一些模擬數據文件。例如新建一個 「mock-data.json」 的文件,將以上返回體數據保存其中。
在命令行模式下運行 php
命令,Mac 用戶直接打開終端便可,Windows 用戶須要先安裝 XAMPP 套件,並將 php.exe 所在的目錄配置到系統環境變量中,再使用 CMD 運行如下命令:
php -S 0.0.0.0:8080 mock-data.json
開啓以後訪問任意 API 地址(http://127.0.0.1:8080/any-api-uri-you-want/)均會返回 mock-data.json 的數據響應體。經過將 8080 端口換成 80 端口(Mac 須要使用 sudo 權限),再設置相似 127.0.0.1 www.example.com
的 HOST 配置,即可以模擬 API 的 Domain Host(http://www.example.com/any-api-uri-you-want/)形式。
固然,也能夠本身編寫一個 index.php 的入口文件來實現一個基於 URL Path 規則的簡單 Rewrite 功能,用來同時支持多個 API 的數據模擬。
Fiddler/Charles 的 Map Local(本地映射)不光是用於 HTTP Edit,一樣能夠用於 HTTP Mock,當一個 404 請求(還未真正實現的 API)被代理服務器捕獲後,能夠設置映射到本地自定義的 mock-data.json 模擬數據文件,從而被模擬成一個正常的 200 請求。
迄今爲止,我還未發現一個理想中的 Mock API 開源系統,若有哪位同窗有見到過請在 Github 上留言周知,如下是我對最理想 Mock System 的構想:
這個接口很複雜,內部調用了好幾個其餘接口,如何定位問題究竟出在哪一步?
對於新人來講,最快的成長方式是不斷地在新項目中實踐,從頭至尾參與到項目的每一個系統細節的設計與討論。若是能參與到重點、大型項目中,甚至幸運地獲得大牛的親自指導,成長速度將會日新月異。
但更多的狀況是,新人做爲離職程序員的補充力量來接手一個老項目甚至是爛攤子。面對一個複雜的陌生系統,吐槽與抱怨無濟於事。這時,若是能使用「HTTP PEM 調試法」,從接口設計與調用的角度來剖析、理解整個系統的設計,就能快速上手業務。例如,PHP 程序員能夠在項目代碼中全部的 curl 調用點,將「CURLOPT_PROXY」設置成 Fiddler/Charles 的代理服務,而後一步步調試,從接口字段上理解數據庫設計和 Controller 背後的業務邏輯。
最後,歡迎各位給我留言分享更多關於「HTTP PEM」和其餘調試方法的經驗與體會。