方法
通过上一篇文章的学习,我们知道,TS 其实是一套约束规则。
理解了这一点,就可以大概确定我们的学习方向。
最后抛开规则的学习,最重要的应该是什么?毫无疑问,是实践。这也是无法从官方文档获取到的重要讯息。
许多人只看官方文档,一脸懵逼!规则的学习好像不难,可运用到实践到底是什么样子?不知道。
所以,第一件事情,我们要抛开规则,来看一看,把 ts 用在实践里到底是什么样。这里以 react 中实现拖拽为例。
拖拽的原理与实现过程之前已经学习过,所以这里就把之前的代码直接拿过来调整一下
环境
一个简单的方式,是直接使用 create-react-app
创建一个已经支持typescript开发的项目。
npx create-react-app tsDemo --template typescript
当然,在不同的脚手架项目中支持 typescirpt 可能不太一样,因此这里就不做统一讲解,大家根据自己的需求在网上搜索方案即可。
除此之外,也可以clone我们的 练习项目。
.d.ts
在ts的开发中,.d.ts
文件扮演着至关重要的作用。通常情况下,这样的文件,我们称之为声明文件。
那么声明文件是一个什么样的东西呢?
前一篇文章我们讲过,ts 的规则能够描述一个简单的变量,能够描述一个复杂的JSON 数据,能够描述函数,也能够描述对象 class。可是大量的描述规则代码如果和实际功能代码糅合在一起,势必会导致整个代码冗余杂乱。因此在实践中,当声明内容很多时,通常会统一在一个文件中编写ts的描述规则,这个文件,就是以 .d.ts
为后缀名的声明文件。
如果声明文件过多,那么就非常可能重名,为了避免相互干扰,typescript又引入了作用域 namespace
概念。
因此,如果我们要利用ts实现拖拽组件,那么文件结构会如下展示:
1+ Drag2- index.tsx3- style.scss4- interface.d.ts
其中 interface.d.ts
中会声明在开发过程中遇到的所有复杂数据结构。大概内容如下:
1declare namespace drag {2...3}
我们可以使用 interface
语法来约束一个JSON数据。
在创建一个需要符合这个约束规则的数据时,只需要直接使用命名空间 drag
即可。ts 会自动帮助我们识别而无需引入,或者 ts 会自动帮助我们引入(必要时)。
整个拖拽功能完整声明文件如下
10/** declare 为声明关键字,让外部组件能访问该命名空间*/20declare namespace drag {30interface JSONDemo {40name: string,50age: number60}70interface DragProps {80width?: number,90height?: number,10left?: number,11top?: number,12zIndex?: number,13maxWidth?: number,14maxHeight?: number,15className?: string,16onDragEnd?: (target: DragEndParam) => any,17children?: any18}1920interface DragState {21left: number,22top: number23}2425interface DragEndParam {26X: number,27Y: number28}2930type TouchEvent = React.TouchEvent & React.MouseEvent;3132interface LiteralO {33width: number,34height: number,35[key: string]: any36}37}
本文的主要目的在于帮助大家了解实践中ts的运用,所以如果初学ts,对一些语法不是很熟悉不用太过在意,具体的语法可以通过官方文档,或者后续文章中学习
通常情况下,每个「复杂」组件都会对应创建一个 .d.ts
的声明文件。
如果声明比较简单,我们可以不需要 .d.ts
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的理解。
最后,理解泛型
10class Component<P, S> {2030static contextType?: Context<any>;405060context: any;7080constructor(props: Readonly<P>);90/**10* @deprecated11* @see https://reactjs.org/docs/legacy-context.html12*/13constructor(props: P, context?: any);141516setState<K extends keyof S>(17state: ((prevState: Readonly<S>, props: Readonly<P>) => (Pick<S, K> | S | null)) | (Pick<S, K> | S | null),18callback?: () => void19): void;2021forceUpdate(callBack?: () => void): void;22render(): ReactNode;232425readonly props: Readonly<{ children?: ReactNode }> & Readonly<P>;26state: Readonly<S>;27/**28* @deprecated29* https://reactjs.org/docs/refs-and-the-dom.html#legacy-api-string-refs30*/31refs: {32[key: string]: ReactInstance33};34}
这是在React的声明文件中,对于 React.Component
的描述。我们可以看到一些常用的 state, setState, render
等都有对应的描述。关键的地方是声明文件中有许多用到泛型的地方可能大家理解起来会比较困难。
class Component<P, S>
这里的<P, S>
就是传入的泛型约束变量。
从构造函数 constructor(props: P, context?: any);
的约束中,我们可以得知,P其实就是 react 组件中 props 的约束条件。
其中对于 state 的约束 state: Readonly<S>;
也可以看到,S 是对 State 的约束。
暂时对泛型不理解也没关系,后续我们再进一步学习
基于上面几点理解,我们就可以实现 Drag 组件了。如下。代码仅仅只是阅读可能难以理解,一定要动手试试看!
100// index.tsx200300import * as React from 'react';400import classnames from 'classnames';500import './style.css';600700const isMoblie: boolean = 'ontouchstart' in window; // 是否为移动端800900class Drag extends React.Component<drag.DragProps, drag.DragState> {100private elementWid: number;110private elementHeight: number;120private left: number;130private top: number;140private zIndex: number;150private clientWidth: number;160private clientHeight: number;170180private clientX: number;190private clientY: number;200210private startX: number;220private startY: number;230240private disX: number;250private disY: number;260270private _dragStart: () => any;280private _dragMove: () => any;290private _dragEnd: () => any;300310constructor(props: drag.DragProps) {320super(props);330this.elementWid = props.width || 100;340this.elementHeight = props.height || 100;350this.left = props.left || 0;360this.top = props.top || 0;370this.zIndex = props.zIndex || 0;380this.clientWidth = props.maxWidth || 600;390this.clientHeight = props.maxHeight || 600;400this._dragStart = this.dragStart.bind(this);410420this.state = {430left: this.left,440top: this.top450};460}470480public dragStart(ev: React.TouchEvent & React.MouseEvent): void {490const target = ev.target;500if (isMoblie && ev.changedTouches) {510this.startX = ev.changedTouches[0].pageX;520this.startY = ev.changedTouches[0].pageY;530} else {540this.startX = ev.clientX;550this.startY = ev.clientY;560}570// @ts-ignore 偏移位置 = 鼠标的初始值 - 元素的offset580this.disX = this.startX - target.offsetLeft;590600// @ts-ignore610this.disY = this.startY - target.offsetTop;620630this.zIndex += 1;640650this._dragMove = this.dragMove.bind(this);660this._dragEnd = this.dragEnd.bind(this);670680if (!isMoblie) {690document.addEventListener('mousemove', this._dragMove, false);700document.addEventListener('mouseup', this._dragEnd, false);710}720}730740public dragMove(ev: drag.TouchEvent): void {750if (isMoblie && ev.changedTouches) {760this.clientX = ev.changedTouches[0].pageX;770this.clientY = ev.changedTouches[0].pageY;780} else {790this.clientX = ev.clientX;800this.clientY = ev.clientY;810}820830// 元素位置 = 现在鼠标位置 - 元素的偏移值840let left = this.clientX - this.disX;850let top = this.clientY - this.disY;860870if (left < 0) {880left = 0;890}900910if (top < 0) {920top = 0;930}940950if (left > this.clientWidth - this.elementWid) {960left = this.clientWidth - this.elementWid;970}980990if (top > this.clientHeight - this.elementHeight) {100top = this.clientHeight - this.elementHeight;101}102103this.setState({ left, top });104}105106public dragEnd(ev: drag.TouchEvent): void {107const { onDragEnd } = this.props;108document.removeEventListener('mousemove', this._dragMove);109document.removeEventListener('mouseup', this._dragEnd);110111if (onDragEnd) {112onDragEnd({113X: this.startX - this.clientX,114Y: this.startY - this.clientY115})116};117}118119public render() {120const { className, width = 100, height = 100, zIndex } = this.props;121const { left = 0, top = 0 } = this.state;122123const styles: drag.LiteralO = {124width,125height,126left,127top128}129130if (zIndex) {131styles['zIndex'] = this.zIndex;132}133134/**135* dragbox 为拖拽默认样式136* className 表示可以从外部传入class修改样式137*/138const cls = classnames('dragbox', className);139140return (141<div142className={cls}143onTouchStart={this._dragStart}144onTouchMove={this._dragMove}145onTouchEnd={this._dragEnd}146onMouseDown={this._dragStart}147onMouseUp={this._dragEnd}148style={styles}149>150{this.props.children}151</div>152)153}154}155156157export default Drag;158159// /**160// * 索引类型161// * 表示key值不确定,但是可以约束key的类型,与value的类型162// */163// interface LiteralO {164// [key: number]: string165// }166167// const enx: LiteralO = {168// 1: 'number',169// 2: 'axios',170// 3: 'http',171// 4: 'zindex'172// }173174// /**175// * 映射类型用另外一种方式约束JSON的key值176// */177// type keys = 1 | 2 | 3 | 4 | 5;178// type Mapx = {179// [key in keys]: string180// }181182// const enx2: Mapx = {183// 1: 'number',184// 2: 'axios',185// 3: 'http',186// 4: 'zindex',187// 5: 'other'188// }189190// interface Person {191// name: string,192// age: number193// }194// type Mapo = {195// [P in keyof Person]: string196// }197198// const enx3: Mapo = {199// name: 'alex',200// age: '20'201// }
你会发现,React与ts的结合使用,并没有特别。我们只需要把React组件,看成一个class,他和其他的calss,并没有什么特别的不同了。
函数式组件同理。
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。
固有元素使用特殊的接口 JSX.IntrinsicElements 来查找。我们也可以利用这个接口,来定义自己的固有元素「但是没必要」。
1// 官网demo2declare namespace JSX {3interface IntrinsicElements {4foo: any5}6}78<foo />; // 正确9<bar />; // 错误
固有元素都以小写开头。
我们可以通过以下方式,给固有元素定义属性。
1declare namespace JSX {2interface IntrinsicElements {3foo: { bar?: boolean }4}5}67// `foo`的元素属性类型为`{bar?: boolean}`8<foo bar />;
「基于值的元素」
也就是 React 中常常提到的自定义元素。规定必须以大写字母开头。基于值的元素会简单的在它所在的作用域里按标识符查找。
1// demo来自官方2import MyComponent from "./myComponent";34<MyComponent />; // 当前作用域找得到,正确5<SomeOtherComponent />; // 找不到,错误
React自定义组件有两种方式
由于这两种基于值的元素在 JSX 表达式里无法区分,因此 TypeScript 首先会尝试将表达式做为函数组件进行解析。如果解析成功,那么 TypeScript 就完成了表达式到其声明的解析操作。如果按照函数组件解析失败,那么 TypeScript 会继续尝试以类组件的形式进行解析。如果依旧失败,那么将输出一个错误。
「函数组件」
正如其名,组件被定义成 JavaScript 函数,它的第一个参数是 props 对象。 TypeScript 会强制它的「函数执行的」返回值可以赋值给 JSX.Element。
10// demo来自官方文档20interface FooProp {30name: string;40X: number;50Y: number;60}7080declare function AnotherComponent(prop: {name: string});90function ComponentFoo(prop: FooProp) {10return <AnotherComponent name={prop.name} />;11}1213const Button = (prop: {value: string}, context: { color: string }) => <button>
「类组件」
当一个组件由 class 创建而成「例如我们刚才实践的Drag组件」,那么当我们在使用该组件「即生成实例对象」时,则该实例类型必须赋值给 JSX.ElementClass 或抛出一个错误。
10// demo来自官方文档20declare namespace JSX {30interface ElementClass {40render: any;50}60}7080class MyComponent {90render() {}10}1112function MyFactoryFunction() {13return { render: () => {} }14}1516<MyComponent />; // 正确17<MyFactoryFunction />; // 正确
函数组件的 props 直接作为参数传入,而类组件的 props,则取决于 JSX.ElementAttributesProperty。
10// 案例来自官方文档20declare namespace JSX {30interface ElementAttributesProperty {40props; // 指定用来使用的属性名50}60}7080class MyComponent {90// 在元素实例类型上指定属性10props: {11foo?: string;12}13}1415// `MyComponent`的元素属性类型为`{foo?: string}`16<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 应该被声明在单一的属性里。
简单来说,我们可以在 this.props
的智能提示中,得到 children 的索引。
1declare namespace JSX {2interface ElementChildrenAttribute {3children: {}; // specify children name to use4}5}
「JSX表达式结果类型」
默认地 JSX 表达式结果的类型为 any。 我们可以自定义这个类型,通过指定JSX.Element 接口。 然而,不能够从接口里检索元素、属性或 JSX 的子元素的类型信息。 它是一个黑盒。