任何做過 web 開發的同學,都避免不了在浏覽器内進行調試。而大部分同學的首選工具,就是 chrome devtools。devtools 本身我們無需多說,是一個大家不能再熟悉的工具了。但是埋藏在 devtools 下面的開放協定以及它賦予的衆多可能性,至今仍未見到充分的剖析和應用。
為什麼我們關注 devtools:
原因 1:devtools 是開源項目
原因 2:它足夠簡單
devtools 僅僅是簡單的由 html、javascript、css、images 組成的,本質上就是一個 webapp,純粹的前端應用。當你去了解、修改它時,你不需要了解 c++ 和任何編譯的知識。
原因 3:它的應用架構足夠開放,滿足任何形式的功能擴充。
事實上 devtools 是一個充分子產品化的 javascript 網頁應用。它的每個功能你都可以去擴充(僅需要了解 javascript)。
原因 4:大部分前端都已經習慣它并且喜歡它。
<a></a>
談到遠端調試前,有必要先了解各元件之間的關系。
浏覽器擁有多個 tab,并為每個 tab 單獨提供 websocket 的 endpoint uri
每個 devtool 執行個體隻能檢視一個 tab,即隻能與一個 tab 保持通訊
大家在本地電腦上就可以體驗這個遠端調試是怎樣一回事。執行如下步驟:
徹底關閉目前 chrome 程序
在 chrome 的啟動參數上加上 <code>--remote-debugging-port=9222</code>,例如 mac 平台:
<code>open -a google\ chrome –args –remote-debugging-port=9222</code>
在開啟的 chrome 浏覽器裡打開任意網頁,例如:http://www.taobao.com/
在其他浏覽器或者 chrome 的新 tab 打開 http://localhost:9222,你會得到這樣的界面:
點選 “淘寶網” 的方框,就進入頁面的調試界面了:
注意看位址欄,我們通路的是一個标準的 http 協定下的網頁,不是 chrome 的私有協定。這裡,你可以用 devtools 再次檢視這個頁面,即按下 <code>cmd</code> + <code>option</code> + <code>i</code>。你會發現,這真的就是一個 html 應用。
再觀察一下這個 url:
<code>http://localhost:9222/devtools/inspector.html?ws=localhost:9222/devtools/page/06d198ac-907f-430c-999c-16ccd7d2d489</code>
通過 querystring,我們告訴了 devtools 的前端應用,它應該連接配接到哪個 websocket 服務。
你可以再你剛打開的檢視 devtools 的 devtools(好繞口)裡面,觀察整個調試過程中的 websocket 通訊。例如:
以前用 websocket 做過 rpc 的同學應該看得出來,google 實作的的确就是一個遠端調用的接口。這個接口裡面有兩種通訊模式:
request/response:就如同一個異步調用,通過請求的資訊,擷取相應的傳回結果。這樣的通訊必然有一個 message id,否則兩方都無法正确的判斷請求和傳回的比對狀況。
notification:和第一種不同,這種模式用于由一方單方面的通知另一方某個資訊。和 “事件” 的概念類似。
通過調試協定來擷取頁面加載的所有網絡請求并列印。為了簡單,我們編寫一個 node.js 的應用來實作。大緻步驟如下:
用 websocket 用戶端連接配接調試服務
分别監聽<code>network.requestwillbesent</code>、<code>network.loadingfailed</code>、<code>network.loadingfinished</code>、<code>network.responsereceived</code>、<code>network.requestservedfromcache 的 notification</code>,并且列印相關的 log。
發送 <code>page.navigate</code> 的請求,将頁面跳轉到某個頁面,例如:http://www.taobao.com/
這裡拿到的資料足以繪制一個非常準确的頁面加載的瀑布圖。從調試協定裡拿到的資料具有以下特點:
準确,這是 webkit 核心回報的資料;而不是外層 javascript 接口的統計,也不是通過代理監控網絡資料拿到的結果。
豐富,有很多資料,别的方法根本拿不到。例如,緩存狀況、javascript 方法執行情況。
标準,調試協定本身已經定義了大量的 json 資料結構,你不需要再次進行抽象設計。
完整代碼如下(請先安裝好相應的 npm 子產品,并且打開 chrome 本地的 9222 調試端口):
<code>var websocketclient = require("websocket").client,</code>
<code>util = require("util"),</code>
<code>ee = require("events").eventemitter,</code>
<code>request = require("request"),</code>
<code>chalk = require("chalk"),</code>
<code>exec = require("child_process").exec;</code>
<code></code>
<code>// `commander` class is message handler that talks to debug service exposed by chrome</code>
<code>var commander = function(conn) {</code>
<code>ee.call(this);</code>
<code>this.connection = conn;</code>
<code>this.sendcommands = [];</code>
<code>var self = this;</code>
<code>object.defineproperty(this, "nextmsgid", {</code>
<code>get: function() {</code>
<code>return self.sendcommands.length;</code>
<code>},</code>
<code>enumerable: true,</code>
<code>configurable: false</code>
<code>});</code>
<code>conn.on("message", this.onmessage.bind(this));</code>
<code>};</code>
<code>util.inherits(commander, ee);</code>
<code>// send message using websocket connection</code>
<code>commander.prototype.send = function(method, params, callback) {</code>
<code>this.sendcommands.push([method, params, callback]);</code>
<code>var msg = json.stringify({</code>
<code>id: this.nextmsgid,</code>
<code>method: method,</code>
<code>params: params</code>
<code>console.log(msg);</code>
<code>this.connection.send(msg);</code>
<code>//handler for receiving a message</code>
<code>commander.prototype.onmessage = function(msg) {</code>
<code>var command, data;</code>
<code>if(msg.type === "utf8") {</code>
<code>data = json.parse(msg.utf8data);</code>
<code>if(data.id) {//it's method request/response invocation</code>
<code>command = this.sendcommands[data.id-1];</code>
<code>if(command) {</code>
<code>if(command.callback) {</code>
<code>command.callback(data.params);</code>
<code>}</code>
<code>} else {</code>
<code>console.warn("unmatched message id %s", data.id);</code>
<code>} else {//notifications</code>
<code>this.emit(data.method, data.params);</code>
<code>console.warn("message of unknown encoding");</code>
<code>//find tab info</code>
<code>request("http://localhost:9222/json", function(e, res, data) {</code>
<code>data = json.parse(data);</code>
<code>var url = data[0].websocketdebuggerurl;</code>
<code>if(!url) {</code>
<code>throw new error("no url");</code>
<code>var client = new websocketclient();</code>
<code>//once it's connect, start our actions</code>
<code>client.on("connect", function(conn) {</code>
<code>console.log("client connceted");</code>
<code>var commander = new commander(conn);</code>
<code>//shoud enable this freatures</code>
<code>commander.send("network.enable",{});</code>
<code>commander.send("page.enable",{});</code>
<code>//listen to wanted notifications</code>
<code>commander.on("network.requestwillbesent", function(data) {</code>
<code>console.log("[%s] %s %s: %s", chalk.green(data.timestamp), chalk.blue("willsend"), data.requestid, data.request.url);</code>
<code>commander.on("network.loadingfailed", function(data) {</code>
<code>console.log("[%s] %s %s", chalk.green(data.timestamp), chalk.red("loadfail"), data.requestid);</code>
<code>commander.on("network.loadingfinished", function(data) {</code>
<code>console.log("[%s] %s %s", chalk.green(data.timestamp), chalk.magenta("loaddone"), data.requestid);</code>
<code>commander.on("network.responsereceived", function(data) {</code>
<code>console.log("[%s] %s %s: %s status %s %s", chalk.cyan(data.timestamp), chalk.red("resprecv"), data.requestid, data.type, data.response.status, data.response.headers["content-length"]);</code>
<code>commander.on("network.requestservedfromcache", function(data) {</code>
<code>console.log("%s %s", chalk.gray(data.timestamp), chalk.red("respcache"), data.requestid);</code>
<code>commander.on("page.domcontenteventfired", function() {</code>
<code>console.log(chalk.bggreen("ondomcontentload\t\t\t\t\t\t\t\t"));</code>
<code>commander.on("page.loadeventfired", function() {</code>
<code>console.log(chalk.bgcyan("onload\t\t\t\t\t\t\t\t"));</code>
<code>//navigate to target page</code>
<code>commander.send("page.navigate", {url: "http://www.taobao.com"});</code>
<code>client.connect(url);</code>
運作後的結果如下:
本篇内容僅僅介紹調試協定這個概念,以及它的通訊原理。并且,我們通過一個實驗,來展示這套協定的強大特性。後面,我們還會讨論其他浏覽器的調試協定,以及移動裝置的調試。
本文來自雲栖社群合作夥伴“linux中國”,原文發表于2013-04-02.