Ts + React + Mobx 實現移動端瀏覽器控制檯

自從使用 Typescript 寫 H5 小遊戲後,就對 Ts 產生了依賴(智能提示以及友好的重構提示),但對於其 Type System 還需要更多的實踐。

最近開發 H5 小遊戲,在移動端調試方面,爲求方便沒有采用 inspect 的模式。用的是粗暴的 vConsole,用人家東西要學會感恩,所以決定去了解它的原理,最後用 Ts + React 碼一個移動端瀏覽器控制檯,算是 Ts + React 實戰

通過該教程可以學習:

  • Ts + React + Mobx 開發流程
  • 基本的 Type System
  • 一些 JavaScript 基礎概念
  • 瀏覽器控制檯相關知識

    • Console
    • NetWork、XHR
    • Storage
    • DevTool 核心渲染代碼

項目源碼 供上, 第一次用 Typescript + React 碼項目,記錄迭代的過程,有興趣入坑的可 star 一下 期待 CodeReview。

開始

本着快速開發的理念(本人要帶娃),於是基於 Create React App 腳手架搭建項目,UI 框架使用了同樣採用
Ts 編寫的 AntMobile。 開始項目講解前,顯然需要對這兩個有一定的瞭解 ( 建議可作爲進一步學習 Ts + React 的參考 )

下面,先來看下預覽圖片

UI 很簡單,按功能劃分爲

  • Log 、 System
  • Network
  • Elemnet
  • Storage

主要從以上這幾個功能模塊展開

PS: 教程會略過一些,諸如如何支持 stylus ( 項目執行過 yarn run eject ),interface 要不要加 I,render 要不要 Public, 如何去除一些 Tslint 等。( 跟蹤文件 git history 可略知一二 )PWA 等

基本代碼風格

通篇會按這種風格 ( 並不是最佳實踐 ) 去編寫組件,( 比較少無狀態組件,也沒有高階組件的應用 )。

import React, { Component } from 'react';

interface Props {
  // props type here
}

interface State {
  // state type here
}

export default class ClassName extends Component<Props, State> {
  // state: State = {...}; 我更喜歡將 state 寫在這。

  constructor(props: Props) {
    super(props);
    this.state = {
      // some state
    };
  }

  // some methods...

  render() {
    // return
  }
}

Log

調試控制檯最常用是 Log,與之不可分割的 API 就是 window.console 。常用的方法有['log', 'info', 'warn', 'debug', 'error']。UI 表現上可分爲 Log,Warn,Error 三類。

如何自己實現一個控制檯 console 面板呢? 其實很簡單,只需要 “重寫” window.console 對應的這些方法,然後再調用系統自帶的 console 方法即可。這樣你就可以實現在原有方法基礎上附加一些你想要的操作。( 可惜這麼做會有一些副作用,後面會講到。 )

代碼邏輯如下:

const methodList = ['log', 'info', 'warn', 'debug', 'error'];

methodList.map(method => {
  // 1. 保存 window 自帶 console 方法。
  this.console[method] = window.console[method];
});

methodList.map(method => {
  window.console[method] = (...args: any[]) => {
    // 2. 做一些保存數據及展示的操作。

    // 3. 調用原生 console 方法。
    this.console[method].apply(window.console, infos);
  };
});

由於項目我們用的是 React ,由於是數據驅動,所以只需要關心數據即可。

在 Log 中的數據,其實就是 console.log(參數) 中的參數,再將這些參數用 mobx 以數組的形式統一管理後交由 List 組件渲染。

import { observable, action, computed } from 'mobx';

export interface LogType {
  logType: string;
  infos: any[]; // 來自 console 方法的參數。
}

export class LogStore {
  @observable logList: LogType[] = [];
  @observable logType: string = 'All';

  // some action...
}

export default new LogStore();

數據和列表展示都有了,那麼 如何用樹形結構展示基本數據類型與引用類型

基本類型 ( undefined,null,string,number,boolean,symbol )展示比較簡單,這邊講一下引用類型 ( Array,Object )的展示實現。對應項目中就是 logView 組件。

logView 組件

從之前的預覽圖片可以大致看到整個數據展示結構,都是 key-value 的形式。

這裏跟 Pc 端瀏覽器控制檯不一樣的是,沒有展示 __proto__ 相關的東西。然後,function 只是以方法名加括號的形式展示,如 log()

接下來我們看下這個 UI 對應的 html 結構。

我們需要展示的就只是 key 和 value 以及父子縮進,典型的樹形結構,遞歸可以搞定。

對於 Object 直接就是 key-valueArray 其實也是索引和值的對應關係。

基本邏輯:

<li className="my-code-wrap">
  <div className="my-code-box">
    // 1. 判斷是否需要顯示展開圖標
    {opener}
    <div className="my-code-key">
      // 2. 顯示 key
      {name}
    </div>
    <div className="my-code-val">
      // 3. 根據值類型,選擇其展示方式
      {preview}
    </div>
  </div>
  // 4. 如果是 Object 或 Array,則重複 1.
  {children}
</li>

至此一個簡單的 log 展示邏輯就完成了。接下來說一下控制檯裏面的 JS 命令行執行。

  sendCMD() {
    return (cmd: string) => {
      let result = void 0;
      try {
        result = eval.call(window, '(' + cmd + ')');
      } catch (e) {
        try {
          result = eval.call(window, cmd);
        } catch (e) {
          ;
        }
      }
      // mobx中的 action
      logStore.addLog({ logType: 'log', infos: [result] })
    }
  }

eval() 函數會將傳入的字符串當做 JavaScript 代碼進行執行。但他是一個危險的函數,他執行的代碼擁有着執行者的權利。這裏直接讓用戶傳參,意味着用戶可以決定執行什麼樣的代碼(包括惡意代碼),所以這種瀏覽器控制檯是絕對不能出現在生產環境的

小結

log 的實現不難,就在原有 winodw.console 方法的基礎上,添加參數收集功能,並交由 mobx 管理。再將參數通過樹形結構的方式展示給用戶。但是,這種方式可能造成非常多不必要的渲染,每次調用 console 方法 ( 包括 error 和 warning),都會觸發相應的 render ,如果在 log 組件的 render 方法裏面調用 console 就會造成棧溢出 (相當於在 render 調用 setState),不過好在這只是用於開發中的調試階段,另外,對於線上 bug 排查,我們可以用 charles 代理的方式注入代碼而無需影響原有代碼。即便如此,前端自己實現的瀏覽器控制檯還是無法跟原生控制檯媲美的 (最多用來看下有沒有報錯,又不想使用麻煩的 inspect 模式) ,比如追蹤調用棧,以及 script error。所以,爲什麼要使用 Typescript,很重要的一點是儘可能地在開發階段規避一些 bug。但面對海量級用戶,手機千奇百怪,這時就只能通過前端異常監控,專業的有 fundebug 或者自己簡單處理一下。扯遠了,還是回到我們走馬觀花的下一部分 system 吧。

System

system 主要用於展示瀏覽器端不太容易查看的信息,比如當前瀏覽器的用戶代理(user agent)字符串或者當前真實的 URL (由於某些原因,URL 可能被修改)。當然這些要展示的信息跟業務以及需要調試的內容關聯比較大,因此這個面板還是自定義比較。需要注意的是:通過檢測 userAgent 的值來判斷瀏覽器類型是不可靠的,也是不推薦的,因爲用戶可以修改 userAgent 的值。( 好在我們只是用來調試,面向的是開發者,而不是提供給其他白菜用戶使用 )

PS: 作爲擴展,可以使用 特徵檢測 來檢測 web 特性的在手機瀏覽器上的 ( 包括某些客戶端的 webview ) 支持情況,從而在開發階段提早做一些降級處理!另外,如果需要的話,可以在 system 展示一些調用客戶端協議 (JSbridge) 相關的信息。我們就此跳過吧,進入更爲關心的下一部分 network

Network

接着來實現 network,開始前先來了解下 XMLHttpRequest

使用 XMLHttpRequest (XHR)對象可以與服務器交互。您可以從 URL 獲取數據,而無需讓整個的頁面刷新。這使得 Web 頁面可以只更新頁面的局部,而不影響用戶的操作。XMLHttpRequest 在 Ajax 編程中被大量使用。

比較重要的方法 opensendgetAllResponseHeaders,還有一些需要了解的屬性 onreadystatechangereadyStatestatusresponse 等,不瞭解的讀者自行補習下。

我們如果要捕獲用戶發送請求並用於前端展示,需要用到 open 和 send 方法,監聽變換需要用到 onreadystatechange

另外,XMLHttpRequest.readyState 屬性返回的是一個 XMLHttpRequest 代理當前所處的狀態。一個 XHR 代理總是處於下列狀態中的一個:

狀態 描述
0 UNSENT 代理被創建,但尚未調用 open() 方法。
1 OPENED open() 方法已經被調用。
2 HEADERS_RECEIVED send() 方法已經被調用,並且頭部和狀態已經可獲得。
3 LOADING 下載中; responseText 屬性已經包含部分數據。
4 DONE 下載操作已完成。

瞭解這些基礎知識後,來看下代碼實現邏輯:

  mockAjax() {
    // 這裏的 (window as any).XMLHttpRequest 我用的很虛。太粗暴了
    const XMLHttpRequest = (window as any).XMLHttpRequest;
    if (!XMLHttpRequest) {
      return;
    }
    const that = this;
    // 1、備份原生 XMLHttpRequest 的 open 和 send 方法
    const XHRnativeOpen = XMLHttpRequest.prototype.open;
    const XHRnativeSend = XMLHttpRequest.prototype.send;

    // 2、重寫 open 方法
    XMLHttpRequest.prototype.open = function (...args: any) {
      // 3、獲取 open 方法傳入的參數
      const [method, url] = args;

      // 4、保存原有  onreadystatechange
      const userOnreadystatechange = this.onreadystatechange;

      this.onreadystatechange = function (...stateArgs: any) {
        // do something

        // 5、根據 readyState 做相應處理,主要是保存需要展示的數據,比如 response 和 header

        // 6、調用原有 onreadystatechange
        return (
          userOnreadystatechange &&
          userOnreadystatechange.apply(this, stateArgs)
        );
      };

      // 7、調用原生 XMLHttpRequest.open 方法
      return XHRnativeOpen.apply(this, args);
    };
    XMLHttpRequest.prototype.send = function (...args: any) {
      // 8、重寫 XMLHttpRequest.send 方法並保存數據
      return XHRnativeSend.apply(this, args);
    };
  }

這樣基本上就完成了 network 數據的收集,接下來就是表格展示的事了。但,擼完還是覺得過於粗暴,我碼項目以來還是第一次修改 prototype,而且是 XMLHttpRequest 的,生怕對基礎掌握的不夠引發了更多的 bug。於是準備去看下 axios 的源碼,看人家是怎麼玩弄 XMLHttpRequest ,後看能不能優化一下。(後話了...) 這邊需要說的是,如果使用 fetch 發送請求,就 GG 了。給了自己迭代足夠的理由,( 當然前提是否有必要,萬一我又去做 PC端了呢 !)

Element

在用 vconsole 的時候,我就特別關心 element 面板究竟是怎麼實現的。下面就讓我們來撩一下:

回顧下 UI 界面

如果數據來源是 document.documentElement,那不就是下圖麼!

有必要的話,先熟悉下 HTML5 標籤,和 DOM Node

這邊我們只需要關心,三個類型的節點:元素, 文本 和 註釋 ( 瞭解 nodeType)。

對於元素 (標籤) 我們只需要知道兩種不同的展示方式,自閉合標籤以及非自閉合 (對於UI來說,僅僅是縮進的區別),以及它們都是由標籤名和屬性組成,如:<body style="background:#000"></body><img src="...">。下面看下要實現這樣一個 elemnt 的 html 結構是怎麼樣的:

對應實現就是項目裏的 htmlView 組件,主要的代碼邏輯如下:


import { parseDOM } from 'htmlparser2';

// 1. 將 HTML 文本,解析爲 JSON 格式
const tree = parseDOM(document.documentElement.outerHTML);


// 2. 轉換爲易於展示的 JSON 格式,並轉換爲 Immutable 數據

  getRoot() {
    const { tree, defaultExpandedTags } = this.props;

    transformNodes(tree, [], true);
    return Immutable.fromJS(tree[0]);

    function transformNodes(trees: any[], keyPath: any, initial?: boolean) {
      trees.forEach((node: any, i: number) => {
        // 3. 數據轉換邏輯
      });
    }
  }

// 3. 根據 type 來區分渲染 UI

if (type === 'text' || type === 'comment') {

}

對於 htmlparser2 的轉換規則可以看這個 demohtmlparser2得到的數據可能並不適用於渲染,經過處理後最終用於渲染數據的結構如下:

依然是數據驅動的思路,剩下的就只是渲染的邏輯處理。

Storage

Storage 實現也比較簡單。前端比較關心的一般是 localstoragecookies。它們都有自己的獲取,修改,和清除方法。我們只需要拿到數據給表格渲染即可。

關於 Typescript

到目前爲止,講得更多的是控制檯的實現思路。有點對不起標題黨 Ts + React + Mobx,說實話,碼玩這個項目發現並沒有太多的技巧。在這聊一下我用 Typescript 的感受。正如文章一開是說的,最大的感受就是開發體驗的改善。另外就是:

組件 props 和 state 的定義

// Ts 讓代碼更加易於閱讀,只需要看組件這部分代碼即可知道,
// 組件接受哪些屬性以及其內部狀態,並且可以知道他們都接受什麼樣的類型。

interface Props {
  togglePane: () => void;
  logList: LogType[]
}

interface State {
  searchVal: string
}

// 組件泛型
export default class ClassName extends PureComponent<Props, State> {
  // ...
}

其他常用 type,如果想了解 React 相關的 type 可以看這裏
高質量的 Type definitions

  "devDependencies": {
    "@types/jest": "^23.3.9",
    "@types/node": "^10.12.5",
    "@types/react": "^16.7.2",
    "@types/react-dom": "^16.0.9",
    "typescript": "^3.1.6"
  }
// 獲取 ref 上有所不同
export default class Log extends Component<Props, State> {
  private searchBarRef = createRef<SearchBar>()
  sendCMD = ()=> {
      this.searchBarRef.current!.focus()
  }
  render() {
    return (
      <Flex>
        <SearchBar
          ref={this.searchBarRef}
          onclic={this.sendCMD}
        />
      </Flex>
    );
  }
}

能總結的確實很少,對 Ts 中 type system 的感受就是少用 any。大概瞭解下常用的 React 和 window 的 type 即可。(在vscode 編輯器下。直接F12跳轉到 window 或 React 定義處就可以看到所有的類型聲明)

另外在不知道類型的時候,可以利用類型推斷來獲取類型。

我也是剛開始用 Typescript ,說多錯多!不誤人子弟了,就總結到這吧。

yarn run eject

使用 Create React App 腳手架創建完項目後,在 package.json 裏面提供了這樣一個命令

{
  "scripts": {
    "eject": "react-scripts eject"
  }
}

執行完這個命令後,會將封裝的配置全部反編譯到當前項目,這樣用戶就可以完全取得webpack文件的控制權。出於學習目的,還是放出來比較好!

Create React App 水好深,適合單獨拎出來研究!

總結

不得不承認,這是一個練手的項目。可能都完全不適合用 Ts + React 來做,只是希望自己跨出這一步,擁抱 Ts。教程通篇圍繞 前端如何實現瀏覽器控制檯 展開,比較少介紹 TS + React 技巧方面。可以說是一種比較保守的實現方式 ( 因爲不確定是不是最佳實踐 ),
希望拋磚引玉,有人可以 codeReview 下,不勝感激!另外,希望這篇教程有給大家帶來一些知識擴展的作用。

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章