Luna:你想要的 React Native 除錯工具

Shopee技術團隊發表於2022-01-28
本文作者Shopee Digital Purchase 前端團隊。

1. 背景

React Native(以下簡稱 RN)目前在 Shopee 前端團隊得到大量應用。RN 雖然有很多優勢,但是其開發和除錯流程與 Mobile Web 相比卻不那麼友好,特別是在執行時的除錯。

在開發模式下,雖然 RN 提供了官方的除錯工具,但是相比純前端的瀏覽器 Devtool,它的功能比較弱;而非開發模式下,例如 Test 和 UAT 環境,RN 程式碼被打包成了一個 Bundle,這個時候官方除錯工具也派不上用場了,這不僅對測試同學的問題復現產生阻礙,也使開發同學的問題定位變得更加困難。

目前業界對於 RN 的除錯雖然有工具,但或多或少都存在缺陷(如下圖所示),而且這些工具都是針對開發模式下的除錯,對於打包後的生產環境的除錯往往還是需要靠人肉去做,效率比較低下。

因此一款能夠幫助在非開發環境定位問題的工具顯得尤為重要,Luna 就此應運而生,本文將介紹這款 RN 工具關鍵技術的設計以及實現。

2. 功能介紹

先通過下面幾張圖瞭解一下 Luna:

從圖片可以看出來,Luna 是一款 RN 的應用內除錯工具,更偏向於解決生產環境除錯的痛點

Luna 由一個橙色的觸發按鈕以及佔據半屏的本體組成。本體包含了 Log、Network、Redux 和 Shopee 這四個版塊,分別承載了日誌記錄、網路請求檢視、Redux 樹檢視以及 Shopee 相關資訊檢視的功能。

其中,Log 和 Network 作為核心模組存在,而 Shopee 和 Redux 則是作為 Luna 提供的公共外掛引入進來的。這種 Core-Plugin 模式就是 Luna 現在的執行模式:預設提供 Log、Network 等功能,也支援使用者編寫自定義模組匯入到 Luna。

四大版塊的功能如下:

1)Log 版塊

Log 版塊接管了 console.log,將所有 Log 和未捕獲的錯誤收集到 Luna ,然後倒序展示出來。它支援按 Log 的型別進行過濾,也支援對 Log 進行模糊查詢。如下圖所示:

2)Network 版塊

Network 版塊收集了頁面發出的請求資訊,包含了請求狀態、請求耗費時長、請求頭、請求體以及響應頭和響應體等等,使用者可以方便地檢視 API 請求。

3)Shopee 版塊

Shopee 版塊提供了一些 Shopee App 相關的功能,比如便捷的翻譯文案切換、Cookies 檢視、DataStore 儲存檢視與刪除,還有使用者 ID / 名字與裝置系統資訊,以及版本號相關的資訊檢視。這些功能可以幫助開發者更方便地除錯應用,也便於 QA 更快地復現與定位 bug。

4)Redux 版塊

Redux 版塊展示了 Store(共享資料儲存倉庫)樹,方便使用者檢視整個 Store 的狀態。

3. 方案設計

3.1 整體設計

Luna 作為一個 monorepo 多包單倉庫架構的專案,包含了 Core、Shopee Plugin 和 Redux Plugin 三個包模組。

其中,Core 核心模組包含了三大部分:Log 日誌版塊、Network 網路版塊、Plugins 外掛接入版塊。下文將一一介紹每個模組的設計。

3.2 Core

Core 模組是 Luna 的核心模組,作為一個單獨的 npm 包存在,提供了最基本的功能與外掛接入的能力。Core 模組作為一個 Provider 巢狀在元件樹的根部,接受業務程式碼,並將 Luna 插入進去。Core 使用 mobx 作為儲存,維護了 Log 日誌和 Network 記錄的收集與展示、以及自定義外掛的控制等等功能。

3.2.1 接入方案

Luna 的靈感源自於 Web 端開源的 vConsole 和 Eruda 這兩款除錯工具,但在 Luna 的接入方案選擇中,我們碰到了在 Mobile Web 中從未碰到過的難題:在現代化 Web 開發中,不論是 Vue 還是 React,只要是單頁應用,都會有一個用於掛載的根節點,以這個根節點為起點構建整個元件樹。所以除錯工具也只需要掛在某一根節點下,即可感知整個應用的狀態:

而在 React Native 中,每個頁面(View)都有自己的根節點(如下圖所示),不同的頁面之間並沒有一個公共的祖先節點,如果要保證每個頁面都能訪問到 Luna,就得在每個頁面都單獨進行一次注入,不僅接入成本陡增,而且資料的保留也成了一大難題。

所以如何保證 Luna 在各個頁面都能訪問到,並且還能保留不同頁面資料、以及在發生錯誤時不影響到 Luna,同時還要減少頁面接入的成本,成為了一個難題。那麼 Luna 是怎麼做的呢?

首先,Luna 將初始化與頁面註冊解耦,將 Luna.init 前置到了應用初始化時。這使得資料的收集與頁面的註冊分離,保證了頁面的切換不會導致資料的丟失。

import Luna from "@shopee/luna";
Luna.init();

接著,Luna 利用 Shopee Plugin 重寫了用於註冊 Shopee RN Page 的方法,用新的元件包裹了傳入的頁面元件,同時將 Luna 也包含在裡面,以 HOC 的形式將元件返回到外層。每一個使用這個註冊頁面的方法所註冊的頁面,都會把 Luna 自動包含在頁面裡,無需在每個頁面手動引入 Luna,同時每個頁面也都可以訪問到 Luna。

最後,Luna 還對傳入的 Component 包裹了一層 ErrorBoundary,用於捕獲頁面產生的執行時錯誤,使得在頁面產生錯誤時 Luna 還可以訪問得到,並且可以在 Luna 裡看到報錯的資訊。

3.2.2 Log

日誌收集

Log 模組顧名思義,用於顯示系統和使用者列印出來的日誌。

Luna 劫持了全域性變數 global.console,對各種型別的 Log 進行收集;同時, Luna 也劫持了 console.tron.log,收集開發時使用 Reactotron 列印出來的相關 Log;Luna 還劫持了 ErrorUtils,將未捕獲的錯誤也一併收集到日誌 Store 裡。這三種型別的日誌就是 Log 版塊的資料來源。

Luna 以類似於中介軟體的做法劫持了全域性的 console,劫持的過程中將其加入 Log store,然後執行其原本的執行函式,其主要程式碼如下所示:

export const overrideConsole = (consoleStore) => {
  const mixinType = [
    LOG_TYPE.LOG,
    LOG_TYPE.ERROR,
    LOG_TYPE.WARN,
    LOG_TYPE.DEBUG,
    LOG_TYPE.INFO,
  ];
  mixinType.forEach((type) => {
    // @ts-ignore
    const originConsoleFun = global.console[type];
    // @ts-ignore
    global.console[type] = (...params) => {
      consoleStore.addLog(params, type);
      originConsoleFun(...params);
    };
  });
};

日誌展示

Log 日誌包含了型別篩選、搜尋框和日誌列表,由於 Luna 日誌的型別眾多、內容複雜且一直處於一個動態更新的狀態,所以很容易產生效能問題。所以在日誌列表的展示部分,我們做了大量的效能優化,主要包含兩個部分,如下圖所示:

1)巢狀型別展示優化

由於開源方案的樹狀展示庫存在相容性問題,我們選擇自己編寫樹狀展示元件,用於解決資料型別複雜、資料量大帶來的展示問題。它具有以下特點:

  • 支援多行文字的展開與收縮,收縮時只顯示部分內容;
  • 對大陣列與物件採取了懶載入方案,展開後只展示小於 100 行的內容,使用者每點選一次剩餘部分(N),則展示後 N*100 條資料。這種做法避免了大資料顯示所帶來的效能問題;
  • 對一行的超長文字進行換行控制,保持每個 Log 不超過三行,保證每屏的 Log 數量是受控的。

2)列表滑動效能優化

Luna 的 Log 並不是一次性載入完畢,而是實時生成的。這使得在列表滑動過程中很可能同時有新的資料產生,而使用者往往需要往下滑動,來尋找他們列印出來的 Log。所以 Luna 針對滑動的效能也做了一些特定優化:

  • Luna 採用了 FlatList 來渲染 Log 列表,同時還在 Log 收集時隱式生成 ID ,作用於 FlatList 的 keyExtractor,以此提高渲染效率;
  • 由於 Log 是動態生成的,這對 FlatList 的效能有著不小的影響。針對於此,Luna 將 Log 列表進行倒序顯示,將最後產生的資料,也就是使用者點選 Luna 時最關心的資料放在 FlatList 的最前面,同時列印出時間。這樣就減少了使用者滑動的頻率;
  • 我們還計劃對 Luna 進行更嚴格的日誌分頁載入,將顯示和儲存的 Log 列表分開,在滑動進行到底時,獲取儲存的 Log 列表的「下一頁」,徹底保證動態資料產生過程中的列表滑動效能。

3.2.3 Network

Network 模組的資料收集源於 XMLHttpRequest。Luna 劫持了 React Native 的 XMLHttpRequest,重寫了 open、send 和 setRequestHeader 方法,將每個請求,以及請求相關的欄位都儲存到 Network 列表裡。由於 RN 的 Fetch 底層其實也是使用了 XHR,所以對 XHR 作劫持,可以達到全覆蓋的效果。Network 劫持的主要程式碼如下所示:

export const overrideNetwork = (consoleStore) => {
  originOpen = XMLHttpRequest.prototype.open;
  const originSetHeader = XMLHttpRequest.prototype.setRequestHeader;
  XMLHttpRequest.prototype.open = function (...args) {
    this._xmlItem = { openData: args };
    this.addEventListener("load", () => {
      const xmlItem = this._xmlItem;
      const requestHeaders = this._requestHeaders;
      const endTime = new Date().getTime();
      const time = endTime - xmlItem.startTime;
      consoleStore.addNetworkLog({
        url: this.responseURL,
        method: xmlItem.openData[0],
        status: this.status,
        rspHeader: this.getAllResponseHeaders(),
        response: this.response,
        body: xmlItem.sendData,
      });
    });
    originOpen.apply(this, args);
  };
};

而在 Network 列表的展示方案中,我們則加入了很多細節上的考量,比如:

  • 優先展示請求的 URL 的末尾 Path;
  • 根據 response 的狀態碼的不同設定不同的底色;
  • 根據請求時間的長短展示不同的時間單位。

這些細節是在日積月累的使用中進行的點滴改進,它們確實讓 Luna 實際的使用者體驗更上了一層樓。

3.3 Plugins

3.3.1 外掛機制

為什麼需要外掛機制?

在介紹什麼是外掛機制之前,你可能內心會有一個疑問,為什麼會有外掛機制呢?究其原因,Luna 在實現功能的時候,有一些功能是依託於 Shopee 的 SDK 實現的;另一部分功能如 Redux 是非必選的,使用者使用的狀態管理框架可能是 mbox;為了保持 Luna 核心模組的純淨,以及保留 Luna 對於非 Shopee 框架下的可擴充性,我們解開了這些不必要的耦合,將 Shopee 模組與 Redux 模組改造成外掛機制,供使用者按需引用。

什麼是外掛機制?

Luna 在核心模組之外,Core 還支援自定義外掛。Luna 提供了兩個第一方外掛:Redux Plugin 和 Shopee Plugin,如果你有自己 App 的定製化需求,也可以非常方便地編寫自己的外掛,匯入到 Luna 裡,如下圖所示。

3.3.2 官方外掛

Luna 也採用外掛機制提供了兩個官方外掛:Redux Plugin 和 Shopee Plugin,這兩個包作為單獨的 npm 包供有需要的使用者引入。其中:

  • Redux Plugin 作為一個 Redux 中介軟體存在,通過 Store.getState 獲取到 Redux 的狀態,並將其顯示在介面上。使用者可以很方便地查詢到當前 Redux 的儲存值。
  • Shopee Plugin 是依託於 Shopee React Native SDK 的一個外掛,專門針對於 Shopee App 內的專案開發。它通過 Shopee 的 SDK 提供了許多功能,這個外掛主要面向 Shopee 內部的開發與測試同學,方便他們進行 Shopee App 內的除錯。

3.3.2 開發自定義外掛

除了官方外掛之外,使用者還可以自己擴充套件外掛,如何開發一個 Luna 的外掛呢?Luna 的外掛機制十分類似 Vue 的 install-use 機制,但是它省略了 Vue 外掛的 install 步驟,只要需要提供元件內容注入到 Luna 提供的 use 方法就可以。所以其實步驟非常簡單,只需要兩步:

  • 編寫你的元件,宣告名稱;
  • 將元件和名稱匯入到 Luna Core 的例項。

Luna 便可以識別到你的元件,顯示在主介面上了,接下來你就可以在外掛裡新增自己所需的功能。

4. 未來展望

Luna 現階段已經在 Shopee 的一些業務裡穩定執行,也受到了使用它的開發和測試同學的一致好評。在未來我們會朝兩個大的目標努力:

1)自動化 Luna 接入

現階段 Luna 的接入還是具有侵入性的人工程式碼接入,未來我們打算通過部署平臺,在部署的時候自動將 Luna 接入進去,並且只在開發、測試環境下生效,不僅可以實現 0 程式碼的接入成本,也不影響生產環境,還減少了打包後的程式碼體積。

2)元件樹狀態檢視器

在 Web 端幾乎每個開發者都會使用 React Devtool,而其中深受大家喜愛的就是 Components 模組,它展示了開發時的整棵元件樹,以及每個元件相關的 Props、State 和 Hooks。而在 React Native 端現時還沒有一個類似 React Devtool 一樣好用的開發除錯工具,而對 RN 的狀態檢視又是開發者的一大痛點,因此 Luna 計劃在未來增加對於元件樹以及元件狀態的檢視器,屆時在 RN 上同時檢視 Log、Network 以及元件狀態,將變得不再困難。

相關文章