如何通过typescript入门并实战react ts开发?

2026-05-19 20:251阅读0评论SEO问题
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计6226个文字,预计阅读时间需要25分钟。

如何通过typescript入门并实战react ts开发?

TypeScript 介绍TypeScript 是微软开发的一种自由和开源的编程语言。它是 JavaScript 的超集,在 JavaScript 的基础上增加了可选的静态类型和基于类的面向对象编程特性。

ts介绍

TypeScript是一种由微软开发的自由和开源的编程语言。它是 JavaScript 的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程。

与js关系

ts与js区别 TypeScript JavaScript JavaScript 的超集,用于解决大型项目的代码复杂性 一种脚本语言,用于创建动态网页。 强类型,支持静态和动态类型 动态弱类型语言 可以在编译期间发现并纠正错误 只能在运行时发现错误 不允许改变变量的数据类型 变量可以被赋予不同类型的值 使用ts好处?
  • 规范我们的代码,编译阶段发现错误,在原生基础上加了一层定义
  • ts是js的类型化超集,支持所有js,并在此基础上添加静态类型和面向对象思想
  • 静态类型,定义之后就不能改变
ts缺点
  • 不能被浏览器理解,需要被编译成 JS
  • 有一定的学习成本
  • 代码量增加
环境安装

npm install -g typescript // yarn方式 yarn global add typescript // 检查是否安装成功 tsc -v // 编译ts文件 tsc index.ts

vscode中监听ts文件自动编译

// 1. 初始化tsconfig.json文件 tsc --init 2. 左上角菜单点击终端->运行任务->点击typescript->选择tsc监视tsconfig文件 基础类型 boolean、number 和 string 类型

  • boolean

let isDone: boolean = false; // let isDone: boolean = "123"; //Type 'string' is not assignable to type 'boolean'

赋值与定义的不一致,会报错,静态类型语言的优势就体现出来了,可以帮助我们提前发现代码中的错误。

  • number

let decimal: number = 6;

  • string

let color: string = "blue"; 数组类型

let list: number[] = [1, 2, 3]; // let list: Array<number> = [1, 2, 3]; // 数组里的项写错类型会报错 let list: number[] = [1, 2, "3"]; // error // push 时类型对不上会报错 list.push(4) list.push("4") // error

如果数组想每一项放入不同类型数据,怎么办?那我们可以使用元组类型

元组类型

元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。

let x: [string, number] = ["hello", 10]; // 写错类型会报错 let x: [string, number] = [10, "hello"]; // Error // 越界会报错 let x: [string, number] = ["hello", 10, 110]; // Error // 使用 push 时,不会有越界报错,但是只能 push 定义的 number 或者 string 类型 x.push("hello2") x.push(11) console.log(x) //['hello', 10, 'hello2', 11] // push 一个没有定义的类型,报错 x.push(true) // error undefined 和 null 类型

let u:undefined = undefined let n:null = null

默认情况下 null 和 undefined 是所有类型的子类型。就是说你可以把 null 和 undefined 赋值给 number 类型的变量。

let age: number = null let realName: string = undefined

但是如果指定了 --strictNullChecks 标记,null 和 undefined 只能赋值给 void 和它们各自,不然会报错。

any、unknown 和 void 类型
  • any

不清楚用什么类型,可以使用 any 类型

let notSure: any = 4 notSure = "maybe a string" // 可以是 string 类型 notSure = false // 也可以是 boolean 类型 notSure.name // 可以随便调用属性和方法 notSure.getName()

不过,不建议使用 any,不然就丧失了 TS 的意义。

  • unknown

不建议使用 any,当我不知道一个类型具体是什么时,该怎么办?

如何通过typescript入门并实战react ts开发?

可以使用 unknown 类型

把 param 定义为 any 类型,TS 就能编译通过,没有把潜在的风险暴露出来,万一传的不是 number 类型,不就没有达到预期了吗。

function divide(param: any) { return param / 2; }

把 param 定义为 unknown 类型 ,TS 编译器就能拦住潜在风险
因为不知道 param 的类型,使用运算符 /,导致报错。

function divide(param: unknown) { return param / 2; // error }

配合类型断言,可以解决这个问题

function divide(param: unknown) { return param as number / 2; }

  • void

void类型与 any 类型相反,它表示没有任何类型。
比如函数没有明确返回值,默认返回 Void 类型

function welcome(): void { console.log('hello') } never 类型

never类型表示的是那些永不存在的值的类型。

有些情况下值会永不存在,比如,

  • 如果一个函数执行时抛出了异常,那么这个函数永远不存在返回值,因为抛出异常会直接中断程序运行。
  • 函数中执行无限循环的代码,使得程序永远无法运行到函数返回值那一步。

// 异常 function fn(msg: string): never { throw new Error(msg) } // 死循环 function fn(): never { while (true) {} } 枚举

  • 枚举的意义在于,可以定义一些带名字的常量集合,清晰地表达意图和语义,更容易地理解代码和调试。
  • 常用于和后端联调时,区分后端返回的一些代表状态语义的数字或字符串,降低阅读代码时的心智负担。
基本使用

定义一个数字枚举

enum Color { Red, Green, Blue, } console.log(Color.Red) // 0 console.log(Color.Green) // 1 console.log(Color.Blue) // 2 console.log(Color[0]) // Red

特点:

  • 数字递增,枚举成员会被赋值为从 0 开始递增的数字
  • 反向映射,枚举会对枚举值到枚举名进行反向映射,

如果枚举第一个元素赋有初始值,就会从初始值开始递增

enum Color { Red=3, Green, Blue, } console.log(Color.Red) // 3 console.log(Color.Green) // 4 console.log(Color.Blue) // 5 手动赋值

定义一个枚举来管理外卖状态,分别有已下单,配送中,已接收三个状态。

enum Status { Buy = 1, Send, Receive } console.log(Status.Buy) // 1 console.log(Status.Send) // 2 console.log(Status.Receive) // 3

但有时候后端给你返回的数据状态是乱的,就需要我们手动赋值。
比如后端说 Buy 是 100,Send 是 20,Receive 是 1,就可以这么写:

enum Status { Buy = 100, Send = 20, Receive = 1 } console.log(Status.Buy) // 100 console.log(Status.Send) // 20 console.log(Status.Receive) // 1 计算成员

枚举中的成员可以被计算

enum FileAccess { // constant members None, Read = 1 << 1, Write = 1 << 2, ReadWrite = Read | Write, // computed member G = "123".length } console.log(FileAccess.None) // 0 console.log(FileAccess.Read) // 2 -> 010 console.log(FileAccess.Write) // 4 -> 100 console.log(FileAccess.ReadWrite) // 6 -> 110 console.log(FileAccess.G) // 3 字符串枚举

字符串枚举意义在于,提供有具体语义的字符串,可以更容易地理解代码和调试。

enum Direction { Up = "UP", Down = "DOWN", Left = "LEFT", Right = "RIGHT", } const value = 'UP' if (value === Direction.Up) { // do something } 常量枚举

常量枚举编译出来的 JS 代码会简洁很多,提高了性能。常量枚举不允许包含计算成员

const enum Direction { Up = "UP", Down = "DOWN", Left = "LEFT", Right = "RIGHT", } const value = 'UP' if (value === Direction.Up) { // do something }

会被编译成:

const value = 'UP'; if (value === "UP" /* Up */) { // do something }

不写const会被编译成:

(function (Direction) { Direction["Up"] = "UP"; Direction["Down"] = "DOWN"; Direction["Left"] = "LEFT"; Direction["Right"] = "RIGHT"; })(Direction || (Direction = {})); const value = 'UP'; if (value === Direction.Up) { // do something }

这一堆定义枚举的逻辑会在编译阶段会被删除,常量枚举成员在使用的地方被内联进去。

枚举类型

当枚举作为类型时,表示该属性只能为枚举中的某一个成员

enum SEX{ man = '男', woman = '女', unknown = '未知' } let arr:Array<SEX> = [SEX.man,SEX.woman] let s:SEX = SEX.man console.log(s); // 男 接口

在面向对象语言中,接口(Interfaces)是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类(classes)去实现(implement)。

在ts中,接口除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述。

接口一般首字母大写

定义对象类型

定义对象类型,可以对对象形状进行描述,接口和对象形状必须保持一致

interface IntfPerson { name: string; age: number; } const jack: IntfPerson = { name: 'jack', age: 25 } // 缺少或多出属性都会报错 const jack: IntfPerson = { name: 'jack' } const jack: IntfPerson = { name: 'jack', age: 25, city: '深圳' } 可选属性

如果不需要完全匹配形状,可以使用可选属性

interface IntfPerson { name: string; age?: number; } const jack: IntfPerson = { name: 'jack' } 只读属性

有时候我们希望对象中的一些字段只能在创建的时候被赋值,那么可以用 readonly 定义只读属性

interface IntfPerson { readonly id: number; name: string; age?: number; } let jack: IntfPerson = { id: 89757, name: 'Tom', }; // 报错 jack.id = 9527; 描述函数类型

interface 也可以用来描述函数类型

interface ISum { (x:number,y:number):number } const add:ISum = (num1, num2) => { return num1 + num2 } console.log(add(1,2)); // 3 类 es6中类的用法

先了解下es6中类的用法:

  • 使用 class 定义类,使用 constructor 定义构造函数
  • 通过 new 生成新实例的时候,会自动调用构造函数。

class Animal { public name; constructor(name:string) { this.name = name; } sayHi() { return `My name is ${this.name}`; } } let a = new Animal('Kerry'); console.log(a.sayHi()); // My name is Kerry

  1. 类的继承
    使用 extends 关键字实现继承,子类中使用 super 关键字来调用父类的构造函数和方法

class Cat extends Animal { constructor(name:string) { super(name); // 调用父类的 constructor(name) console.log(this.name); } sayHi() { return 'Meow, ' + super.sayHi(); // 调用父类的 sayHi() } } let c = new Cat('Tom'); // Tom console.log(c.sayHi()); // Meow, My name is Tom

  1. 存取器
    使用 getter 和 setter 可以改变属性的赋值和读取行为

class Animal { constructor(name:string) { this.name = name; } get name() { return 'Kerry'; } set name(value) { console.log('setter: ' + value); } } let a = new Animal('Kitty'); // setter: Kitty a.name = 'Tom'; // setter: Tom console.log(a.name); // Kerry

  1. 静态方法
    使用 static 修饰符修饰的方法称为静态方法,它们不需要实例化,而是直接通过类来调用:

class Animal { static cry() { console.log('会哭'); // 会哭 } } Animal.cry(); // 报错 const animal = new Animal() animal.cry() // error es7中类的用法

  1. 实例属性
    ES6 中实例的属性只能通过构造函数中的 this.xxx 来定义,ES7 提案中可以直接在类里面定义:

class Animal { name = 'Kerry'; constructor() { // ... } } let a = new Animal(); console.log(a.name); // Kerry

  1. 静态属性
    ES7 提案中,可以使用 static 定义一个静态属性:

class Animal { static num = 42; constructor() { // ... } } console.log(Animal.num); // 42 ts中类的用法

  1. 类修饰符

ts里面定义属性的时候,提供了三种修饰符

public:公有,在类里面、子类、类外面都可以访问
protected:保护类型,在类里面、子类里面可以访问,在类外面没法访问
private:私有,在类里面可以访问,子类、类外部都没法访问

属性如果不加修饰符,默认就是公有public

class Person { public name:string // protected name:string // private name:string constructor(name:string){ this.name = name } run():string{ return `${this.name}在运动` } } class Web extends Person { constructor(name:string){ super(name) // 初始化父类的构造函数 } run():string{ return `${this.name}在运动-子类` } work(): string { return `${this.name}在工作` } } const w = new Web('kerry') console.log(w.run()); // kerry在运动-子类 console.log(w.work()); // kerry在工作 console.log(w.name); // kerry

  1. 抽象类
  • 抽象类是不允许被实例化

abstract class Animal { public name; public constructor(name:string) { this.name = name; } } let a = new Animal('Kerry'); // error

  • 抽象类中的抽象方法必须被子类实现

abstract class Animal { public name; public constructor(name:string) { this.name = name; } public abstract sayHi():void; } class Cat extends Animal { public sayHi() { console.log(`Meow, My name is ${this.name}`); } } let cat = new Cat('Kerry');

  • 给类添加类型

class Animal { name: string; constructor(name: string) { this.name = name; } sayHi(): string { return `My name is ${this.name}`; } } let a: Animal = new Animal('Kerry'); console.log(a.sayHi()); // My name is Kerry 接口和类 类实现接口

一般来讲,一个类只能继承自另一个类,有时候不同类之间可以有一些共有的特性,这时候就可以把特性提取成接口(interfaces),
用 implements 关键字来实现,大大提高了面向对象的灵活性

这里举个例子:
门是一个类,防盗门是门的子类。如果防盗门有一个报警器的功能,我们可以简单的给防盗门添加一个报警方法。这时候如果有另一个类,车,也有报警器的功能,就可以考虑把报警器提取出来,作为一个接口,防盗门和车都去实现它

interface Alarm { alert(): void; } class Door { } class SecurityDoor extends Door implements Alarm { alert() { console.log('SecurityDoor alert'); } } class Car implements Alarm { alert() { console.log('Car alert'); } } 一个类可以实现多个接口

interface Alarm { alert(): void; } interface Light { lightOn(): void; lightOff(): void; } class Car implements Alarm, Light { alert() { console.log('Car alert'); } lightOn() { console.log('Car light on'); } lightOff() { console.log('Car light off'); } } 接口继承接口

interface Alarm { alert(): void; } interface LightableAlarm extends Alarm { lightOn(): void; lightOff(): void; } class Car implements LightableAlarm { lightOn(): void { console.log('Car light on'); } lightOff(): void { console.log('Car light off'); } alert(): void { console.log('Car light on'); } } 类型断言和类型推论 类型断言

语法:值 as 类型
使用断言,目的是告诉ts,我比你更清楚这个参数是什么类型,以此防止报错

function getLength(arg: number | string): number { const str = arg as string if (str.length) { return str.length } else { const number = arg as number return number.toString().length } } console.log(getLength(123)); // 3 console.log(getLength("123")); // 3 // 需要注意的是,如果把一个类型断言成联合类型中不存在的类型则会报错 function getLength(arg: number | string): number { return (arg as number[]).length // error } 类型推论

ts里,在有些没有明确指出类型的地方,类型推论会帮助提供类型。

  • 定义时不赋值,ts会自动推导成 any 类型

let a a = 18 a = 'hello'

  • 初始化变量

// 因为赋值的时候赋的是一个字符串类型,所以ts自动推导出userName是一个string类型。 let userName = 'lin' // userName = 123 //再修改时会报错

  • 设置默认参数值

// 会自动推导为number类型 function printAge(num = 18) { console.log(num) return num } printAge(20) // printAge("Kerry") // error

  • 函数返回值

// 定义没有返回值的函数,会自动推导返回值为void类型 function welcome() { console.log('hello') } welcome() 联合类型、交叉类型和类型别名

  • 联合类型 | 是指可以取几种类型中的任意一种,而交叉类型 & 是指把几种类型合并起来。
  • 交叉类型和 interface 的 extends 非常类似,都是为了实现对象形状的组合和扩展。
联合类型

如果希望一个变量可以支持多种类型,就可以用联合类型(union types)来定义。

例如,一个变量既支持 number 类型,又支持 string 类型

// 联合类型大大提高了类型的可扩展性 let num: number | string | boolean num = 8 num = 'eight' num = true 交叉类型

如果要对对象形状进行扩展,可以使用交叉类型 &
比如 Person 有 name 和 age 的属性,而 Student 在 name 和 age 的基础上还有 grade 属性
这和类的继承是一模一样的,这样 Student 就继承了 Person 上的属性,

interface Person { name: string age: number } type Student = Person & { grade: number } 类型别名

类型别名,就是给类型起个别名。类型别名用 type 关键字来书写,有了类型别名,我们书写 TS 的时候可以更加方便简洁。
比如这个例子,getName 这个函数接收的参数可能是字符串,可能是函数

type Name = string type NameResolver = () => string type NameOrResolver = Name | NameResolver // 联合类型 function getName(n: NameOrResolver): Name { if (typeof n === 'string') { return n } else { return n() } } // 调用 console.log(getName('kerry')); // kerry console.log(getName(() => 'kerry')); // kerry type 和 interface

能用interface就用interface,实现不了再考虑用type

相同点1:都可以描述一个对象或者函数
  • 描述一个对象

type IntfPerson = { name: string age: number } interface IntfPerson { name: string age: number } const person: IntfPerson = { name: 'kerry', age: 18 }

  • 描述一个函数

type addType = (num1:number,num2:number) => number interface addType { (num1:number,num2:number):number } const add:addType = (num1, num2) => { return num1 + num2 } 相同点2:都可以实现继承

interface 使用 extends 实现继承, type 使用交叉类型实现继承

我们定义一个 IntfPerson 类型和 Student 类型,Student 继承自 IntfPerson

// interface 继承 interface interface Person { name: string } interface Student extends Person { grade: number } // type 继承 type type IntfPerson = { name: string } type Student = IntfPerson & { grade: number } // 用交叉类型 // interface 继承 type type IntfPerson = { name: string } interface Student extends IntfPerson { grade: number } // type 继承 interface interface IntfPerson { name: string } type Student = IntfPerson & { grade: number } // 用交叉类型 不同点1:type 可以声明基本类型、联合类型、交叉类型、元组,interface 不行

基本类型 type Name = string // 联合类型 type Name = string | number // 元组 type Name = [string, number] 交叉类型 interface IntfPerson { name: string } type Student = IntfPerson & { grade: number } let myName:Name = 'kerry' let myName:Name = 123 let myName:Name = ['kerry',2] let kerry:Student = { name:'kerry', grade: 1 } 不同点2: interface可以声明合并,type不行

interface User{ nage: string, age: number, } interface User{ sex: string } let user: User = { nage: '小明', age: 10, sex: '男' } 泛型

泛型是指在定义函数、接口或类的时候,不预先指定具体类型,而是在使用的时候再指定类型。可以使得输入输出类型统一,且可以输入输出任何类型。

基本用法

定义一个print函数,传入一个string类型参数并返回一个string类型

  • 不使用泛型:

function print(arg:string):string { console.log(arg) return arg }

假如这时候又多了一个number类型,则需要这样写

function print(arg:string | number):string | number { console.log(arg) return arg } const result:string | number = print(123) console.log('result',result); // 123

  • 使用泛型解决

function print<T>(arg:T):T { console.log(arg) return arg } // 两种方式使用: // 方式1:定义要使用的类型 const result = print<number>(123) // 方式2:使用ts类型自动推断 const result = print(123) 接口中使用泛型,定义函数类型

interface IGeneric<T> { (arg: T): void } function fn<T>(arg: T): void { console.log(arg); } let myFn: IGeneric<number> = fn; myFn(13); //13 在类中使用泛型

定义一个栈,有入栈和出栈两个方法,如果想入栈和出栈的元素类型统一,写法如下:

class Stack<T> { private data: T[] = [] push(item:T) { return this.data.push(item) } pop():T | undefined { return this.data.pop() } } 入栈和出栈是number类型 const s1 = new Stack<number>() s1.push(1) console.log(s1.pop()); // 1 泛型约束

假设现在有这么一个函数,打印传入参数的长度

function printLength<T>(arg: T): T { console.log(arg.length) // 因为不确定 T 是否有 length 属性,会报错 return arg } printLength([1,2,3])

结合interface,通过extends来实现泛型约束

interface ILength { length: number } function printLength<T extends ILength>(arg: T): T { console.log(arg.length) return arg } printLength([1,2,3]) 装饰器 定义

  • 装饰器是一种特殊类型的声明,它能够被附加到类声明、方法、属性或参数上,可以修改类的行为。
  • 通俗讲装饰器就是一个方法,可以注入到类、方法、属性参数上来扩展类、属性、方法、参数的功能
  • 常见的装饰器有:类装饰器、属性装饰器、方法装饰器、参数装饰器
  • 装饰器的写法:普通装饰器(不可传参)、装饰器工厂(可传参)
类装饰器:普通装饰器

function logClass(parmas:any){ parmas.prototype.apiUrl = "www.baidu.com" } @logClass class HttpClient { constructor(){ } getData(){ } } const www.baidu.com 类装饰器:装饰器工厂

function logClass(parmas:string){ return function(target:any){ console.log(parmas) console.log(target) } } @logClass('hello') class HttpClient { constructor(){ } getData(){ } }

console打印出来结果:

hello index.js:134 class HttpClient { constructor() { } getData() { } } 属性装饰器

接收2个参数,target当前类的原型对象,attr当前属性名称

function logClass(parmas:string){ return function(target:any){ console.log(parmas) console.log(target) } } function logProperty(params:string){ return function(target:any,attr:any){ console.log(target) // 当前类的原型对象 console.log(attr) // url // 修改属性值 target[attr] = params } } @logClass('hello') class HttpClient { @logProperty('api.xxx.com') public url: string | undefined constructor(){ } getData(){ console.log(this.url) // api.xxx.com } } const api.xxx.com') getData(...args:any[]){ console.log(args); // ['123', '122'] console.log('我是getData里面的方法') } } const localhost:3000/)

ts实战

我们使用react+ts实现一个todo List功能

功能包括:

  • 查看任务列表
  • 添加、删除任务
  • 完成任务
先看效果图

初始化一个react项目

create-react-app.dev/

  1. 初始化一个typescript项目

yarn create react-app react-ts-demo --template typescript

  1. 运行项目

cd react-ts-demo & yarn start

  1. 浏览器访问:localhost:3000/
代码实现
  1. 创建一个父组件todolist

export interface ITodoItem { taskName: string, status: TaskStatus } // React.FC表示react的一个函数组件,是React.FunctionComponent的简写形式 const TodoList:React.FC = ()=>{ const [list, setList] = useState<Array<ITodoItem>>([]) const [inputValue, setInputValue] = useState<string>("") const updateList = (list:ITodoItem[])=>{ setList(list) saveTaskList(list) } const handleAdd = ()=>{ if(!inputValue.trim()){ return alert("输入不能为空") } let [...newList] = list; newList.push({ taskName: inputValue, status: TaskStatus.PENDING }) updateList(newList) setInputValue("") } const handleStart = (index:number)=>{ let [...newList] = list; newList = newList.map((item,indx)=>{ if(indx === index){ return { ...item, status: TaskStatus.IN_PROGRESS } } return item }) updateList(newList) } const handleComplete = (index:number)=>{ let [...newList] = list; newList = newList.map((item,indx)=>{ if(indx === index){ return { ...item, status: TaskStatus.COMPLETED } } return item }) updateList(newList) } const handleDel = (index:number)=>{ let [...newList] = list; newList.splice(index,1) updateList(newList) } // 初始化列表数据 useEffect(() => { const list = getTaskList() setList(list) }, []) return ( <div className={styles.container}> <h1 className={styles.title}>TODO LIST</h1> <div className={styles.form}> <input className={styles.textInput} type="text" value={inputValue} placeholder="输入任务名" onChange={(e)=>setInputValue(e.target.value)}/> <button className={styles.addBtn} onClick={handleAdd}>添加任务</button> </div> <div className={styles.list}> {list.map((item,index)=> { return <TodoItem key={index} index={index} item={item} handleStart={handleStart} handleComplete={handleComplete} handleDel={handleDel}/> })} </div> </div> ) } export default TodoList;

  1. 创建一个子组件todoItem并在父组件中引入

interface IProps { index: number item: ITodoItem handleStart: (index: number) => void handleComplete: (index: number) => void handleDel: (index: number) => void } // 对于接收的组件参数,需要定义参数类型 const TodoItem:React.FC<IProps> = ({index,item,handleStart,handleComplete,handleDel})=>{ return ( <div className={styles.taskItem}> <div className={styles.name}>{item.taskName}</div> <div className={styles.statusText}> {item.status===TaskStatus.PENDING && <span className={styles.pending}>待开始</span>} {item.status===TaskStatus.IN_PROGRESS && <span className={styles.inProgress}>进行中</span>} {item.status===TaskStatus.COMPLETED && <span className={styles.completed}>已完成</span>} </div> <div className={styles.btns}> {item.status===TaskStatus.PENDING && ( <button className={styles.btn} onClick={()=>handleStart(index)}>开始</button> )} {item.status===TaskStatus.IN_PROGRESS && ( <button className={styles.btn} onClick={()=>handleComplete(index)}>完成</button> )} <button className={styles.btn} onClick={()=>handleDel(index)}>删除</button> </div> </div> ) } export default TodoItem

  1. 数据保存和获取

我们在第一次进入的时候获取初始化数据

const getTaskList = ()=> { const taskList = localStorage.getItem("taskList") if(taskList){ return JSON.parse(taskList) } return [] } // 初始化列表数据 useEffect(() => { const list = getTaskList() setList(list) }, [])

添加、完成以及删除的时候保存和更新数据

const saveTaskList = (list: ITodoItem[])=>{ localStorage.setItem('taskList',JSON.stringify(list)) } const updateList = (list:ITodoItem[])=>{ setList(list) saveTaskList(list) }

上面代码我已经上传到github,欢迎star

本文共计6226个文字,预计阅读时间需要25分钟。

如何通过typescript入门并实战react ts开发?

TypeScript 介绍TypeScript 是微软开发的一种自由和开源的编程语言。它是 JavaScript 的超集,在 JavaScript 的基础上增加了可选的静态类型和基于类的面向对象编程特性。

ts介绍

TypeScript是一种由微软开发的自由和开源的编程语言。它是 JavaScript 的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程。

与js关系

ts与js区别 TypeScript JavaScript JavaScript 的超集,用于解决大型项目的代码复杂性 一种脚本语言,用于创建动态网页。 强类型,支持静态和动态类型 动态弱类型语言 可以在编译期间发现并纠正错误 只能在运行时发现错误 不允许改变变量的数据类型 变量可以被赋予不同类型的值 使用ts好处?
  • 规范我们的代码,编译阶段发现错误,在原生基础上加了一层定义
  • ts是js的类型化超集,支持所有js,并在此基础上添加静态类型和面向对象思想
  • 静态类型,定义之后就不能改变
ts缺点
  • 不能被浏览器理解,需要被编译成 JS
  • 有一定的学习成本
  • 代码量增加
环境安装

npm install -g typescript // yarn方式 yarn global add typescript // 检查是否安装成功 tsc -v // 编译ts文件 tsc index.ts

vscode中监听ts文件自动编译

// 1. 初始化tsconfig.json文件 tsc --init 2. 左上角菜单点击终端->运行任务->点击typescript->选择tsc监视tsconfig文件 基础类型 boolean、number 和 string 类型

  • boolean

let isDone: boolean = false; // let isDone: boolean = "123"; //Type 'string' is not assignable to type 'boolean'

赋值与定义的不一致,会报错,静态类型语言的优势就体现出来了,可以帮助我们提前发现代码中的错误。

  • number

let decimal: number = 6;

  • string

let color: string = "blue"; 数组类型

let list: number[] = [1, 2, 3]; // let list: Array<number> = [1, 2, 3]; // 数组里的项写错类型会报错 let list: number[] = [1, 2, "3"]; // error // push 时类型对不上会报错 list.push(4) list.push("4") // error

如果数组想每一项放入不同类型数据,怎么办?那我们可以使用元组类型

元组类型

元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。

let x: [string, number] = ["hello", 10]; // 写错类型会报错 let x: [string, number] = [10, "hello"]; // Error // 越界会报错 let x: [string, number] = ["hello", 10, 110]; // Error // 使用 push 时,不会有越界报错,但是只能 push 定义的 number 或者 string 类型 x.push("hello2") x.push(11) console.log(x) //['hello', 10, 'hello2', 11] // push 一个没有定义的类型,报错 x.push(true) // error undefined 和 null 类型

let u:undefined = undefined let n:null = null

默认情况下 null 和 undefined 是所有类型的子类型。就是说你可以把 null 和 undefined 赋值给 number 类型的变量。

let age: number = null let realName: string = undefined

但是如果指定了 --strictNullChecks 标记,null 和 undefined 只能赋值给 void 和它们各自,不然会报错。

any、unknown 和 void 类型
  • any

不清楚用什么类型,可以使用 any 类型

let notSure: any = 4 notSure = "maybe a string" // 可以是 string 类型 notSure = false // 也可以是 boolean 类型 notSure.name // 可以随便调用属性和方法 notSure.getName()

不过,不建议使用 any,不然就丧失了 TS 的意义。

  • unknown

不建议使用 any,当我不知道一个类型具体是什么时,该怎么办?

如何通过typescript入门并实战react ts开发?

可以使用 unknown 类型

把 param 定义为 any 类型,TS 就能编译通过,没有把潜在的风险暴露出来,万一传的不是 number 类型,不就没有达到预期了吗。

function divide(param: any) { return param / 2; }

把 param 定义为 unknown 类型 ,TS 编译器就能拦住潜在风险
因为不知道 param 的类型,使用运算符 /,导致报错。

function divide(param: unknown) { return param / 2; // error }

配合类型断言,可以解决这个问题

function divide(param: unknown) { return param as number / 2; }

  • void

void类型与 any 类型相反,它表示没有任何类型。
比如函数没有明确返回值,默认返回 Void 类型

function welcome(): void { console.log('hello') } never 类型

never类型表示的是那些永不存在的值的类型。

有些情况下值会永不存在,比如,

  • 如果一个函数执行时抛出了异常,那么这个函数永远不存在返回值,因为抛出异常会直接中断程序运行。
  • 函数中执行无限循环的代码,使得程序永远无法运行到函数返回值那一步。

// 异常 function fn(msg: string): never { throw new Error(msg) } // 死循环 function fn(): never { while (true) {} } 枚举

  • 枚举的意义在于,可以定义一些带名字的常量集合,清晰地表达意图和语义,更容易地理解代码和调试。
  • 常用于和后端联调时,区分后端返回的一些代表状态语义的数字或字符串,降低阅读代码时的心智负担。
基本使用

定义一个数字枚举

enum Color { Red, Green, Blue, } console.log(Color.Red) // 0 console.log(Color.Green) // 1 console.log(Color.Blue) // 2 console.log(Color[0]) // Red

特点:

  • 数字递增,枚举成员会被赋值为从 0 开始递增的数字
  • 反向映射,枚举会对枚举值到枚举名进行反向映射,

如果枚举第一个元素赋有初始值,就会从初始值开始递增

enum Color { Red=3, Green, Blue, } console.log(Color.Red) // 3 console.log(Color.Green) // 4 console.log(Color.Blue) // 5 手动赋值

定义一个枚举来管理外卖状态,分别有已下单,配送中,已接收三个状态。

enum Status { Buy = 1, Send, Receive } console.log(Status.Buy) // 1 console.log(Status.Send) // 2 console.log(Status.Receive) // 3

但有时候后端给你返回的数据状态是乱的,就需要我们手动赋值。
比如后端说 Buy 是 100,Send 是 20,Receive 是 1,就可以这么写:

enum Status { Buy = 100, Send = 20, Receive = 1 } console.log(Status.Buy) // 100 console.log(Status.Send) // 20 console.log(Status.Receive) // 1 计算成员

枚举中的成员可以被计算

enum FileAccess { // constant members None, Read = 1 << 1, Write = 1 << 2, ReadWrite = Read | Write, // computed member G = "123".length } console.log(FileAccess.None) // 0 console.log(FileAccess.Read) // 2 -> 010 console.log(FileAccess.Write) // 4 -> 100 console.log(FileAccess.ReadWrite) // 6 -> 110 console.log(FileAccess.G) // 3 字符串枚举

字符串枚举意义在于,提供有具体语义的字符串,可以更容易地理解代码和调试。

enum Direction { Up = "UP", Down = "DOWN", Left = "LEFT", Right = "RIGHT", } const value = 'UP' if (value === Direction.Up) { // do something } 常量枚举

常量枚举编译出来的 JS 代码会简洁很多,提高了性能。常量枚举不允许包含计算成员

const enum Direction { Up = "UP", Down = "DOWN", Left = "LEFT", Right = "RIGHT", } const value = 'UP' if (value === Direction.Up) { // do something }

会被编译成:

const value = 'UP'; if (value === "UP" /* Up */) { // do something }

不写const会被编译成:

(function (Direction) { Direction["Up"] = "UP"; Direction["Down"] = "DOWN"; Direction["Left"] = "LEFT"; Direction["Right"] = "RIGHT"; })(Direction || (Direction = {})); const value = 'UP'; if (value === Direction.Up) { // do something }

这一堆定义枚举的逻辑会在编译阶段会被删除,常量枚举成员在使用的地方被内联进去。

枚举类型

当枚举作为类型时,表示该属性只能为枚举中的某一个成员

enum SEX{ man = '男', woman = '女', unknown = '未知' } let arr:Array<SEX> = [SEX.man,SEX.woman] let s:SEX = SEX.man console.log(s); // 男 接口

在面向对象语言中,接口(Interfaces)是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类(classes)去实现(implement)。

在ts中,接口除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述。

接口一般首字母大写

定义对象类型

定义对象类型,可以对对象形状进行描述,接口和对象形状必须保持一致

interface IntfPerson { name: string; age: number; } const jack: IntfPerson = { name: 'jack', age: 25 } // 缺少或多出属性都会报错 const jack: IntfPerson = { name: 'jack' } const jack: IntfPerson = { name: 'jack', age: 25, city: '深圳' } 可选属性

如果不需要完全匹配形状,可以使用可选属性

interface IntfPerson { name: string; age?: number; } const jack: IntfPerson = { name: 'jack' } 只读属性

有时候我们希望对象中的一些字段只能在创建的时候被赋值,那么可以用 readonly 定义只读属性

interface IntfPerson { readonly id: number; name: string; age?: number; } let jack: IntfPerson = { id: 89757, name: 'Tom', }; // 报错 jack.id = 9527; 描述函数类型

interface 也可以用来描述函数类型

interface ISum { (x:number,y:number):number } const add:ISum = (num1, num2) => { return num1 + num2 } console.log(add(1,2)); // 3 类 es6中类的用法

先了解下es6中类的用法:

  • 使用 class 定义类,使用 constructor 定义构造函数
  • 通过 new 生成新实例的时候,会自动调用构造函数。

class Animal { public name; constructor(name:string) { this.name = name; } sayHi() { return `My name is ${this.name}`; } } let a = new Animal('Kerry'); console.log(a.sayHi()); // My name is Kerry

  1. 类的继承
    使用 extends 关键字实现继承,子类中使用 super 关键字来调用父类的构造函数和方法

class Cat extends Animal { constructor(name:string) { super(name); // 调用父类的 constructor(name) console.log(this.name); } sayHi() { return 'Meow, ' + super.sayHi(); // 调用父类的 sayHi() } } let c = new Cat('Tom'); // Tom console.log(c.sayHi()); // Meow, My name is Tom

  1. 存取器
    使用 getter 和 setter 可以改变属性的赋值和读取行为

class Animal { constructor(name:string) { this.name = name; } get name() { return 'Kerry'; } set name(value) { console.log('setter: ' + value); } } let a = new Animal('Kitty'); // setter: Kitty a.name = 'Tom'; // setter: Tom console.log(a.name); // Kerry

  1. 静态方法
    使用 static 修饰符修饰的方法称为静态方法,它们不需要实例化,而是直接通过类来调用:

class Animal { static cry() { console.log('会哭'); // 会哭 } } Animal.cry(); // 报错 const animal = new Animal() animal.cry() // error es7中类的用法

  1. 实例属性
    ES6 中实例的属性只能通过构造函数中的 this.xxx 来定义,ES7 提案中可以直接在类里面定义:

class Animal { name = 'Kerry'; constructor() { // ... } } let a = new Animal(); console.log(a.name); // Kerry

  1. 静态属性
    ES7 提案中,可以使用 static 定义一个静态属性:

class Animal { static num = 42; constructor() { // ... } } console.log(Animal.num); // 42 ts中类的用法

  1. 类修饰符

ts里面定义属性的时候,提供了三种修饰符

public:公有,在类里面、子类、类外面都可以访问
protected:保护类型,在类里面、子类里面可以访问,在类外面没法访问
private:私有,在类里面可以访问,子类、类外部都没法访问

属性如果不加修饰符,默认就是公有public

class Person { public name:string // protected name:string // private name:string constructor(name:string){ this.name = name } run():string{ return `${this.name}在运动` } } class Web extends Person { constructor(name:string){ super(name) // 初始化父类的构造函数 } run():string{ return `${this.name}在运动-子类` } work(): string { return `${this.name}在工作` } } const w = new Web('kerry') console.log(w.run()); // kerry在运动-子类 console.log(w.work()); // kerry在工作 console.log(w.name); // kerry

  1. 抽象类
  • 抽象类是不允许被实例化

abstract class Animal { public name; public constructor(name:string) { this.name = name; } } let a = new Animal('Kerry'); // error

  • 抽象类中的抽象方法必须被子类实现

abstract class Animal { public name; public constructor(name:string) { this.name = name; } public abstract sayHi():void; } class Cat extends Animal { public sayHi() { console.log(`Meow, My name is ${this.name}`); } } let cat = new Cat('Kerry');

  • 给类添加类型

class Animal { name: string; constructor(name: string) { this.name = name; } sayHi(): string { return `My name is ${this.name}`; } } let a: Animal = new Animal('Kerry'); console.log(a.sayHi()); // My name is Kerry 接口和类 类实现接口

一般来讲,一个类只能继承自另一个类,有时候不同类之间可以有一些共有的特性,这时候就可以把特性提取成接口(interfaces),
用 implements 关键字来实现,大大提高了面向对象的灵活性

这里举个例子:
门是一个类,防盗门是门的子类。如果防盗门有一个报警器的功能,我们可以简单的给防盗门添加一个报警方法。这时候如果有另一个类,车,也有报警器的功能,就可以考虑把报警器提取出来,作为一个接口,防盗门和车都去实现它

interface Alarm { alert(): void; } class Door { } class SecurityDoor extends Door implements Alarm { alert() { console.log('SecurityDoor alert'); } } class Car implements Alarm { alert() { console.log('Car alert'); } } 一个类可以实现多个接口

interface Alarm { alert(): void; } interface Light { lightOn(): void; lightOff(): void; } class Car implements Alarm, Light { alert() { console.log('Car alert'); } lightOn() { console.log('Car light on'); } lightOff() { console.log('Car light off'); } } 接口继承接口

interface Alarm { alert(): void; } interface LightableAlarm extends Alarm { lightOn(): void; lightOff(): void; } class Car implements LightableAlarm { lightOn(): void { console.log('Car light on'); } lightOff(): void { console.log('Car light off'); } alert(): void { console.log('Car light on'); } } 类型断言和类型推论 类型断言

语法:值 as 类型
使用断言,目的是告诉ts,我比你更清楚这个参数是什么类型,以此防止报错

function getLength(arg: number | string): number { const str = arg as string if (str.length) { return str.length } else { const number = arg as number return number.toString().length } } console.log(getLength(123)); // 3 console.log(getLength("123")); // 3 // 需要注意的是,如果把一个类型断言成联合类型中不存在的类型则会报错 function getLength(arg: number | string): number { return (arg as number[]).length // error } 类型推论

ts里,在有些没有明确指出类型的地方,类型推论会帮助提供类型。

  • 定义时不赋值,ts会自动推导成 any 类型

let a a = 18 a = 'hello'

  • 初始化变量

// 因为赋值的时候赋的是一个字符串类型,所以ts自动推导出userName是一个string类型。 let userName = 'lin' // userName = 123 //再修改时会报错

  • 设置默认参数值

// 会自动推导为number类型 function printAge(num = 18) { console.log(num) return num } printAge(20) // printAge("Kerry") // error

  • 函数返回值

// 定义没有返回值的函数,会自动推导返回值为void类型 function welcome() { console.log('hello') } welcome() 联合类型、交叉类型和类型别名

  • 联合类型 | 是指可以取几种类型中的任意一种,而交叉类型 & 是指把几种类型合并起来。
  • 交叉类型和 interface 的 extends 非常类似,都是为了实现对象形状的组合和扩展。
联合类型

如果希望一个变量可以支持多种类型,就可以用联合类型(union types)来定义。

例如,一个变量既支持 number 类型,又支持 string 类型

// 联合类型大大提高了类型的可扩展性 let num: number | string | boolean num = 8 num = 'eight' num = true 交叉类型

如果要对对象形状进行扩展,可以使用交叉类型 &
比如 Person 有 name 和 age 的属性,而 Student 在 name 和 age 的基础上还有 grade 属性
这和类的继承是一模一样的,这样 Student 就继承了 Person 上的属性,

interface Person { name: string age: number } type Student = Person & { grade: number } 类型别名

类型别名,就是给类型起个别名。类型别名用 type 关键字来书写,有了类型别名,我们书写 TS 的时候可以更加方便简洁。
比如这个例子,getName 这个函数接收的参数可能是字符串,可能是函数

type Name = string type NameResolver = () => string type NameOrResolver = Name | NameResolver // 联合类型 function getName(n: NameOrResolver): Name { if (typeof n === 'string') { return n } else { return n() } } // 调用 console.log(getName('kerry')); // kerry console.log(getName(() => 'kerry')); // kerry type 和 interface

能用interface就用interface,实现不了再考虑用type

相同点1:都可以描述一个对象或者函数
  • 描述一个对象

type IntfPerson = { name: string age: number } interface IntfPerson { name: string age: number } const person: IntfPerson = { name: 'kerry', age: 18 }

  • 描述一个函数

type addType = (num1:number,num2:number) => number interface addType { (num1:number,num2:number):number } const add:addType = (num1, num2) => { return num1 + num2 } 相同点2:都可以实现继承

interface 使用 extends 实现继承, type 使用交叉类型实现继承

我们定义一个 IntfPerson 类型和 Student 类型,Student 继承自 IntfPerson

// interface 继承 interface interface Person { name: string } interface Student extends Person { grade: number } // type 继承 type type IntfPerson = { name: string } type Student = IntfPerson & { grade: number } // 用交叉类型 // interface 继承 type type IntfPerson = { name: string } interface Student extends IntfPerson { grade: number } // type 继承 interface interface IntfPerson { name: string } type Student = IntfPerson & { grade: number } // 用交叉类型 不同点1:type 可以声明基本类型、联合类型、交叉类型、元组,interface 不行

基本类型 type Name = string // 联合类型 type Name = string | number // 元组 type Name = [string, number] 交叉类型 interface IntfPerson { name: string } type Student = IntfPerson & { grade: number } let myName:Name = 'kerry' let myName:Name = 123 let myName:Name = ['kerry',2] let kerry:Student = { name:'kerry', grade: 1 } 不同点2: interface可以声明合并,type不行

interface User{ nage: string, age: number, } interface User{ sex: string } let user: User = { nage: '小明', age: 10, sex: '男' } 泛型

泛型是指在定义函数、接口或类的时候,不预先指定具体类型,而是在使用的时候再指定类型。可以使得输入输出类型统一,且可以输入输出任何类型。

基本用法

定义一个print函数,传入一个string类型参数并返回一个string类型

  • 不使用泛型:

function print(arg:string):string { console.log(arg) return arg }

假如这时候又多了一个number类型,则需要这样写

function print(arg:string | number):string | number { console.log(arg) return arg } const result:string | number = print(123) console.log('result',result); // 123

  • 使用泛型解决

function print<T>(arg:T):T { console.log(arg) return arg } // 两种方式使用: // 方式1:定义要使用的类型 const result = print<number>(123) // 方式2:使用ts类型自动推断 const result = print(123) 接口中使用泛型,定义函数类型

interface IGeneric<T> { (arg: T): void } function fn<T>(arg: T): void { console.log(arg); } let myFn: IGeneric<number> = fn; myFn(13); //13 在类中使用泛型

定义一个栈,有入栈和出栈两个方法,如果想入栈和出栈的元素类型统一,写法如下:

class Stack<T> { private data: T[] = [] push(item:T) { return this.data.push(item) } pop():T | undefined { return this.data.pop() } } 入栈和出栈是number类型 const s1 = new Stack<number>() s1.push(1) console.log(s1.pop()); // 1 泛型约束

假设现在有这么一个函数,打印传入参数的长度

function printLength<T>(arg: T): T { console.log(arg.length) // 因为不确定 T 是否有 length 属性,会报错 return arg } printLength([1,2,3])

结合interface,通过extends来实现泛型约束

interface ILength { length: number } function printLength<T extends ILength>(arg: T): T { console.log(arg.length) return arg } printLength([1,2,3]) 装饰器 定义

  • 装饰器是一种特殊类型的声明,它能够被附加到类声明、方法、属性或参数上,可以修改类的行为。
  • 通俗讲装饰器就是一个方法,可以注入到类、方法、属性参数上来扩展类、属性、方法、参数的功能
  • 常见的装饰器有:类装饰器、属性装饰器、方法装饰器、参数装饰器
  • 装饰器的写法:普通装饰器(不可传参)、装饰器工厂(可传参)
类装饰器:普通装饰器

function logClass(parmas:any){ parmas.prototype.apiUrl = "www.baidu.com" } @logClass class HttpClient { constructor(){ } getData(){ } } const www.baidu.com 类装饰器:装饰器工厂

function logClass(parmas:string){ return function(target:any){ console.log(parmas) console.log(target) } } @logClass('hello') class HttpClient { constructor(){ } getData(){ } }

console打印出来结果:

hello index.js:134 class HttpClient { constructor() { } getData() { } } 属性装饰器

接收2个参数,target当前类的原型对象,attr当前属性名称

function logClass(parmas:string){ return function(target:any){ console.log(parmas) console.log(target) } } function logProperty(params:string){ return function(target:any,attr:any){ console.log(target) // 当前类的原型对象 console.log(attr) // url // 修改属性值 target[attr] = params } } @logClass('hello') class HttpClient { @logProperty('api.xxx.com') public url: string | undefined constructor(){ } getData(){ console.log(this.url) // api.xxx.com } } const api.xxx.com') getData(...args:any[]){ console.log(args); // ['123', '122'] console.log('我是getData里面的方法') } } const localhost:3000/)

ts实战

我们使用react+ts实现一个todo List功能

功能包括:

  • 查看任务列表
  • 添加、删除任务
  • 完成任务
先看效果图

初始化一个react项目

create-react-app.dev/

  1. 初始化一个typescript项目

yarn create react-app react-ts-demo --template typescript

  1. 运行项目

cd react-ts-demo & yarn start

  1. 浏览器访问:localhost:3000/
代码实现
  1. 创建一个父组件todolist

export interface ITodoItem { taskName: string, status: TaskStatus } // React.FC表示react的一个函数组件,是React.FunctionComponent的简写形式 const TodoList:React.FC = ()=>{ const [list, setList] = useState<Array<ITodoItem>>([]) const [inputValue, setInputValue] = useState<string>("") const updateList = (list:ITodoItem[])=>{ setList(list) saveTaskList(list) } const handleAdd = ()=>{ if(!inputValue.trim()){ return alert("输入不能为空") } let [...newList] = list; newList.push({ taskName: inputValue, status: TaskStatus.PENDING }) updateList(newList) setInputValue("") } const handleStart = (index:number)=>{ let [...newList] = list; newList = newList.map((item,indx)=>{ if(indx === index){ return { ...item, status: TaskStatus.IN_PROGRESS } } return item }) updateList(newList) } const handleComplete = (index:number)=>{ let [...newList] = list; newList = newList.map((item,indx)=>{ if(indx === index){ return { ...item, status: TaskStatus.COMPLETED } } return item }) updateList(newList) } const handleDel = (index:number)=>{ let [...newList] = list; newList.splice(index,1) updateList(newList) } // 初始化列表数据 useEffect(() => { const list = getTaskList() setList(list) }, []) return ( <div className={styles.container}> <h1 className={styles.title}>TODO LIST</h1> <div className={styles.form}> <input className={styles.textInput} type="text" value={inputValue} placeholder="输入任务名" onChange={(e)=>setInputValue(e.target.value)}/> <button className={styles.addBtn} onClick={handleAdd}>添加任务</button> </div> <div className={styles.list}> {list.map((item,index)=> { return <TodoItem key={index} index={index} item={item} handleStart={handleStart} handleComplete={handleComplete} handleDel={handleDel}/> })} </div> </div> ) } export default TodoList;

  1. 创建一个子组件todoItem并在父组件中引入

interface IProps { index: number item: ITodoItem handleStart: (index: number) => void handleComplete: (index: number) => void handleDel: (index: number) => void } // 对于接收的组件参数,需要定义参数类型 const TodoItem:React.FC<IProps> = ({index,item,handleStart,handleComplete,handleDel})=>{ return ( <div className={styles.taskItem}> <div className={styles.name}>{item.taskName}</div> <div className={styles.statusText}> {item.status===TaskStatus.PENDING && <span className={styles.pending}>待开始</span>} {item.status===TaskStatus.IN_PROGRESS && <span className={styles.inProgress}>进行中</span>} {item.status===TaskStatus.COMPLETED && <span className={styles.completed}>已完成</span>} </div> <div className={styles.btns}> {item.status===TaskStatus.PENDING && ( <button className={styles.btn} onClick={()=>handleStart(index)}>开始</button> )} {item.status===TaskStatus.IN_PROGRESS && ( <button className={styles.btn} onClick={()=>handleComplete(index)}>完成</button> )} <button className={styles.btn} onClick={()=>handleDel(index)}>删除</button> </div> </div> ) } export default TodoItem

  1. 数据保存和获取

我们在第一次进入的时候获取初始化数据

const getTaskList = ()=> { const taskList = localStorage.getItem("taskList") if(taskList){ return JSON.parse(taskList) } return [] } // 初始化列表数据 useEffect(() => { const list = getTaskList() setList(list) }, [])

添加、完成以及删除的时候保存和更新数据

const saveTaskList = (list: ITodoItem[])=>{ localStorage.setItem('taskList',JSON.stringify(list)) } const updateList = (list:ITodoItem[])=>{ setList(list) saveTaskList(list) }

上面代码我已经上传到github,欢迎star