自从使用 Typescript 写 H5 小游戏后,就对 Ts 产生了依赖(智能提示以及友好的重构提示),但对于其 Type System 还须要更多的实践。html
最近开发 H5 小游戏,在移动端调试方面,为求方便没有采用 inspect 的模式。用的是粗暴的 vConsole,用人家东西要学会感恩,因此决定去了解它的原理,最后用 Ts + React 码一个移动端浏览器控制台,算是 Ts + React 实战。前端
经过该教程能够学习:node
项目源码 供上, 第一次用 Typescript + React 码项目,记录迭代的过程,有兴趣入坑的可 star 一下 期待 CodeReview。react
本着快速开发的理念(本人要带娃),因而基于 Create React App 脚手架搭建项目,UI 框架使用了一样采用 Ts 编写的 AntMobile。 开始项目讲解前,显然须要对这两个有必定的了解 ( 建议可做为进一步学习 Ts + React 的参考 )webpack
下面,先来看下预览图片ios
UI 很简单,按功能划分为git
主要从以上这几个功能模块展开github
PS: 教程会略过一些,诸如如何支持 stylus ( 项目执行过 yarn run eject ),interface 要不要加 I,render 要不要 Public, 如何去除一些 Tslint 等。( 跟踪文件 git history 可略知一二 )PWA 等web
通篇会按这种风格 ( 并非最佳实践 ) 去编写组件,( 比较少无状态组件,也没有高阶组件的应用 )。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,与之不可分割的 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
组件。
从以前的预览图片能够大体看到整个数据展现结构,都是 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 主要用于展现浏览器端不太容易查看的信息,好比当前浏览器的用户代理(user agent)字符串或者当前真实的 URL (因为某些缘由,URL 可能被修改)。固然这些要展现的信息跟业务以及须要调试的内容关联比较大,所以这个面板仍是自定义比较。须要注意的是:经过检测 userAgent
的值来判断浏览器类型是不可靠的,也是不推荐的,由于用户能够修改 userAgent 的值。( 好在咱们只是用来调试,面向的是开发者,而不是提供给其余白菜用户使用 )
PS: 做为扩展,可使用 特征检测 来检测 web 特性的在手机浏览器上的 ( 包括某些客户端的 webview ) 支持状况,从而在开发阶段提前作一些降级处理!另外,若是须要的话,能够在 system 展现一些调用客户端协议 (JSbridge) 相关的信息。咱们就此跳过吧,进入更为关心的下一部分 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;
// 一、备份原生 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端了呢 !)
在用 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 实现也比较简单。前端比较关心的通常是 localstorage
和 cookies
。它们都有本身的获取,修改,和清除方法。咱们只须要拿到数据给表格渲染便可。
到目前为止,讲得更多的是控制台的实现思路。有点对不起标题党 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 ,说多错多!不误人子弟了,就总结到这吧。
使用 Create React App 脚手架建立完项目后,在 package.json
里面提供了这样一个命令
{
"scripts": {
"eject": "react-scripts eject"
}
}
复制代码
执行完这个命令后,会将封装的配置所有反编译到当前项目,这样用户就能够彻底取得webpack文件的控制权。出于学习目的,仍是放出来比较好!
Create React App 水好深,适合单独拎出来研究!
不得不认可,这是一个练手的项目。可能都彻底不适合用 Ts + React 来作,只是但愿本身跨出这一步,拥抱 Ts。教程通篇围绕 前端如何实现浏览器控制台 展开,比较少介绍 TS + React 技巧方面。能够说是一种比较保守的实现方式 ( 由于不肯定是否是最佳实践 ), 但愿抛砖引玉,有人能够 codeReview 下,不胜感激!另外,但愿这篇教程有给你们带来一些知识扩展的做用。