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-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 下,不勝感激!另外,希望這篇教程有給大家帶來一些知識擴充套件的作用。