天天看點

Webkit 遠端調試協定初探Webkit 遠端調試協定初探

Webkit 遠端調試協定初探Webkit 遠端調試協定初探

任何做過 web 開發的同學,都避免不了在浏覽器内進行調試。而大部分同學的首選工具,就是 chrome devtools。devtools 本身我們無需多說,是一個大家不能再熟悉的工具了。但是埋藏在 devtools 下面的開放協定以及它賦予的衆多可能性,至今仍未見到充分的剖析和應用。

為什麼我們關注 devtools:

原因 1:devtools 是開源項目

原因 2:它足夠簡單

devtools 僅僅是簡單的由 html、javascript、css、images 組成的,本質上就是一個 webapp,純粹的前端應用。當你去了解、修改它時,你不需要了解 c++ 和任何編譯的知識。

原因 3:它的應用架構足夠開放,滿足任何形式的功能擴充。

事實上 devtools 是一個充分子產品化的 javascript 網頁應用。它的每個功能你都可以去擴充(僅需要了解 javascript)。

原因 4:大部分前端都已經習慣它并且喜歡它。

<a></a>

談到遠端調試前,有必要先了解各元件之間的關系。

Webkit 遠端調試協定初探Webkit 遠端調試協定初探

浏覽器擁有多個 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,你會得到這樣的界面:

Webkit 遠端調試協定初探Webkit 遠端調試協定初探

點選 “淘寶網” 的方框,就進入頁面的調試界面了:

Webkit 遠端調試協定初探Webkit 遠端調試協定初探

注意看位址欄,我們通路的是一個标準的 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 通訊。例如:

Webkit 遠端調試協定初探Webkit 遠端調試協定初探

以前用 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>

運作後的結果如下:

Webkit 遠端調試協定初探Webkit 遠端調試協定初探

本篇内容僅僅介紹調試協定這個概念,以及它的通訊原理。并且,我們通過一個實驗,來展示這套協定的強大特性。後面,我們還會讨論其他浏覽器的調試協定,以及移動裝置的調試。

本文來自雲栖社群合作夥伴“linux中國”,原文發表于2013-04-02.

繼續閱讀