Chrome DevTools Inspector 擴充套件實踐

雲音樂技術團隊發表於2022-04-27

截圖自:https://developer.chrome.com/...

本文作者:原草

前言

雲音樂 app 內有很多使用 react native 開發的應用,例如雲貝中心、雲音樂商城、會員中心等。為了更好地提升開發效率,改善除錯體驗,團隊決定開發 react native 除錯工具,通過為 react native debugger 增加一些擴充套件功能,實現業務資訊的展示和除錯能力,例如:跨端通訊資訊展示、網路資訊展示等。

因為雲音樂 react native 應用的網路請求都是通過客戶端傳送,因此想把客戶端網路資訊通過 chrome devtools protocol 展示在 network 裡。經過嘗試後,期間遇到了一些問題和做出的嘗試記錄如下。

chrome devtools 介紹

chrome devtools 是前端常用的除錯工具,整合在 chrome 裡。web 應用通過 chrome devtools protocol 與 devtools frontend (平時開啟 f12 除錯皮膚的頁面,也是個前端專案,下面用 frontend 表示)建立連線,將被除錯應用資訊傳遞到 frontend 上展示。
web 除錯方案示例

ChromeDevTools/devtools-frontend 是 chrome devtools frontend 的專案程式碼。也可以作為單獨的專案使用,用於自定義除錯功能。專案內使用的 devtools frontend 是 3964 版本,新版的 frontend 對打包方式、程式碼結構都做了調整。

devtools frontend 模組載入

通過配置遠端除錯埠 --remote-debugging-port=9222 啟動 chrome,http://localhost:9222/ 可以看得到除錯連結,例如:http://localhost:9222/devtools/inspector.html?ws=localhost:9222/devtools/page/7B2421304FE8EF659B264D4F476083DA 是一個 inspector 的地址,從 inspector.html 入手看下專案如何啟動。

inspector.html 載入 Runtime.js 和 inspector.js,inspector.js 只做了一件事

Runtime.startApplication('inspector');

模組載入過程如下
Runtime.startApplication('inspector') 載入過程

模組經過載入解析過程後,啟動應用:

  1. 前端應用通過讀取 module.json 獲得模組資訊;
  2. 例項化 new Runtime,建立 Runtime -> Module -> Extension 依賴關係;
  3. 載入核心模組資源(script 和 css 資源);
  4. 啟動核心模組入口(Main.Main)。

到此是模組的載入過程。

inspector 啟動方式

檢視 inspector.json 內容

{
  "modules" : [
    { "name": "screencast", "type": "autostart" }
  ],
  "extends": "devtools_app",
  "has_html": true
}

inspector 應用繼承自 devtools_app。只比 devtools_app 多了一個模組 screencast 頁面快照,可以用於實時檢視頁面變化,。

devtools_app 應用就是常用的 devtools 工具,可以用於 web 應用除錯,devtools_app.json 依賴的模組內容如下:

{
  "modules" : [
    { "name": "emulation", "type": "autostart" },
    { "name": "inspector_main", "type": "autostart" },
    { "name": "browser_debugger" },
    { "name": "elements" },
    { "name": "network" },
    ...
  ],
  "extends": "shell",
  "has_html": true
}

devtools_app.json 繼承自 shell.json,應該就是 devtools 依賴的核心模組。shell.json 依賴的模組內容如下:

{
  "modules" : [
    { "name": "bindings", "type": "autostart" },
    { "name": "common", "type": "autostart" },
    { "name": "components", "type": "autostart"},
    { "name": "extensions", "type": "autostart" },
    { "name": "host", "type": "autostart" },
    { "name": "main", "type": "autostart" },
    { "name": "protocol", "type": "autostart" },
    { "name": "ui", "type": "autostart" },
    ...
  ]
}

devtools_app 包含的 inspector_main 模組,將通過連結引數中傳入的 ws 引數建立 socket 連線用於獲取監聽後端 protocol 資訊。

SDK._createMainConnection = function() {
  const wsParam = Runtime.queryParam('ws');
  const wssParam = Runtime.queryParam('wss');
  if (wsParam || wssParam) {
    const ws = wsParam ? `ws://${wsParam}` : `wss://${wssParam}`;
    SDK._mainConnection = new SDK.WebSocketConnection(ws, SDK._websocketConnectionLost);
  } else if (InspectorFrontendHost.isHostedMode()) {
    SDK._mainConnection = new SDK.StubConnection();
  } else {
    SDK._mainConnection = new SDK.MainConnection();
  }
  return SDK._mainConnection;
};

自此建立與被除錯專案連線,想了解其他模組的內容,也可以按照上面的思路檢視。

react native debugger 除錯過程

react native debugger 是一個用於除錯 react native 應用程式的 electron 獨立程式,基於官方遠端除錯功能,增加了 react-devtools-coreredux-devtools-extension 作為擴充套件支援。

react native 在 app 上並不是 web 頁面,react native dedug 過程 在開啟 debug remote 後:

  1. app 傳送 /launch-js-devtools 請求給 server 開啟除錯 tab,並建立 socket 連線;
  2. tab 載入 debugger-ui 頁面, 與 server 建立 socket 連線(/debugger-proxy?role=debugger&name=Chrome);
  3. tab 開啟 debugger-ui 同時,啟動了 worker 執行 debuggerWorker.js 指令碼,用來執行 bundle 程式碼;
  4. 最後通過 inspect worker 的方式來達到對 react native 應用的除錯。
    react native debugger 除錯方案示例

react native 中如果使用 fetch 或是 XMLHttpRequest 傳送的網路請求,是可以在 frontend 除錯過程中得到的。雲音樂 app 內的 react native 應用的網路請求都是通過客戶端傳送,那麼客戶端網路資訊通過 chrome devtools protocol 展示在 network 裡,要怎麼做呢?

chrome devtools inspector 擴充套件

chrome devtools protocol

增加 network 開始想到最直接的方式,就是通過 web 和 frontend 之間的 socket 鏈路。
使用 protocol 方案

chrome devtools protocol(簡稱 CDP) 是 devtools 和 web 應用間傳遞的偵錯程式協議。基於 websocket 建立 devtools 和瀏覽器核心的快速資料通道,chrome devtools protocol 也允許第三方對 web 應用進行除錯。

CDP 協議按域 Domain 劃分能力,每個域下有 Method、Event 和 Types。

Method 對應 socket 通訊的請求/響應模式,Events 對應 socket 通訊的釋出/訂閱模式,Types 為互動中使用到的實體。

  • Method: 包含 request/response,如同非同步呼叫,通過請求資訊,獲取相應返回結果,通訊需要有 message id
request: {"id":1,"method":"Page.canScreencast"}
response: {"id":1,"result":{"result":false}}
  • Event:發生的事件資訊,用於傳送通知資訊。

    {"method":"Network.loadingFinished","params:{"requestId":"14307.143","timestamp":1424097364.31611,"encodedDataLength":0}}
  • Types:互動實體

    Network.BlockedReason
    Network.ConnectionType
    Network.Cookie
    ...

在 chrome 中可以使用 "Protocol Monitor" 傳送 Method。
chrome Protocol Monitor 除錯

在 electron 內,可以通過 app.commandLine.appendSwitch 新增 chrome 的啟動引數 remote-debugging-port 開啟 chrome 除錯埠 9222。

const CHROME_REMOTE_DEBUG_PORT = 9222; // devtools frontend 除錯埠
app.commandLine.appendSwitch('remote-debugging-port', `${CHROME_REMOTE_DEBUG_PORT}`);

通過 http://localhost:9222/json 介面請求找到當前頁面 React Native Debugger 的除錯地址 webSocketDebuggerUrl。

[ {
   "description": "",
   "devtoolsFrontendUrl": "/devtools/inspector.html?ws=localhost:9222/devtools/page/ADC97007929236D82B4613E4E6B36C4B",
   "id": "ADC97007929236D82B4613E4E6B36C4B",
   "title": "React Native Debugger - Waiting for client connection (port 8081)",
   "type": "page",
   "url": "file:///Users/jarry/netease/react-native-debugger/electron/app.html",
   "webSocketDebuggerUrl": "ws://localhost:9222/devtools/page/ADC97007929236D82B4613E4E6B36C4B"
} ]

然後建立一個 socket 通道給 app。

嘗試發現 nework 資訊並沒有展示,app 通過 socket 連線傳送的資訊給到瀏覽器核心,但瀏覽器核心並沒有將資訊轉發給 frontend,這裡理所當然的把 web 應用作為除錯資訊處理物件。

採用 proxy 方式

web 應用和 frontend 在同一網段,可以使用上面的除錯方式,如果兩部分不在一個內網,就需要用到遠端除錯方式。在 web 應用和 frontend 之間建立 proxy 服務,實現 CDP 訊息轉發,以達到跨域除錯目標。

受啟發於跨域除錯方式,proxy 在 web 和 frontend 之間做轉發的同時,也可以轉發其他的 CDP 訊息。

proxy 示意圖

  1. 啟動 ws 用作 proxy 服務

proxy.js 示例程式碼:

const WS_PROXY_PORT = 9233; // proxy ws 埠
const CHROME_REMOTE_DEBUG_PORT = 9222; // devtools frontend 除錯埠

const proxyStart = async () => {
  // 建立 proxy server

  const server = http.createServer((request, response) => {
    response.writeHead(404);
    response.end();
  });
  server.listen(WS_PROXY_PORT);

  const wss = new WebSocket.Server({ server });

  wss.on('connection', (ws, request, client) => {
    // 處理來自 remote-debug 的請求
    if (request.headers['sec-websocket-protocol'] === 'remote-debug') {
      axios.get(`http://127.0.0.1:${CHROME_REMOTE_DEBUG_PORT}/json`).then(res => {
        // 查詢被除錯頁 webSocketDebuggerUrl
        const { data } = res;
        if (data && data.length > 0) {
          for (let i = 0; i < data.length; i++) {
            const page = data[i];
            if (page.title.indexOf('React Native Debugger') === 0) {
              debugConnection = new WebSocket(page.webSocketDebuggerUrl);
              });
              // 把被除錯頁面的資料全部轉發給偵錯程式前端
              debugConnection.on('message', (message) => {
                if (frontendConnection) {
                  if (debugMessageArr.length) {
                    debugMessageArr.forEach(item => {
                      frontendConnection.send(item);
                    });
                    debugMessageArr = [];
                  }
                  frontendConnection.send(message);
                } else {
                  debugMessageArr.push(message);
                  console.log('無法轉發給 frontend, 沒有建立連線\n');
                }
              });
              break;
            }
          }
        }
      }).catch(error => {
        console.log(error);
      });
    }

    // 處理來自 frontend 的請求
    if (request.url === '/frontend') {
      frontendConnection = ws;
      frontendConnection.on('message', (message) => {
        // 把偵錯程式前端的請求直接轉發給被除錯頁面
        if (debugConnection) {
          if (frontendMessageArr.length) {
            frontendMessageArr.forEach(item => {
              debugConnection.send(item);
            });
            frontendMessageArr = [];
          }
          debugConnection.send(message);
        } else {
          frontendMessageArr.push(message);
          console.log('偵錯程式後端未準備好, 先開啟被除錯的頁面');
        }
      });
    }

    // 處理來自客戶端請求
    if (request.url === '/app') {
      appConnection = ws;
      appConnection.on('message', (message) => {
        // 把客戶端的請求直接轉發給前端偵錯程式
        if (frontendConnection) {
          if (debugMessageArr.length) {
            debugMessageArr.forEach(item => {
              frontendConnection.send(item);
            });
            debugMessageArr = [];
          }
          frontendConnection.send(message);
        } else {
          debugMessageArr.push(message);
          console.log('無法轉發給frontend,沒有建立連線\n');
        }
      });
    }
  });
};

這裡處理了三方面的資料

  • remote-debug:被除錯頁面資料。通過查詢 http://127.0.0.1:${CHROME_REMOTE_DEBUG_PORT}/json 獲取 webSocketDebuggerUrl,建立 debugConnection 連線,被除錯資訊從此來,傳送給 frontend;
  • frontend:偵錯程式資料。轉發此資料給 debugConnection。
  • app:客戶端資料,轉發此資料給 debugConnection。
  1. remote-debug 建連

remote-debug 跑在 web,是為了通知 proxy 建立連線的,因為我的 proxy 跑在 electron,和 remote debug 同域,所以從 proxy 直接獲取 webSocketDebuggerUrl。也可以在 remote-debug 內獲取 webSocketDebuggerUrl 用通知的方式告訴 proxy。

const ws = new WebSocket('ws://localhost:9233', 'remote-debug');
  1. frontend 建連

frontend 由於要更換 ws 地址,因此這裡沒有采用 chrome 自帶的 devtools,而是本地啟動的 frontend 服務,更換 ws 連線為 proxy frontend 連線。有關 frontend web 服務會在下一節介紹。

http://localhost:8090/front_end/devtools_app.html?ws=localhost:9233/frontend
  1. app 側

app 端會在 okhttp 框架裡新增攔截器,攔截所有請求資料然後通過 ws 傳送給 proxy。

自此就實現了 inspector 擴充套件 network 資訊的功能。

frontend 本地服務

因為要自定義開發,devtools frontend 版本是跟隨 chrome 版本釋出的,最好採用和 chrome 版本一致的 frontend,比如我這裡通過 process.versions.chrome 拿到 electron 內 chrome 版本號是 78.0.3904.130,對應的 devtools 版本號就是 3904,但能獲得到最早的版本就是 3964了,使用 3964 暫時也沒遇到其他問題。

devtools-fronend->scripts->server.js

啟動本服務就可以得到一個執行在 8090 的 frontend 服務。

frontend 啟動路徑

總結

本文從 chrome devtools inspector 擴充套件為出發點,介紹了 devtools frontend 除錯原理及模組載入方式,react native debugger 除錯原理,跨域除錯方案,最終實現 devtools inspector 的除錯擴充套件。文內涉及各除錯工具知識較多,大多做了概括,技術細節也都留了文件連結可以自行獲取,希望對做 chrome 除錯工具的同學有所啟發和幫助。歡迎對文中相關問題批評指正。

參考連結

本文釋出自網易雲音樂技術團隊,文章未經授權禁止任何形式的轉載。我們常年招收各類技術崗位,如果你準備換工作,又恰好喜歡雲音樂,那就加入我們 grp.music-fe(at)corp.netease.com!

相關文章