4
React with TypeScript
我们可以使用 ES6 语法的 class 来创建 React 组件,所以如果熟悉 ES6 class 语法,则可以比较轻松的进一步学习TypeScript的class语法。在React中使用结合TypeScript是非常便利的。
首先,应该使用明确的访问控制符表明变量的有效范围
借鉴于其他编程语言的特性,一个类中的角色可能会包含
private
声明的私有变量/方法
public
声明的共有变量/方法
static
声明的静态变量/方法
也就是说,每声明一个变量或者方法,我们都应该明确指定它的角色。而不是直接使用this.xxxx
随意的给 class 新增变量。
然后,我们可以通过 TypeScript 的特性阅读 React 的声明(.d.ts
)文件。以进一步了解React组件的使用。
React的声明文件,详细的描述了React的每一个变量,方法的实现。通过阅读它的声明文件,我们可以进一步加深对React的理解。
最后,理解泛型
class Component<P, S> { static contextType?: Context<any>; context: any; constructor(props: Readonly<P>); /** * @deprecated * @see https://reactjs.org/docs/legacy-context.html */ constructor(props: P, context?: any); setState<K extends keyof S>( state: ((prevState: Readonly<S>, props: Readonly<P>) => (Pick<S, K> | S | null)) | (Pick<S, K> | S | null), callback?: () => void ): void; forceUpdate(callBack?: () => void): void; render(): ReactNode; readonly props: Readonly<{ children?: ReactNode }> & Readonly<P>; state: Readonly<S>; /** * @deprecated * https://reactjs.org/docs/refs-and-the-dom.html#legacy-api-string-refs */ refs: { [key: string]: ReactInstance }; }
这是在React的声明文件中,对于React.Component
的描述。我们可以看到一些常用的state, setState, render
等都有对应的描述。关键的地方是声明文件中有许多用到泛型的地方可能大家理解起来会比较困难。
class Component<P, S>
这里的就是传入的泛型约束变量。
从构造函数constructor(props: P, context?: any);
的约束中,我们可以得知,P其实就是react组件中props的约束条件。
其中对于state的约束state: Readonly
;也可以看到,S是对State的约束。
暂时对泛型不理解也没关系,后续我们再进一步学习
基于上面几点理解,我们就可以实现Drag组件了。如下。代码仅仅只是阅读可能难以理解,一定要动手试试看!
// index.tsx import * as React from'react'; import classnames from'classnames'; import'./style.css'; const isMoblie: boolean = 'ontouchstart'inwindow; // 是否为移动端 class Drag extends React.Component<drag.DragProps, drag.DragState> { private elementWid: number; private elementHeight: number; private left: number; private top: number; private zIndex: number; private clientWidth: number; private clientHeight: number; private clientX: number; private clientY: number; private startX: number; private startY: number; private disX: number; private disY: number; private _dragStart: () => any; private _dragMove: () => any; private _dragEnd: () => any; constructor(props: drag.DragProps) { super(props); this.elementWid = props.width || 100; this.elementHeight = props.height || 100; this.left = props.left || 0; this.top = props.top || 0; this.zIndex = props.zIndex || 0; this.clientWidth = props.maxWidth || 600; this.clientHeight = props.maxHeight || 600; this._dragStart = this.dragStart.bind(this); this.state = { left: this.left, top: this.top }; } public dragStart(ev: React.TouchEvent & React.MouseEvent): void { const target = ev.target; if (isMoblie && ev.changedTouches) { this.startX = ev.changedTouches[0].pageX; this.startY = ev.changedTouches[0].pageY; } else { this.startX = ev.clientX; this.startY = ev.clientY; } // @ts-ignore 偏移位置 = 鼠标的初始值 - 元素的offset this.disX = this.startX - target.offsetLeft; // @ts-ignore this.disY = this.startY - target.offsetTop; this.zIndex += 1; this._dragMove = this.dragMove.bind(this); this._dragEnd = this.dragEnd.bind(this); if (!isMoblie) { document.addEventListener('mousemove', this._dragMove, false); document.addEventListener('mouseup', this._dragEnd, false); } } public dragMove(ev: drag.TouchEvent): void { if (isMoblie && ev.changedTouches) { this.clientX = ev.changedTouches[0].pageX; this.clientY = ev.changedTouches[0].pageY; } else { this.clientX = ev.clientX; this.clientY = ev.clientY; } // 元素位置 = 现在鼠标位置 - 元素的偏移值 let left = this.clientX - this.disX; let top = this.clientY - this.disY; if (left < 0) { left = 0; } if (top < 0) { top = 0; } if (left > this.clientWidth - this.elementWid) { left = this.clientWidth - this.elementWid; } if (top > this.clientHeight - this.elementHeight) { top = this.clientHeight - this.elementHeight; } this.setState({ left, top }); } public dragEnd(ev: drag.TouchEvent): void { const { onDragEnd } = this.props; document.removeEventListener('mousemove', this._dragMove); document.removeEventListener('mouseup', this._dragEnd); if (onDragEnd) { onDragEnd({ X: this.startX - this.clientX, Y: this.startY - this.clientY }) }; } public render() { const { className, width = 100, height = 100, zIndex } = this.props; const { left = 0, top = 0 } = this.state; const styles: drag.LiteralO = { width, height, left, top } if (zIndex) { styles['zIndex'] = this.zIndex; } /** * dragbox 为拖拽默认样式 * className 表示可以从外部传入class修改样式 */ const cls = classnames('dragbox', className); return ( <div className={cls} onTouchStart={this._dragStart} onTouchMove={this._dragMove} onTouchEnd={this._dragEnd} onMouseDown={this._dragStart} onMouseUp={this._dragEnd} style={styles} > {this.props.children} </div> ) } } exportdefault Drag; // /** // * 索引类型 // * 表示key值不确定,但是可以约束key的类型,与value的类型 // */ // interface LiteralO { // [key: number]: string // } // const enx: LiteralO = { // 1: 'number', // 2: 'axios', // 3: 'http', // 4: 'zindex' // } // /** // * 映射类型用另外一种方式约束JSON的key值 // */ // type keys = 1 | 2 | 3 | 4 | 5; // type Mapx = { // [key in keys]: string // } // const enx2: Mapx = { // 1: 'number', // 2: 'axios', // 3: 'http', // 4: 'zindex', // 5: 'other' // } // interface Person { // name: string, // age: number // } // type Mapo = { // [P in keyof Person]: string // } // const enx3: Mapo = { // name: 'alex', // age: '20' // }
你会发现,React与ts的结合使用,并没有特别。我们只需要把React组件,看成一个class,他和其他的calss,并没有什么特别的不同了。
函数式组件同理。
5
JSX
普通的ts文件,以.ts
作为后缀名。
而包含JSX的文件,则以.tsx
作为后缀名。这些文件通常也被认为是React组件。
若要支持jsx,我们需要在tsconfig.js中,配置jsx的模式。一般都会默认支持。
ts支持三种jsx模式,preserve, react, react-native
。这些模式只在代码生成阶段起作用 - 类型检查并不受影响。
这句话怎么理解呢?也就意味着,typescript在代码生成阶段,会根据我们配置的模式,对代码进行一次编译。例如,我们配置jsx: preserve
,根据下面的图,.tsx 文件会 被编译成 .jsx文件。而这个阶段是在代码生成阶段,因此,生成的 .jsx还可以被后续的代码转换操作。例如再使用babel进行编译。
配图来自官方文档
类型检查
这部分内容可能会难理解一点,大家不必强求现在就掌握,以后再说也OK
我们在实际使用过程中,经常会遇到组件类型兼容性的错误,甚至也看不太明白报错信息在说什么。这大概率是对JSX的属性类型理解不到位导致。
理解JSX的类型检测之前,我们需要理清楚两个概念。
「固有元素」
通常情况下,固有元素是指html中的已经存在元素。例如div。
固有元素div
固有元素使用特殊的接口 JSX.IntrinsicElements 来查找。我们也可以利用这个接口,来定义自己的固有元素「但是没必要」。
// 官网demo declare namespace JSX { interface IntrinsicElements { foo: any } } <foo />; // 正确 <bar />; // 错误
固有元素都以小写开头。
我们可以通过以下方式,给固有元素定义属性。
declare namespace JSX { interface IntrinsicElements { foo: { bar?: boolean } } } // `foo`的元素属性类型为`{bar?: boolean}` <foo bar />;
「基于值的元素」
也就是React中常常提到的自定义元素。规定必须以大写字母开头。基于值的元素会简单的在它所在的作用域里按标识符查找。
// demo来自官方 import MyComponent from"./myComponent"; <MyComponent />; // 当前作用域找得到,正确 <SomeOtherComponent />; // 找不到,错误
React自定义组件有两种方式
class 类组件
function 函数组件
由于这两种基于值的元素在 JSX 表达式里无法区分,因此 TypeScript首先会尝试将表达式做为函数组件进行解析。如果解析成功,那么TypeScript 就完成了表达式到其声明的解析操作。如果按照函数组件解析失败,那么 TypeScript 会继续尝试以类组件的形式进行解析。如果依旧失败,那么将输出一个错误。
「函数组件」
正如其名,组件被定义成 JavaScript 函数,它的第一个参数是 props 对象。TypeScript 会强制它的「函数执行的」返回值可以赋值给 JSX.Element。
// demo来自官方文档 interface FooProp { name: string; X: number; Y: number; } declare function AnotherComponent(prop: {name: string}); function ComponentFoo(prop: FooProp) { return <AnotherComponent name={prop.name} />; } const Button = (prop: {value: string}, context: { color: string }) => <button>
「类组件」
当一个组件由 class 创建而成「例如我们刚才实践的Drag组件」,那么当我们在使用该组件「即生成实例对象」时,则该实例类型必须赋值给 JSX.ElementClass 或抛出一个错误。
// demo来自官方文档 declare namespace JSX { interface ElementClass { render: any; } } class MyComponent { render() {} } function MyFactoryFunction() { return { render: () => {} } } <MyComponent />; // 正确 <MyFactoryFunction />; // 正确
函数组件的props直接作为参数传入,而类组件的 props,则取决于 JSX.ElementAttributesProperty。
// 案例来自官方文档 declare namespace JSX { interface ElementAttributesProperty { props; // 指定用来使用的属性名 } } class MyComponent { // 在元素实例类型上指定属性 props: { foo?: string; } } // `MyComponent`的元素属性类型为`{foo?: string}` <MyComponent foo="bar" />
如果未指定 JSX.ElementAttributesProperty,那么将使用类元素构造函数或 SFC 调用的第一个参数的类型。因此,如果我们在定义类组件时,应该将props对应的泛型类型传入,以确保JSX的正确解析。
「子孙类型检查」
从TypeScript 2.3开始,ts引入了 children 类型检查。children 是元素属性「attribute」类型的一个特殊属性「property」,子 JSXExpression 将会被插入到属性里。与使用JSX.ElementAttributesProperty 来决定 props 名类似,我们可以利用 JSX.ElementChildrenAttribute 来决定 children 名。JSX.ElementChildrenAttribute 应该被声明在单一的属性里。
declare namespace JSX { interface ElementChildrenAttribute { children: {}; // specify children name to use } }
「JSX表达式结果类型」
默认地JSX表达式结果的类型为any。我们可以自定义这个类型,通过指定JSX.Element接口。然而,不能够从接口里检索元素,属性或JSX的子元素的类型信息。它是一个黑盒。
自动推导结果为JSX.Element