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

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

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

經過該教程能夠學習:node

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

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

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

開始

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

下面,先來看下預覽圖片git

UI 很簡單,按功能劃分爲github

  • Log 、 System
  • Network
  • Elemnet
  • Storage

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

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

基本代碼風格

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

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;
    // 一、備份原生 XMLHttpRequest 的 open 和 send 方法
    const XHRnativeOpen = XMLHttpRequest.prototype.open;
    const XHRnativeSend = XMLHttpRequest.prototype.send;

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

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

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

        // 五、根據 readyState 作相應處理,主要是保存須要展現的數據,好比 response 和 header

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

      // 七、調用原生 XMLHttpRequest.open 方法
      return XHRnativeOpen.apply(this, args);
    };
    XMLHttpRequest.prototype.send = function (...args: any) {
      // 八、重寫 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 下,不勝感激!另外,但願這篇教程有給你們帶來一些知識擴展的做用。

參考

相關文章
相關標籤/搜索