注:本文内容較長且細節較多,建議先收藏再閱讀,原文将在 Github 上維護與更新。
在 HTTP 接口開發與調試過程中,我們經常遇到以下類似的問題:
為什麼本地環境接口可以調用成功,但放到手機上就跑不起來? 這個接口很複雜,内部調用了好幾個其他接口,如何定位問題究竟出在哪一步? 後端開發還沒有把接口提供好,前端開發任務無法推進……
「貓哥網絡程式設計系列」最核心的任務便是向各位分享一個我從多年的前後端項目中總結而來的「萬能」HTTP 調試法,掌握并從網絡程式設計原理上了解它,能讓我們順利定位并解決所有 HTTP 接口問題。由于該方法主要涉及到的知識點包括 HTTP 代理(Proxy)、編輯(Edit)與資料模拟(Mock),是以我稱之為「HTTP PEM 調試法」。
接下來,我們就針對前面提出的幾個問題,詳細講解下 PEM 調試法的思路。
如何調試線上 App 中的 H5 頁面?
在上一期《貓哥網絡程式設計系列:詳解 BAT 面試題》中,我們有介紹到 Windows 下的 Fiddler 和 Mac 下的 Charles 這兩款 HTTP 抓包工具,其實它們就是兩個 HTTP 代理伺服器(HTTP Proxy Server)。由于 HTTP 是一種符合 REST 架構風格(Representational State Transfer)的協定,具有無狀态(Stateless)與統一接口(Uniform Interface)的架構限制,是以其代理機制的實作十分的簡單。
打個比方,我們可以把 Proxy Server 了解成一個快遞中轉站,當一個包裹經過中轉站時,包裹的資訊(發件人、收件人與包裹裡的貨物)通常不會做任何的改動,直接發往下一個中轉站或顧客手中。但中轉站完全有能力修改快遞單資訊、拆箱檢查貨物,甚至是私吞或調換貨物。
當我們需要快速定位「線上産品的接口問題」時,如果沒有源碼、資料、依賴服務和足夠的時間去搭建一個測試環境,則通常會使用 HTTP 代理伺服器來進行快速抓包調試。
Fiddler 預設隻允許本地 IP(127.0.0.1)使用代理服務,通過設定「Tools -> Connections -> Allow remote computers to connect」可以開啟其他 IP(通常是同一區域網路内的其他裝置)使用代理服務。

Charles 預設開放代理服務,但陌生裝置首次連接配接時需要授權确認,通過以下配置可以設定成無需授權。
以上兩款軟體預設的代理端口均是 8888 ,軟體開啟之後,我們可以在對應的平台終端下通過<code>ipconfig</code>(Windows) 或 <code>ifconfig</code>(Mac)指令查詢本機的區域網路 IP,還可以使用 <code>telnet</code> 指令檢查代理通道是否可用。(注: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)可以将響應頭設定成常見狀态碼的傳回,或将響應體映射成本地檔案,通過外部編輯器修改檔案内容進行調試。其中,若設定響應為 <code>*bpu</code> 或 <code>*bpafter</code> 可以在請求前與響應前的事件觸發時進行斷點調試,十分友善。
需要注意的是,在 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 全局安裝 weinre: <code>npm install -g weinre</code>
在本地 8081 端口上啟動 weinre 服務:<code>weinre --boundHost 0.0.0.0 --httpPort 8081</code> 。通常在 Node.js 的服務中綁定 IP 為 0.0.0.0 而非 127.0.0.1(本地 IP),意味着可以讓任意來源的 IP 通路該服務
通過上文介紹的 <code>ipconfig</code>(Mac 為 <code>ifconfig</code>)指令擷取本機 IP 後,在本機 Chrome 浏覽器中通路 Weinre 管理背景:http://10.2.69.47:8081 (本例中我的 IP 為 10.2.69.47,請注意将其替換成自己的區域網路 IP)
在管理背景我們能看到相關使用說明,要求将以下腳本插入需要調試的 H5 頁面中:<code><script src="http://10.2.69.47:8081/target/target-script-min.js#anonymous"></script></code>
将以上腳本插入進 H5 頁面後,我們在 PC 端 Chrome 中,通過http://10.2.69.47:8081/client/#anonymous 背景點選進入相應的用戶端調試界面
問題是,我們「如何将 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」):
Fiddler Script 使用的程式設計語言是 JScript.NET(JavaScript 和 C# 的混合文法,類似 TypeScript),<code>OnBeforeResponse</code> 是 HTTP Response 響應前的事件函數,我們隻需要在這裡判斷「如果開啟了 Weinre Debug 功能,那麼就在所有的 HTML 響應體中注入 Weinre Script」,以下是我修改的示例代碼,覆寫以上代碼塊即可。
修改儲存後重新開機 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 開發過程中,推薦使用以下工具進行調試 :
微信官方調試工具 調試基于微信的 Web App
Chrome Remote Debugging 調試 Android Web App
Safari Remote Debugging 調試 iOS Web App
由此可見,「HTTP PEM 調試法」是一個通用的 HTTP 接口調試方案,可以用來快速定位線上接口問題,對于開發人員來說掌握其背後的 HTTP 協定及其代理機制的原理更加重要,接下來我們聊聊常見的 HTTP 接口開發協作方法與 Mock 思路。
我的開發任務沒法推進,因為某某的接口還沒提供給我。
希望新手程式員在看完這一章節之後,不要再向你的項目組和上級回報這樣的說法,因為 HTTP Mock(接口資料模拟)是一項網絡程式設計的基礎技能,從實際項目經驗來看,大部分基于 HTTP 接口的任務都可以并行開發。
不同崗位(例如前端開發與背景開發)或不同業務(例如訂單系統與賬戶系統)的開發人員開始并行開發任務之前,首先要做的應該是對耦合和互相依賴的任務進行邊界劃分與規則約定。具體到某個 HTTP API 接口的約定上,至少應該明确以下資訊:
是否按照 RESTful API 的約定來設計接口
接口的路徑、送出方法、參數、編碼類型(Enctype/Content-Type)
接口傳回的錯誤碼(code)、消息說明(message)、業務資料(data)
針對以上三條資訊,我設想的「最簡」 HTTP API 包含以下幾條原則,供各位參考:
RESTful API 實際上是利用 HTTP 協定的語義(送出類型、傳回碼、Hypermedia Link)來将所有接口操作抽象化為一系列資源對象。這要求 API 的設計者與調用者都具備深厚的 HTTP 協定功底、語義化與抽象化能力。
RESTful 作為一個 Buzzword(流行詞),其含義已經被曲解。HTTP 協定和 REST 架構的設計者 Roy Fielding 很反感這一點,還專門開了部落格以正視聽。大多數人隻将 HTTP 當做一種傳輸協定來使用(既成事實),并不能真正了解 REST 架構風格;
RESTful API 将所有請求抽象化為資源名詞(Resources)的做法争議很大。這種做法總會讓我回想起上個世紀用 FrontPage 做網頁的經曆,「設定一個超連結,從某個資源跳到另外一個資源」。在經過 Web 2.0 浪潮,進入移動網際網路時代後,這種 API 設計容易給人帶來困惑。例如「登入、注冊」這樣的「動詞」如何抽象成「名詞」(還好有 Github API 可以參考 )。而刻意的使用 「HTTP CRUD」(POST/GET/PUT/DELETE Method)操作「資源化」之後的接口,并未帶來更多實質上的收益;
HTTP 狀态碼的分層思路在 RESTful API 模式下被破壞了。HTTP 1.0 中定義的常見狀态碼已經足夠網絡中間元件(代理、網關、路由)使用,HTTP 1.1 中加入的很多狀态碼缺乏實際場景(例如 306 狀态碼的廢棄),它們增加了中間元件以及浏覽器對規範了解與實作的要求。盡可能的将狀态碼交給相應的接口邏輯層而非 HTTP 協定層,能夠将問題簡化;
對比以英文為母語的國外開發者而言,國内開發者對語義化的認知難度更高,例如 RESTful 建議資源命名用複數形式,那收貨位址單詞 address 的複數形式是什麼?address or addresses ?address-list or address-lists?(沒過英語八級的同學已經哭暈在廁所 T_T)
每個人對 RESTful API 的了解都不同,在 HTTP 協定層面做擴充與實作,不如交給接口設計者與調用者自己來約定資料結構(或者參考 JSON-RPC 規範)。把 HTTP 隻當做傳輸協定來使用的好處是,當後端服務間的接口需要直接基于 TCP 傳輸層來做性能優化時,可以十分友善的切換成 Socket 的實作(之前在騰訊做微網誌相關項目時,微網誌開放平台對外隻提供 HTTP 的 Open API,但對内可以提供更高頻率與頻次調用的原生 Socket 協定)。
由于 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 封包,請注意其中的 <code>Content-Type</code> 與 Body 的對應關系(已手動删除無關 HTTP Header)
隻有用戶端 POST 請求體的消息格式與其請求頭聲明的 Content-Type 一緻時,服務端才能正确的接收與響應。因為許多後端的 Web 應用架構會遵照 HTTP 協定的内容協商原則(Content Negotiation)對響應體進行預處理,以提升開發體驗。例如,Python 的 Flask 架構 封裝了request.json、request.form、request.data 等一系列屬性用于存放不同類型的來源資料。
API URI 應該全小寫。屏蔽掉 Linux/Windows 作業系統對檔案名大小寫敏感度不一緻的問題;
URI 命名上應該使用連字元「-」來間隔,而不是使用下劃線「_」或駝峰式。這是出于視覺美觀度和英文語義方面的考慮,英文域名規範規定可以使用連字元,但不能使用下劃線,API 路徑應該和 Domain 命名風格一緻;
URI 使用「動詞+名詞」或者「名詞+動詞」均可,但標明一種之後應該保持一緻。接口風格的一緻性,可以降低使用者的了解成本,好的 API 命名風格能讓人「以一知萬」,能從一個 API 猜測出所有其他 API 的命名形式;
參數命名上應該使用下劃線「_」而非連接配接符「-」。這點主要是從資料庫字段設計的統一性和背景應用程式架構的易用性來考慮;
不同接口的相同參數命名應保持統一,并考慮擴充要求。例如,收集使用者資訊的參數可以統一叫「ua」,為了便于擴充可以約定将用戶端分辨率、浏覽器型号等資訊使用「||」字元串連接配接,如<code>ua=1280x768||chrome</code>,當需要添加作業系統字段時,用戶端隻需按規則追加資訊到原來的參數上,如<code>ua=1280x768||chrome||windows</code>。該條原則還有許多其他的方法來實作,不再一一舉例。
基本的傳回體結構,可參考以下示例代碼。
寥寥的幾行代碼飽含了幾部深刻的血淚史:
出于一緻性的考慮,<code>code</code> 表示傳回碼(也可以了解成錯誤碼),成功時傳回 <code>"0"</code> ,出錯時按預設的錯誤碼規則傳回(微信的傳回碼規範設計的并不好,因為沒有内建的規律和語義);
同上,可以了解 <code>message</code> 與 <code>data</code> 的設計。需要注意的是 data 隻具有 Object 一種類型。無資料的時候傳回一個空對象 <code>{}</code>(而非 <code>null</code>),有多條資料的時候将 Array 類型資料放在其内部的 <code>list</code> 之類的屬性中;
所有原始資料類型建議統一使用字元串類型,包括布爾值用 <code>"0"</code> 和 <code>"1"</code>。原因是前後端對浮點數運算精度不一緻,會導緻商品價格的計算與展示出錯;iOS/Android 用戶端對 JSON null、布爾類型轉換的不一緻會導緻頻繁的 App Crash。
當然,也有許多其他的方案可以解決上面提到的問題,但出于「最簡」的原則,這樣約定的了解成本最低。
有了最簡 API 的約定之後,實作最簡 Mock Server 就相對簡單多了。
首先,我們按照 API 接口約定來建立一些模拟資料檔案。例如建立一個 「mock-data.json」 的檔案,将以上傳回體資料儲存其中。
在指令行模式下運作 <code>php</code> 指令,Mac 使用者直接打開終端即可,Windows 使用者需要先安裝 XAMPP 套件,并将 php.exe 所在的目錄配置到系統環境變量中,再使用 CMD 運作以下指令:
開啟之後通路任意 API 位址(http://127.0.0.1:8080/any-api-uri-you-want/)均會傳回 mock-data.json 的資料響應體。通過将 8080 端口換成 80 端口(Mac 需要使用 sudo 權限),再設定類似 <code>127.0.0.1 www.example.com</code> 的 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 的構想:
API 錄入背景。包含一個按項目(一般是 Domain)次元進行 API 管理的背景。可以在背景上錄入「請求 URI、參數、多種業務資料響應體、全局錯誤碼、API 錯誤碼」等接口資訊;
API 接口文檔。能夠基于 API 背景資料,生成線上的 API 文檔平台;
Postman 導入/導出。能夠基于 API 資料導出生成 Postman Collections,以便導入 Postman 中進行 API 調試;
Mock Server。能夠基于 API 資料快速搭建類似 MockServer 的本地服務,或提供遠端模拟接口服務。
對于新人來說,最快的成長方式是不斷地在新項目中實踐,從頭到尾參與到項目的每個系統細節的設計與讨論。如果能參與到重點、大型項目中,甚至幸運地得到大牛的親自指導,成長速度将會突飛猛進。
但更多的情況是,新人作為離職程式員的補充力量來接手一個老項目甚至是爛攤子。面對一個複雜的陌生系統,吐槽與抱怨無濟于事。這時,如果能使用「HTTP PEM 調試法」,從接口設計與調用的角度來剖析、了解整個系統的設計,就能快速上手業務。例如,PHP 程式員可以在項目代碼中所有的 curl 調用點,将「CURLOPT_PROXY」設定成 Fiddler/Charles 的代理服務,然後一步步調試,從接口字段上了解資料庫設計和 Controller 背後的業務邏輯。
最後,歡迎各位給我留言分享更多關于「HTTP PEM」和其他調試方法的經驗與體會。
歡迎關注我的微信公衆号「貓哥學前班」