使用React+TypeScript构建自己的组件库

TypeScript基础

数据类型

ECMAScript标准定义了8种数据类型

  1. Boolean
  2. Null
  3. Undefined
  4. Number
  5. BigInt
  6. String
  7. Symbol
  8. Object

interface接口

interface的主要作用如下:

  1. 对对象的形状(shape)进行描述
  2. 对类(class)进行抽象
  3. Duck Typing(鸭子类型)
interface Person {
  readonly id: number;
  name: string;
  age?: number;
}

函数表达式

const add: (x: number, y: number, z: number) => number = function (x, y, z): number {
  return x + y + z;
}

面向对象的三大特性:封装,继承,多态

  • 封装:当我们使用类中的某个方法时,我们无需知道类中的具体实现细节
  • 继承:子类可以继承父类,让子类具有父类的属性以及方法
  • 多态:子类可以覆盖父类中的方法,从而让子类和父类的实例表现出不同的特性
class Animal {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  run() {
    console.log(`${this.name} is running`)
  }
}

class Cat extends Animal {
  constructor(name: string) {
    super(name);
    // 重写父类的构造方法,首先需要使用super调用父类的构造函数
    console.log(this.name);
  }

  run () {
    console.log('miao~');
    // 当在子类中需要调用父类的方法时,也可以使用super来调用
    super.run();
  }
}

类中属性或者方法的访问修饰符

  • private: 仅仅能够在当前类的内部访问,在子类或者实例中都无法访问
  • protected: 可以在当前类内部和子类中访问,无法在实例中访问
  • public: 默认的访问修饰符,能够在类,子类,实例中都可以访问到
  • readonly: 用来修饰类中不可变的属性
  • static: 可以直接用类名来访问其修饰的属性和方法

接口interface

当多个class需要都需要实现某些相同的方法时,我们可以使用interface来实现

interface Radio {
  switchRadio() :void;
}

class Car implements Radio {
  switchRadio() {}
}

class Phone implements Radio {
  switchRadio() {}
}

常量枚举

const enum Direction {
    Up = 'UP',
    Down = 'DOWN',
    Left = 'LEFT',
    Right = 'RIGHT'
}

泛型约束

可以使用extends关键字来对泛型参数进行限制

interface IWithLengh {
  length: number;
}

function echoWithLength<T extends IWithLengh>(arg: T): T {
  console.log(arg.length);
  return arg;
}

const str = echoWithLength('abc');
const obj = echoWithLength({ length: 1 });
const arr = echoWithLength([1, 2])

类也可以使用泛型来约束

class Queue<T> {
  private data: T[] = [];
  push(item: T): void {
    this.data.push(item);
  }
  pop(): T {
    return this.data.pop();
  }
}
const queue = new Queue<number>();
queue.push(1);
console.log(queue.pop().toFixed(2));

const queue2 = new Queue<string>()

接口也可以使用泛型来约束

interface KeyPair<T, U> {
    key: T;
    value: U;
}

使用泛型来约束函数

interface IPlus<T> {
  (a: T, b: T): T;
}
function plus(a: number, b: number): number {
  return a + b;
}
const a: IPlus<number> = plus

类型别名

类型别名多用于联合类型中

type NameResolver = () => string;
type NameOrResolver = string | NameResolver;
function getName(n: NameOrResolver): string {
  if (typeof n === 'string') {
    return n;
  } else {
    return n();
  }
}

类型断言

function getLength(input: string | number): number {
  if ((<string>input).length) {
    return (<string>input).length;
  } else {
    return input.toString().length;
  }
}

React基础

使用脚手架工具搭建项目

首先使用create-react-app搭建项目

npx create-react-app ts-with-react --typescript

简单的函数式组件

interface IHelloProps {
  message: string;
}
const Hello: React.FC<IHelloProps> = (props) => {
  return <h2>{ props.message }</h2>
}
Hello.defaultProps = {
  message: 'Hello World'
}

React Hooks解决的问题

  • 组件很难复用状态逻辑
  • 复杂组件难以理解,尤其是生命周期函数

useState

该hook相当于组件内的状态

const LikeButton: React.FC = () => {
  const [like, setLike] = useState(0)
  const [status, setStatus] = useState(true)
  return (
    <>
    <button onClick={() => { setLike(like + 1)}}>
      {like}
    </button>
    <button onClick={() => { setStatus(!status) }}>
      {String(status)}
    </button>
    </>
  )
}

useEffect

该hook默认会在第一次渲染完成和每次界面更新时执行,相当于class组件中的componentDidMountcomponentDidUpdate

useEffect(() => {
  document.title = `点击了${like}次`
})

使用useEffect的返回值清除副作用

const LikeButton: React.FC = () => {
  const [position, setPosition] = useState({ x: 0, y: 0 })
  useEffect(() => {
    const updatePostion = (e: MouseEvent) => {
      setPosition({
        x: e.clientX,
        y: e.clientY
      })
    }
    document.addEventListener('click', updatePostion)
    return () => {
      // 会在下一次更新界面之后,重新添加此effect之前执行
      document.removeEventListener('click', updatePostion)
    }
  })

  return <p>X: {position.x}, Y: {[position.y]}</p>
}

控制useEffect执行时机,这里需要使用到useEffect的第二个参数。第二个参数是一个数组,可以填入依赖项,当这些依赖项发生变化时,才会去执行useEffect。当第二个参数为空数组,显然这种情况是没有依赖可以变化的,因此这种情况的useEffect仅仅会在组件加载和卸载时执行一次。

useEffect(() => {
  document.title = `点击了${like}次`
}, [like])

自定义hook

自定义hook需要以use开头

const useMousePosition = () => {
  const [position, setPosition] = useState({ x: 0, y: 0 })
  useEffect(() => {
    const updatePostion = (e: MouseEvent) => {
      setPosition({
        x: e.clientX,
        y: e.clientY
      })
    }
    document.addEventListener('click', updatePostion)
    return () => {
      document.removeEventListener('click', updatePostion)
    }
  }, [])
  return position
}

使用一个自定义hook

const position = useMousePosition()

使用自定义hooks封装一个请求公共hooks

import { useState, useEffect } from 'react'
import axios from 'axios'
const useURLLoader = (url: string, deps: any[] = []) => {
  const [data, setData] = useState<any>(null)
  const [loading, setLoading] = useState(false)
  useEffect(() => {
    setLoading(true)
    axios.get(url).then(result => {
      setData(result.data)
      setLoading(false)
    })
  }, deps)
  return data
}
export default useURLLoader

完成组件库

完成一个组件库需要考虑的问题

  • 代码结构
  • 样式解决方案
  • 组件需求分析和编码
  • 组件测试用例分析和编码
  • 代码打包输出和发布
  • CI/CD,文档生成等

CSS解决方案

  • inline css
  • css in js
  • styled component
  • sass/less

未完待续....2020年06月15日19:18:23