TypeScript:React、拖拽、实践!【下】

本文涉及的产品
云解析DNS,个人版 1个月
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 通过上一篇文章的学习,我们知道,typescript其实是一套约束规则。理解了这一点,就可以大概确定我们的学习方向。• 是规则,那么知识点之间,就不会有太强的关联性。我们可以一条一条的逐步学习。也就意味着我们并不用急于把官方文档里所有的规则一次性学完。甚至可以把文档当成一个手册,在具体使用时再去查询。• 是约束,也就意味着开发方式的改变与限制。ts的开发会与通常情况下松散灵活的开发不太一样,这就必然会带来初期的不适应。约束带来的痛苦无法避免,我们要有这样一个心理预期。

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进行编译。


微信图片_20220511152408.png

配图来自官方文档


类型检查


这部分内容可能会难理解一点,大家不必强求现在就掌握,以后再说也OK


我们在实际使用过程中,经常会遇到组件类型兼容性的错误,甚至也看不太明白报错信息在说什么。这大概率是对JSX的属性类型理解不到位导致。


理解JSX的类型检测之前,我们需要理清楚两个概念。


固有元素


通常情况下,固有元素是指html中的已经存在元素。例如div。


微信图片_20220511152412.jpg


固有元素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的子元素的类型信息。它是一个黑盒。


微信图片_20220511152415.png


自动推导结果为JSX.Element

相关文章
|
2月前
|
监控 JavaScript 安全
TypeScript在员工上网行为监控中的类型安全实践
本文演示了如何使用TypeScript在员工上网行为监控系统中实现类型安全。通过定义`Website`类型和`MonitoringData`接口,确保数据准确性和可靠性。示例展示了从监控设备获取数据和提交到网站的函数,强调了类型定义在防止错误、提升代码可维护性方面的作用。
115 7
|
2月前
|
数据采集 Web App开发 JavaScript
TypeScript 爬虫实践:选择最适合你的爬虫工具
TypeScript 爬虫实践:选择最适合你的爬虫工具
|
6天前
|
JavaScript 开发者 索引
TypeScript接口与类型别名:深入解析与应用实践
【7月更文挑战第10天】TypeScript的接口和类型别名是定义类型的关键工具。接口描述对象结构,用于类、对象和函数参数的形状约束,支持可选、只读属性及继承。类型别名则为复杂类型提供新名称,便于重用和简化。接口适合面向对象场景,类型别名在类型重用和复杂类型简化时更有优势。选择时要考虑场景和灵活性。
|
6天前
|
JavaScript 前端开发 IDE
React 项目中有效地使用 TypeScript
React 项目中有效地使用 TypeScript
|
6天前
|
前端开发 JavaScript 开发者
React 和 TypeScript
React 和 TypeScript
|
1月前
|
前端开发 JavaScript 开发者
React和TypeScript各自以其独特的优势赢得了广大开发者的青睐
【6月更文挑战第12天】React和TypeScript是前端开发的强强联合。TypeScript提供静态类型检查和面向对象特性,增强代码健壮性和团队协作效率;React凭借组件化、高性能和丰富生态系统引领UI构建。两者结合,能精确定义React组件类型,提升代码组织和维护性,通过安装TypeScript、配置、编写及构建步骤,可在React项目中实现这一优势。这种结合为前端开发带来进步,未来应用将更加广泛。
26 1
|
2月前
|
JavaScript 前端开发 安全
【TypeScript技术专栏】TypeScript在Electron桌面应用中的实践
【4月更文挑战第30天】本文介绍了如何在Electron桌面应用中采用TypeScript以提升代码质量和可维护性。Electron利用Chromium和Node.js让前端开发者能创建桌面应用,而TypeScript的强类型系统和高级语言特性有助于管理复杂项目。通过初始化项目、安装依赖、配置TypeScript、编写主进程和渲染进程代码,开发者可以在Electron中实践TypeScript。一个简单的文本编辑器案例展示了TypeScript在确保类型安全方面的优势。尽管有学习成本,但TypeScript对大型Electron项目来说是值得的。
|
2月前
|
传感器 JavaScript 前端开发
【TypeScript技术专栏】TypeScript在大型项目中的实践与挑战
【4月更文挑战第30天】TypeScript在大型前端项目中日益流行,以其类型系统和ES6+支持提升代码安全与维护性。然而,采用 TypeScript 面临学习曲线、构建时间增加及类型推断挑战。通过最佳实践和成熟工具链(如 tsc、tslint 和 Visual Studio Code)可克服这些问题。案例如Angular、Basecamp和Slack已成功应用TypeScript。掌握TypeScript对提升开发者技能和市场竞争力至关重要。
|
2月前
|
JavaScript 前端开发 开发者
【TypeScript技术专栏】TypeScript与React的完美结合
【4月更文挑战第30天】React和TypeScript在前端开发中备受推崇。React以其组件化、高性能和灵活的生态系统引领UI构建,而TypeScript通过静态类型检查和面向对象特性增强了代码的健壮性和可维护性。两者结合,能提升开发效率,降低错误,使React组件结构更清晰。通过安装TypeScript,配置tsconfig.json,然后用TypeScript编写和打包代码,可实现两者的无缝集成。这种结合为前端开发带来更强的代码质量和团队协作效果,随着技术发展,其应用将更加广泛。
|
2月前
|
设计模式 前端开发 数据可视化
【第4期】一文了解React UI 组件库
【第4期】一文了解React UI 组件库
220 0