Ts + React + Mobx 實現移動端瀏覽器控制檯
自從使用 Typescript 寫 H5 小遊戲後,就對 Ts 產生了依賴(智慧提示以及友好的重構提示),但對於其 Type System 還需要更多的實踐。
最近開發 H5 小遊戲,在移動端除錯方面,為求方便沒有采用 inspect 的模式。用的是粗暴的 ofollow,noindex">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-value
而 Array
其實也是索引和值的對應關係。
基本邏輯:
<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 程式設計中被大量使用。
比較重要的方法 open
, send
, getAllResponseHeaders
,還有一些需要了解的屬性 onreadystatechange
, readyState
, status
, response
等,不瞭解的讀者自行補習下。
我們如果要捕獲使用者傳送請求並用於前端展示,需要用到 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
的轉換規則可以看這個demo, htmlparser2
得到的資料可能並不適用於渲染,經過處理後最終用於渲染資料的結構如下:

依然是資料驅動的思路,剩下的就只是渲染的邏輯處理。
Storage
Storage 實現也比較簡單。前端比較關心的一般是 localstorage
和 cookies
。它們都有自己的獲取,修改,和清除方法。我們只需要拿到資料給表格渲染即可。
關於 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 下,不勝感激!另外,希望這篇教程有給大家帶來一些知識擴充套件的作用。