TypeScript

概念

是微软开发的开源编程语言,是JavaScript的超级,是大型开发应用的基石,提供了丰富的语法提示,在编写阶段能够检查错误

新语法特性:

  • as断言
  • class类关键字(面向对象的三大特性)

问题:什么是TypeScript?

JavaScript和类型系统的结合

问题:类型系统是什么?

  • 在开发过程中找错
  • 使用类型注解来分析代码
  • 仅存在于开发阶段
  • 不会提供性能优化

环境

环境设置

node环境下配置:

因为在Node环境下只能识别 JS 文件,所以 TS 文件需要编译为 JS 文件

  1. //安装TS编译器包 TS -> JS
  2. //TSC -> TypeScript compiler
  3. npm i -g typescript
  4. //TSC使用
  5. tsc index.ts
  6. //安装自动编译包
  7. //解决:多次执行index.js命令输入
  8. npm i -g ts-node@8.5.4
  9. //使用 先编译后执行
  10. ts-node index.ts

定义

类型注解

按照TypeScript形式进行定义,检测代码进行报错,根据错误信息更正错误

interface定义数据类型,确保内容是否符合定义的类型,否则文本编辑器下波浪线报错

  1. //给变量定义
  2. interface Todo{
  3. id: number;
  4. title: string;
  5. completed: boolean;
  6. }
  7. //声明时使用
  8. const todo = result.data as Todo;
  1. //报错提醒
  2. const ID = todo.ID;
  3. 属性“ID”在类型“Todo”上不存在。你是否指的是“id”?
  4. const fininshed = todo.finished;
  5. 类型“Todo”上不存在属性“finished”。ts

给函数定义参数类型,避免传参时顺序搞错时可以自动检测出

  1. //给函数定义参数类型
  2. const logTodo = (id: number, title: string, completed: boolean) => {
  3. console.log(`
  4. todo的id为: ${id},
  5. 标题为: ${title},
  6. 是否完成: ${completed}
  7. `);
  8. };
  9. //报错提醒
  10. logTodo(id, completed, title);
  11. 类型“boolean”的参数不能赋给类型“string”的参数。

问题:什么是类型?

一个方便描述一个具有相应的属性和方法的值的东西

问题:什么是值?

是用户能够赋值给变量的东西,在TypeScript中,不同的值有不同的类型

问题:如何描述一个值?

通过字符串/数值/布尔值/对象等类型去描述一个值,也可以通过接口interface去自定义一个新的类型去描述一个值

类型

问题:为什么要用类型?

  • 能够帮助TypeScript编译器分析代码
  • 能够帮助其他开发者理解整个代码库里存在的值是什么东西

类型指的是方便去描述一个具有相应属性和方法的值,每一个值都有相应的类型

TypeScript中,常见的基础类型有:

  • String:字符串类型
  • Number:数值类型
  • Boolean:布尔值类型
  • 特殊类型:Date/自定义类型(建立在接口的前提下)

类型分类:

  • 元类型(primitive types)

    • number
    • boolean
    • undefined
    • void
    • string
    • symbol
    • null
  • 对象类型(object types)

    • functions
    • arrays
    • classes
    • objects

问题:如何让TypeScript编译器认为不同的值对应着不同的类型?

通过对象类型去完成,而不能通过元类型去完成

问题:使用TypeScript有什么副作用?

无论我们是不是想要,它都会自动帮值设定一个类型,它借助类型检测代码是否存在输入问题

类型系统

TypeScript中,有两个重要的系统:

  • 类型注解系统
  • 类型推断系统

系统可以作用在不同的元素上,变量、函数、对象

问题:什么是类型注解?

开发者主动告诉TypeScript某个值的类型是什么

  1. //元类型
  2. let score: number = 50;
  3. let sports: string = 'basketball';
  4. let isHappy: boolean = true;
  5. let nothingMuch: null = null;
  6. let nothing: undefined = undefined;
  1. //对象类型
  2. //数组
  3. let balls: string[] = ['basketball', 'football', 'volleyball'];
  4. let someNums: number[] = [1, -5, 0];
  5. let truths: boolean[] = [true, true, false];
  6. //类
  7. class Car{};
  8. let car: Car = new Car();
  9. //对象
  10. let person: {name: string; age: number} = {name:'zhangsan', age:20};
  11. //函数
  12. //void: 返回值为空
  13. const logNumber: (num: number) => void = (num) => {};

问题:什么是类型推断?

TypeScript尝试去推断值的类型(被动)

关于变量声明和变量初始化:

  1. //变量声明
  2. let score;
  3. //变量初始化
  4. score = 50;

在创建变量的时候经历了两个过程,变量声明和变量初始化,当两个过程同时存在时(在同一行的时候),类型推断系统才会起作用

问题:什么时候使用类型注解?

  1. 当一个函数返回 any 类型,但是我们想要明确具体类型
  2. 当某一行声明变量了之后,在另一行进行初始化
  3. 当我们想要一个变量拥有一个不能推断出来的类型

手动添加类型注解的 3 种情况:

  1. //1.变量声明和变量初始化不在同一行
  2. let score;
  3. score = 50;
  4. //2.当一个函数返回any类型,但是我们想要明确具体类型
  5. const json = '{"name":“zhangsan", "age": 20}';
  6. //TS不会深层的推断JSON.parse返回的数据,推断为any
  7. let person = JSON.parse(json);
  8. //所以需要手动添加类型
  9. let person: {name: string, age: number} = JSON.parse(json);
  10. //3.当我们想要一个变量拥有一个不能推断出来的类型
  11. let numbers = [-1, 0, 8];
  12. //numAboveZero -> any
  13. //let numAboveZero;
  14. //联合类型
  15. let numAboveZero: boolean | number = false;
  16. for(let i = 0; i < numbers.length; i++){
  17. if(numbers[i] > 0){
  18. numAboveZero = numbers[i];
  19. }
  20. }

如何做函数类型注解?

  1. //TS只会检测类型,而不会检测逻辑
  2. const addNums = (a: number, b: number): 返回值number => {
  3. return a - b;
  4. };
  5. function multiply(a: number, b: number): number{
  6. return a / b;
  7. }
  8. const divide = function(a: number, b: number): number{
  9. return a / b;
  10. }

如何做抛出错误函数类型注解?

  1. //never 永远不结束
  2. const throwError = (message: string): never => {
  3. throw new Error(message);
  4. }

解构写法:

  1. //解构语法
  2. const todayWeather = {
  3. date: new Date(),
  4. weather: '晴天'
  5. }
  6. //没有加解构,但是做了类型注解
  7. const logWeather = (todayWeather: {date: Date, weather: string}): void => {
  8. console.log(todayWeather.date);
  9. console.log(todayWeather.weather);
  10. }
  11. logWeather(todayWeather);
  12. //解构写法:
  13. //加了解构,但是没做类型注解
  14. const ES6logWeather = ({date, weather}) => {
  15. console.log(date);
  16. console.log(weather);
  17. }
  18. //完整写法:
  19. const logWeather = ({date, weather}: {date: Date, weather: string}): void => {
  20. console.log(todayWeather.date);
  21. console.log(todayWeather.weather);
  22. }

如何做对象类型注解?

  1. const profile = {
  2. name: 'mike',
  3. age: 20,
  4. coords: {
  5. lat: 30,
  6. lng: 50
  7. },
  8. setAge(age: number): void{
  9. this.age = age;
  10. }
  11. }
  12. //对象写法:
  13. const { age }: { age: number } = profile;
  14. //对象嵌套写法:
  15. const { coords: {lat, lng} }: { coords: {lat: number; lng: number}} = profile;

类型化数组

TypeScript里,数组也可以像在JavaScript中使用push等方法来操作数组,但有个比较大的区别是,TS的数组是类型化的数组,在数组里放的元素都是同一种类型的,如果加入不同种类类型的元素,也需要写特殊的类型注解

  1. //默认类型推断为 string[]
  2. const basketballPlayers: string[] = ['kobe', 'james', 'pierce'];
  3. //如果数组是空数组时,需要加注解
  4. const basketballPlayers: string[] = [];
  1. //二维嵌套数组:
  2. //注解写法:
  3. const studentsByClass: string[][]
  4. const studentsByClass = [
  5. ['mike', 'tom'],
  6. ['lee'],
  7. ['jack', 'rose']
  8. ];
  9. //提取值的时候帮助推断
  10. //player: string
  11. const player = basketballPlayers[0];
  12. //使用数组方法的时候帮助推断
  13. //favPlayer: string
  14. const favPlayer = basketballPlayers.pop();
  15. //防止加入不一样类型的值
  16. //报错
  17. basketballPlayers.push(123);
  18. //使用map,forEach,reduce函数时提供帮助
  19. basketballPlayers.map((car: string): string => {
  20. return car;
  21. });
  22. //容纳不同类型
  23. //添加类型注解
  24. const importantDates: (string | Date)[] = [];
  25. const importantDates = [new Date(), '2020-10-01'];

问题:什么时候使用类型化数组?

一旦需要记录一些相似类型记录的数据结构

元组

和数组很类似的数据结构,每一个元素代表一个记录的不同属性

  1. //tuple 元组
  2. //格式为:
  3. //[string, boolean, number]
  4. const drink = {
  5. color: 'brown',
  6. carbonated: true,
  7. sugar: 35
  8. }
  9. //元组写法
  10. const pepsi: [string, boolean, number] = ['brown', true, 35];
  11. //类型别名 Type Alias
  12. type Drink = [string, boolean, number];
  13. const sprite: Drink = ['brown', true, 35];

问题:为什么要用元组?

在程序里面,使用元组的数据类型比较少,但特殊场景会用到,跟 CSV 文件打交道时会用到

问题:使用元组会有什么缺点?

会丢失很重要的信息

  1. //除了数值类型不能显示其他类型
  2. //难以阅读值的信息代表什么
  3. const farm: [number, number] = [6, 8];
  4. //侧向说明对象是一个更好的数据结构
  5. const farm = {
  6. chick: 6,
  7. duck: 8
  8. }

接口

创建一个新的类型,来描述一个对象的属性名和属性值, 对象的形状进行描述

TypeScript中,会大量的使用接口和类来实现代码的高度复用,对类的一部分行为的抽象

接口分为:

  • 函数类型接口
  • 类类型接口:

    • 对类的一部分行为的抽象
    • 实现接口中的属性和方法
  • 可索引类型接口
  • 继承接口
  1. const uncleMike = {
  2. name: 'Mike',
  3. age: 20,
  4. married: false
  5. }
  6. //普通写法:
  7. const printPerson = (person: {name: string; age: number; married: boolean}): void => {
  8. console.log(person.name);
  9. }
  10. //定义接口写法:
  11. interface Person {
  12. name: string;
  13. age: number;
  14. married: boolean,
  15. //方法简写
  16. summary(): string;
  17. }
  18. const printPerson = (person: Person): void => {
  19. console.log(person.name);
  20. }

TypeScript中代码复用的一般策略:

  • 定义接收接口,指定类型参数的函数
  • 存在对象/类去满足接口的必要条件

问题:interfacetype有什么区别?

  • 都可以用来定义接口,即定义对象或者函数的形状
  • 都可以实现继承,也可以相互继承
  • type可以实现类型别名

定义了一个对象的蓝图,描述了这个对象的属性和方法

TypeScript中,类具有双重特性:

  • 创建实例(值)
  • 代表类型(类型)

问题:什么时候使用类?

跟接口一样,在TypeScript中,大量使用接口,为了不同文件里面的类进行一个配合工作

描述方法

  1. //定义一个蓝图
  2. class Person {
  3. //方法
  4. protected scream(): void {
  5. console.log('ahhh');
  6. }
  7. }
  8. const person = new Person();
  9. person.scream();
  10. //继承 inheritance
  11. //Men可以做的, Person也可以做
  12. class Men extends Person {
  13. }
  14. const men = new Men();
  15. men.scream();

问题:什么叫修饰符?

一些用于封装的关键字public,private,protected,为了限制类里面不同属性和方法的访问

问题:为什么要使用修饰符?

限制访问防止开发者错误的调用方法导致程序的破坏

修饰符:

  • public:这个方法能够在任何地方被调用(自身,子类,实例)
  • private:这个方法只能在当前这个类的其他方法中被调用(自身)
  • protected:这个方法能够在当前这个类的其他方法或子类的其他方法中被调用(自身,子类)
  • implements:关键字,满足接口,告诉 TS 帮助检测类有没有正确的满足接口的实现,没有时会显示错误
  • abstract:关键字,将当前类定义为抽象类,无法创建实例,配合定义将来会用到的方法和接收的参数还有属性使用

描述属性

  1. class Person {
  2. //属性
  3. name: string = 'Mike';
  4. constructor(public name: string){
  5. this.name = name;
  6. }
  7. }
  8. const person = new Person('Tom');
  9. console.log(person.name);

泛型

定义一个类型变量/泛型变量,这个变量暂时无法知道类型,等待执行时传入参数才知道类型是什么
写法:

  1. //1. 函数泛型的注解方式
  2. let a:<T>(arg: T) => T = identify;
  3. //2. 对象字面量的方式来定义泛型类型
  4. let a: { <T>(arg: T): T } = identify;
  5. //3. 泛型接口的定义方式
  6. let a: IdentifyInterface = identify;

问题:泛型类型是什么?

只需要多一个泛型,它的类型都可以称为泛型类型

配置

利用parcel-bundler包简易的让TypeScript代码运行在浏览器中

parcel-bundler包工作原理:将打包后的 ts 文件转为 js 文件注入到<script>标签中

  1. //安装
  2. npm i -g parcel-bundler
  3. //index.html引入ts文件
  4. <script src="./src/index.ts"></script>
  5. //开启打包服务器
  6. parcel serve index.html
  7. //Server running at http://localhost:1234

案例

百度地图

案例:地图应用

实现:

  • 百度地图展示在页面
  • 在地图上标识地点(用户/公司)
  • 给标注地点添加信息窗口

技术:TypeScript类 + faker

关于:

faker包:随机生成数据供浏览器或 node 使用

  1. //安装faker
  2. npm i -D faker@4.1.0
  3. //引入
  4. import faker from 'faker';
  5. //在类型定义文件中定义帮助TS项目认识faker包
  6. //JS库(包) -> 类型定义文件 -> TS代码
  7. //如流行的JS库如axios自带类型定义文件不用额外去下载
  8. //如果不下载类型定义文件,TS推断会提示需要下载
  9. //去npm下载faker的类型定义文件 搜索@types/faker
  10. npm i -D @types/faker

问题:什么是类型定义文件?

通过类型定义文件检测 JS 库中是否含有符合 TS 程序所需要的包文件

类型定义文件存储库

Definitely Typed

保存方式为:@types/{JS库的名字}, 如@types/faker

常见的 TS 文件结构:

  1. 为了配合类工作的接口定义
  2. 具体类的定义

百度地图开发平台 JavaScript 开发文档 API:

https://lbsyun.baidu.com/index.php?title=jspopularGL/guide/helloworld

  1. //1.引入标签
  2. <script type="text/javascript" src="https://api.map.baidu.com/api?v=1.0&type=webgl&ak=bdmzm8j1GXfW44ABN4D5LRGwXvV1hOOa"></script>
  3. //2.插入容器
  4. <div id="container"></div>
  5. //3.测试是否接入百度API,控制台输入BMapGL
  6. //打印:{version: 'gl', _register: Array(9), guid: 1, register: ƒ, getGUID: ƒ, …}
  7. //4.接入成功
  8. //5.在Map.ts文件中下载引入类型定义文件
  9. npm i -S @types/baidumap-web-sdk
  10. npm install --save @types/bmapgl
  11. //6.创建地图实例
  12. //实例化访问BMap底下的Map类
  13. //Map类接收两个参数:
  14. //参数1:HTMLElement挂载容器
  15. //参数2:配置对象
  16. const map = new BMapGL.Map('container', {
  17. //最大放大
  18. maxZoom: 1
  19. });
  20. //7.设置中心点坐标
  21. var point = new BMapGL.Point(116.404, 39.915);
  22. //地图初始化,同时设置地图展示级别
  23. map.centerAndZoom(point, 15);

定义公共类管理复用性高的参数类:

TypeScript - 图1

  1. //项目结构:
  2. ├─index.html
  3. ├─package.json
  4. ├─src
  5. | ├─Company.ts - 定义公司类/使用公共接口
  6. | ├─CustomMap.ts - 定义公共接口/地图类
  7. | ├─index.ts - 出口文件
  8. | Users.ts - 定义用户类/使用公共接口

项目总结:

  1. 实现private关键字在index.ts中使用 API 的限制
  2. 实现将两个方法或多个方法合并成一个方法,将新增属性或方法归纳在(配合类工作)定义的接口里
  3. 实现主动检测提示类中是否含有接口里定义的属性和方法

源码地址:https://gitee.com/kevinleeeee/ts-map-class-demo

案例:排序应用

技术:TS 编译器

实现排序:

  • 数组[9, 4, 20, -5] -> [-5, 4, 9, 20]
  • 字符串'OedJA -> AdeJO'
  • 链表(9 -> -5 -> 30 -> 2) -> (-5 -> 2 -> 9 -> 30)

冒泡排序算法:

  1. 两个嵌套的for循环
  2. 将会多次遍历我们的数据集合
  3. 每次遍历都会比较数据集合中的一对元素
  4. 如数组中[0, 8, -3, 5],元素0 > 8 ?,如果大于交换位置
  1. //项目结构:
  2. ├─package.json
  3. ├─Readme.md
  4. ├─tsconfig.json - TS项目管理文件
  5. ├─src
  6. | ├─CharacterCollection.ts - 字符串类/逻辑/继承父类Sort
  7. | ├─index.ts - 入口文件
  8. | ├─LinkedList.ts - 链表类/逻辑/继承父类Sort
  9. | ├─NumbersCollection.ts - 数值数组类/逻辑/继承父类Sort
  10. | Sorter.ts - 父类/接口定义/排序器逻辑/进行抽象属性和方法

TS 项目搭建:

TS 编译器项目安装:

  1. //安装
  2. tsc --init
  3. //命令输入后会创建一个tsconfig.json文件

配置tsconfig.json文件:

  1. //配置入口地址和输出目录
  2. "outDir": "./build",
  3. "rootDir": "./src",

设置完毕后测试:

  1. //终端输入命令
  2. tsc
  3. //build目录生成ts文件打包后的js文件

实时监控index.ts文件修改变化的命令:

  1. //终端输入命令
  2. tsc -w
  3. //此时终端监听文件变化
  4. Starting compilation in watch mode...

如何运行编译后的结果?

  1. //终端输入命令
  2. node ./build/index.js
  3. //输出打印内容

那么如何自动运行编译后的结果?

  1. //安装nodemon 检测index.js变化
  2. //安装concurrently 可以同时运行两个脚本
  3. //在package.json文件中修改运行脚本
  4. //编译命令:把ts文件编译到js文件
  5. "start:build": "tsc -w"
  6. //执行编译后的结果
  7. "start:run": "nodemon build/index.js"
  8. //将两个脚本命令同时运行起来
  9. //检测所有npm脚本,以start开头:xxx的脚本,在执行npm start时依次执行
  10. "start": "concurrently npm:start:*"
  11. //自动化过程命令:编译和运行
  12. npm start

知识点 1:

  1. //简写初始化类的属性
  2. //简写前:
  3. class Sorter {
  4. collection: number[];
  5. constructor(collection: number[]) {
  6. //初始化
  7. this.collection = collection;
  8. }
  9. }
  10. //简写后:
  11. class Sorter {
  12. constructor(public collection: number[]) {
  13. ...
  14. }
  15. }

知识点 2:

联合类型会有弊端:

  • 在不同类中,保留相同的属性,不同的属性会自动删除导致访问不到,TS 推断报错
  • 如数组类型和字符串类型的联合类型上,只有保留数组和字符都有的原生方法,其他原生方法均被删除,而且连数组索引访问的特性都被删除

如何解决弊端?

可以利用类型保护的机制

  1. //判断该类型是否是数组构造出来的
  2. //如果是数组构造出来的证明是数组,TS就会推断为数组,从而不会删除属性方法和特性
  3. if(this.collection instanceof Array){
  4. //...this.collection 不会报提示错误
  5. }
  6. //判断数据集合类型是字符串的话
  7. if (typeof this.collection === 'string') {
  8. //...
  9. }

问题:什么时候用typeof? 什么时候用instanceof?

  • 原始值像string/boolean/number/symbol类型用typeof
  • 引用值用intanceof

双层for循环调试执行顺序:

  1. sort(): void {
  2. const {
  3. length
  4. } = this.collection;
  5. //外层遍历所有元素
  6. for (let i = 0; i < length; i++) {
  7. //内层遍历除了最后一个元素的所有元素
  8. //因为最后的元素已经排列到正确的位置了
  9. //为什么 - i? 因为遍历次数就为 length - i - 1
  10. for (let j = 0; j < length - i - 1; j++) {
  11. //前面一项 > 后面一项时
  12. if (this.collection[j] > this.collection[j + 1]) {
  13. //左手边的元素(前面一项)
  14. const leftHand = this.collection[j];
  15. //交换位置
  16. //修改前面一项的值
  17. this.collection[j] = this.collection[j + 1];
  18. //修改后面一项的值
  19. this.collection[j + 1] = leftHand;
  20. }
  21. }
  22. }
  23. }
  1. //数组[10, 5, -8, 0] -> [ -8, 0, 5, 10 ]
  2. <调试> 交换位置之前为[10,5,-8,0]
  3. <调试> 外层第1次循环:i数值为0
  4. <调试> 需要调换3次位置
  5. <调试> 内层第1次循环(j < length-i-1 = 3): j数值为0, 交换位置之后为[5,10,-8,0]
  6. <调试> 内层第2次循环(j < length-i-1 = 3): j数值为1, 交换位置之后为[5,-8,10,0]
  7. <调试> 内层第3次循环(j < length-i-1 = 3): j数值为2, 交换位置之后为[5,-8,0,10]
  8. <调试> 外层第2次循环:i数值为1
  9. <调试> 需要调换2次位置
  10. <调试> 内层第1次循环(j < length-i-1 = 2): j数值为0, 交换位置之后为[-8,5,0,10]
  11. <调试> 内层第2次循环(j < length-i-1 = 2): j数值为1, 交换位置之后为[-8,0,5,10]
  12. <调试> 外层第3次循环:i数值为2
  13. <调试> 需要调换1次位置
  14. <调试> 内层第1次循环(j < length-i-1 = 1): j数值为0, 交换位置之后为[-8,0,5,10]
  15. <调试> 外层第4次循环:i数值为3

注意点:

  1. //数组和字符串修改的区别
  2. var arr = [1, 2, 3];
  3. arr[0] = 10;
  4. console.log(arr);
  5. //[10, 2, 3]
  6. var str = 'red';
  7. str[0] = 'Z';
  8. console.log(str);
  9. //red
  10. //这里字符串是不可变的

链表结构图:

TypeScript - 图2

  1. Node { data: -50, next: Node { data: 0, next: Node { data: 100, next: [Node] } } }
  2. Node { data: 0, next: Node { data: 100, next: Node { data: 300, next: null } } }
  3. Node { data: 100, next: Node { data: 300, next: null } }
  4. Node { data: 300, next: null }

接口和抽象类的对比:

  • interface

    • 在不同的类之间建立合同
    • 几种不同的对象之间要配合的时候
    • 松耦合
  • inheritance/abstract classes

    • 在不同的类之间建立合同
    • 存在相同类型的对象时
    • 强耦合

源码地址:https://gitee.com/kevinleeeee/ts-sort-app-demo

购物车

案例:VueJS + TS 制作购物车实战

技术:

  • vue类 + ts+ 预编译器 scss

实现购物车功能:

  • 单选或全选商品
  • 单项商品数量和价格的联动
  • 单选商品点击删除
  • 多选商品总价展示
  • 结算按钮跳转至配送地址页面
  • 点击相应地址卡片会样式高亮显示
  • 点击默认地址以外卡片的地址会有设为默认选项
  • 地址卡片都有删除按钮
  • 查看更多地址按钮展示全部地址

项目写法:

  • 类管理 vue组件(vue-property-decorator包实现)

问题:vue如何跟 TS结合实现数据类型检测?

编程步骤:

  1. Vue
  2. Class定义组件的方式
  3. 结合 TS

利用 vue脚手架工具创建 ts项目:

  1. //1.用vueCli创建项目名称
  2. vue create vue-ts-shopping-card-project
  3. //2.选择Babel和TypeScript和Router和CSS Pre-processors
  4. //3.是否选择类样式组件语法
  5. Use class-style component syntax? (Y/n) Yes
  6. //4.是否选择Babel
  7. Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes
  8. //5.history mode? Yes
  9. //6.配置文件 In dedicated config files
  10. //7.配置完成创建项目

问题:在 vue项目中,有什么 ts的特性?

  1. //1.main.js -> main.ts
  2. //2.<script lang="ts"></script>
  3. //3.引入vue-property-decorator装饰器包
  4. import {Vue, Component} from 'vue-property-decorator';
  5. //4.装饰器
  6. @Component(参数: 组件对象)
  7. //5.导出对象的写法:
  8. //旧: exported default{name:'xxxx',data(){},... }
  9. //新: exported default class App extends Vue{}

问题:装饰器有什么作用?

vue文档:

  • @Component修饰符注明了此类为一个 Vue组件
  • @Component(参数: 组件对象的所有选项)

告诉整个 vue程序,所添加装饰器的类不是一个普通的类,而是一个组件,不写时,vue并不能识别当前类为一个组件

vue项目中如何使用装饰器?

  1. //安装依赖
  2. npm i -S vue-property-decorator@8.3.0
  3. //组件中添加@Component装饰器
  4. @Component
  5. class App extends Vue{...}

问题:如何在 vue中添加接口?

  1. //定义一个接口文件types.ts
  2. export interface ICat {
  3. ...
  4. }
  5. //在数据文件cats.ts中导入接口
  6. import {ICat} from './types';
  7. export const cats: ICat[] = [...];
  8. //在vue组件中引入接口
  9. //在类内部得属性或方法中补充接口
  10. //如: get cat(): ICat{...}

问题:methods里的逻辑方法在哪里定义?

  1. //在组件的类里面定义
  2. export default class CartBoard extends Vue {
  3. //业务逻辑方法, 如点击函数等
  4. }

问题:过滤器在哪里定义和使用?

  1. //在组件文件中的装饰器函数里新增filters过滤器对象
  2. @Component({
  3. name: "CartBoard",
  4. //过滤器
  5. filters: {
  6. currency: function (value: number) {
  7. if (!value) {
  8. return "0.00";
  9. }
  10. value = value * 1;
  11. //保留2位
  12. return "¥" + value.toFixed(2);
  13. },
  14. },
  15. })
  16. //视图模板使用
  17. <div class="cart-item-total">{{ 123 | currency }}</div>

问题:计算属性在哪里定义和使用?

  1. //在组件的类里面定义
  2. export default class CartBoard extends Vue {
  3. //get关键字 + 计算函数
  4. get totalPrice(){...}
  5. }
  6. //视图模板使用
  7. 总价: <span class="price-num">{{ totalPrice }}</span>

项目总结:

TS可以带来一些好处,单词写错时会自动提醒报错,自动检测数据类型

源码地址:https://gitee.com/kevinleeeee/vue-ts-shopping-card-project

新闻头条

案例:新闻头条

这是一个移动版新闻头条

备注:该案例后端有保存新闻头条mock数据

技术:

  • vue3
  • TypeScript
  • EggJS
  • vuex模块写法
  • 聚合数据API

搭建:

问题:如何启动Eggjs脚手架?

  1. 1. 全局安装
  2. npm install create-egg@latest -g
  3. 2. 创建后端项目脚手架
  4. npm init egg --type=ts
  5. 3.安装依赖
  6. npm i
  7. 4.添加允许跨域
  8. npm i egg-cors@2.2.3 -S
  9. 5.配置 ./api/config/plugin.ts
  10. //启动跨域
  11. const plugin: EggPlugin = {
  12. //启动跨域和引入包
  13. cors: {
  14. enable: true,
  15. package: 'egg-cors'
  16. }
  17. };
  18. 6.配置 ./api/config/plugin.ts
  19. //设置跨域规则
  20. config.cors = {
  21. origin: () => '*',
  22. allowMethods: 'GET,POST,PUT,DELETE,HEAD,PATCH',
  23. credentials: true
  24. };
  25. //CSRF攻击
  26. config.security = {
  27. csrf: {
  28. enable: false
  29. }
  30. };
  31. //用户和API配置
  32. config.userConfig = {
  33. //聚合新闻头条数据请求KEY
  34. APP_KEY: '5f2a6f48f30c9ba3ec8df73793458b0f',
  35. API: {
  36. GET_NEWS_LIST: 'http://v.juhe.cn/toutiao/index'
  37. }
  38. };

请求接口:

  • 根据新闻类型和页码和每页条数来请求新闻列表数据:

    • 请求地址:http://127.0.0.1:7001/api/news_list
    • 请求方式:POST
    • 参数 1:type=junshi
    • 参数 2:pageNum=1
    • 参数 3:count=10

Egg后端框架返回数据的流程:

  • router(挂载接口):管理请求路径时决定使用控制器controller的哪个api
  • controller(外界访问接口):管理等待异步请求(service)返回的数据作为响应数据传给前端
  • service:一个类里面定义的一些异步方法,一般对接数据库操作,或数据请求

vue3环境搭建:

  1. 1. 脚手架创建应用
  2. vue create news-mobile
  3. 2. 选择 Babel + TypeScript + Router + Vuex + CSS pre-processors
  4. 3. 选择3.0版本
  5. 4. 创建完成
  6. npm run serve

项目启动:

  1. //后端
  2. npm run dev
  3. //前端
  4. npm run serve

后端目录:

  1. ├─api
  2. | ├─.autod.conf.js
  3. | ├─.eslintignore
  4. | ├─.eslintrc
  5. | ├─.gitignore
  6. | ├─.travis.yml
  7. | ├─appveyor.yml
  8. | ├─package-lock.json
  9. | ├─package.json
  10. | ├─tsconfig.json
  11. | ├─typings
  12. | | ├─index.d.ts - 定义接口类型的文件
  13. | ├─mock - 请求API接口后缓存的模拟数据
  14. | | ├─newsData
  15. | ├─config - egg框架配置文件
  16. | | ├─config.default.ts - 聚合数据Key/API baseUrl/跨域
  17. | | ├─config.local.ts
  18. | | ├─config.prod.ts
  19. | | plugin.ts - 插件配置/启动跨域插件
  20. | ├─app
  21. | | ├─router.ts - 路由地址分配controller控制器
  22. | | ├─service - 接口服务
  23. | | | api.ts - 请求聚合数据
  24. | | ├─public
  25. | | ├─lib - 工具
  26. | | | utils.ts - typeof封装/路径参数格式化/分割页码列表数据
  27. | | ├─extend
  28. | | | context.ts
  29. | | ├─controller - 控制器/对请求service的方法得到的数据响应给前端
  30. | | | api.ts

前端目录:

  1. ├─news-mobile
  2. | ├─babel.config.js
  3. | ├─package-lock.json
  4. | ├─package.json
  5. | ├─tsconfig.json
  6. | ├─src
  7. | | ├─App.vue - 应用根组件
  8. | | ├─main.ts - 入口文件/导入静态资源文件
  9. | | ├─shims-vue.d.ts
  10. | | ├─views - 页面组件/列表数据请求
  11. | | | ├─Detail.vue - url绑定iframe视图
  12. | | | ├─Home.vue - 更改新闻类型并重新请求新闻列表数据/监听加载骨架屏
  13. | | | MyNews.vue - 收藏列表加载
  14. | | ├─typings - 定义和管理接口类型的文件
  15. | | | ├─common.ts
  16. | | | ├─home.ts
  17. | | | ├─index.ts - 统一管理
  18. | | | store.ts
  19. | | ├─store - vuex状态管理 module模块的写法
  20. | | | ├─index.ts - 统一管理合并模块
  21. | | | ├─home - home状态
  22. | | | | ├─actions.ts - 定义actions(负责commit)
  23. | | | | ├─actionTypes.ts - store里的方法名称变量文件
  24. | | | | ├─index.ts - 出口文件
  25. | | | | ├─mutations.ts - 接收actions命令操作更改state数据
  26. | | | | state.ts - 初始化数据
  27. | | | ├─detail - detail状态
  28. | | | | ├─actions.ts
  29. | | | | ├─actionTypes.ts
  30. | | | | ├─index.ts
  31. | | | | ├─mutations.ts
  32. | | | | state.ts
  33. | | ├─services - 前端请求API封装
  34. | | | index.ts - 请求新闻列表
  35. | | ├─router - 前端路由对象定义
  36. | | | index.ts - 页面header信息/路由规则
  37. | | ├─lib
  38. | | | http.ts - 拦截器写法封装axios
  39. | | ├─directives - 自定义指令
  40. | | | ├─index.ts
  41. | | | navCurrent.ts - v-nav-current对视图绑定更新样式
  42. | | ├─data
  43. | | | nav.ts - navbar静态数据
  44. | | ├─compositions - 自定义hook
  45. | | | ├─common.ts - 公共hook/获取header对应的路由信息/图片淡入/触底加载更多
  46. | | | ├─detail.ts - 获取详情页当前新闻信息/切换收藏/检查收藏状态
  47. | | | ├─home.ts - 获取首页新闻列表/更改新闻类型并重新请求新闻列表数据
  48. | | | ├─index.ts - 统一管理hook
  49. | | | mynews.ts - localStorage中获取收藏的新闻信息
  50. | | ├─components - 组件
  51. | | | ├─Skeleton - 静态骨架屏
  52. | | | | index.vue
  53. | | | ├─NoMore
  54. | | | | index.vue
  55. | | | ├─NoData
  56. | | | | index.vue
  57. | | | ├─NewsList - 新闻列表模板/根据图片数量找对于模板绑定显示/列表触底加载
  58. | | | | ├─index.vue
  59. | | | | ├─Item0.vue
  60. | | | | ├─Item1.vue
  61. | | | | ├─Item2.vue
  62. | | | | Item3.vue
  63. | | | ├─NavBar - 点击nav-item修改state数据/切换
  64. | | | | ├─index.vue
  65. | | | | NavItem.vue
  66. | | | ├─Loading
  67. | | | | index.vue
  68. | | | ├─Header - 路由监听/路由跳转/点击切换收藏
  69. | | | | index.vue
  70. | | ├─assets - 静态资源文件目录
  71. | | | ├─js
  72. | | | | ├─common.js
  73. | | | | fastclick.js
  74. | | | ├─css
  75. | | | | ├─border.css
  76. | | | | ├─common.css
  77. | | | | ├─iconfont.css
  78. | | | | resets.css
  79. | ├─public
  80. | | ├─favicon.ico
  81. | | index.html

项目总结:

  1. 后端需要熟悉MVC的开发模式
  2. 前端

    1. 组件化
    2. vue3中对业务逻辑方法提取到compositionAPI
    3. vue3自定义指令写法
    4. 对请求axios进行封装时用了拦截器的写法
    5. service接口目录做了请求方法的封装
    6. vue3中的vuex里运用了模块的方式编写
  3. TypeScript

    1. typings目录文件的管理和接口(interface,enum写法)的定义
    2. 对所有的变量和方法都进行了类型注解
    3. 类型注解对于日后维护带来便利

项目地址: https://gitee.com/kevinleeeee/vue3-typescript-egg-news-mobile-demo

驾照考题

案例:驾照考题

这是一个移动端的前后端集成的项目

技术:

  • 前端:React Hooks + Redux + TypeScript
  • 后端:EggJs + TypeScript

接口:

redux类型定义过程:

  1. actionType定义变量类型
  2. actions使用类型定义多个action对象{type, payload}
  3. reduces里根据action.type去进行更改相应的数据

项目目录:

  1. ├─tsconfig.json
  2. ├─src
  3. | ├─App.tsx - 路由分支视图
  4. | ├─index.tsx - 入口文件/资源引入/路由引入/仓库引入
  5. | ├─views
  6. | | ├─Home.tsx - 首页/科目选择/驾照类型选择/开始考试按钮
  7. | | ├─Result.tsx - 答题结果页面
  8. | | Test.tsx - 考试中页面/保存用户答题信息并显示下一题信息
  9. | ├─typings - 接口和枚举定义文件
  10. | | index.ts
  11. | ├─store - 仓库
  12. | | ├─actions.ts - 定义action方法并返回action对象
  13. | | ├─actionTypes.ts - 变量名管理
  14. | | ├─index.ts - 出口文件
  15. | | ├─reducers.ts - 根据action对象的类型进行state数据更改
  16. | | state.ts - 数据容器
  17. | ├─services
  18. | | index.ts - 客户端请求函数封装
  19. | ├─lib
  20. | | ├─http.ts - axios函数封装/拦截器方式封装
  21. | | utils.ts - 工具/生成一个组装后的userAnswer对象
  22. | ├─hooks - 自定义hooks
  23. | | index.ts - 做题答题数据过程的hook
  24. | ├─data - 静态数据
  25. | | index.ts - 科目/驾照类型
  26. | ├─components
  27. | | ├─TestPanel - 考试中组件
  28. | | | ├─index.tsx
  29. | | | ├─Question.tsx
  30. | | | ├─Selector.tsx
  31. | | | ├─styles
  32. | | | | ├─index.scss
  33. | | | | ├─question.scss
  34. | | | | selector.scss
  35. | | ├─Subject - 科目选择按钮组件
  36. | | | ├─index.scss
  37. | | | index.tsx
  38. | | ├─ResultPanel - 考试结果组件
  39. | | | ├─index.scss
  40. | | | index.tsx
  41. | | ├─Model - 驾照类型选择按钮组件
  42. | | | ├─index.scss
  43. | | | index.tsx
  44. | | ├─Header - 标题组件
  45. | | | ├─index.scss
  46. | | | index.tsx
  47. | | ├─Button - 按钮组件(可复用)
  48. | | | ├─index.scss
  49. | | | index.tsx
  50. | ├─assets - 静态资源文件
  51. | | ├─js
  52. | | | ├─common.js
  53. | | | fastclick.js
  54. | | ├─css
  55. | | | ├─border.css
  56. | | | ├─common.css
  57. | | | ├─iconfont.css
  58. | | | resets.css
  59. ├─public

源码地址: https://gitee.com/kevinleeeee/reacthook-typescript-egg-license-mobile-demo

页面切换

案例:页面切换

面向对象插件化开发一个·nav切换page页面

技术:

  • vite + ts + 订阅发布模式

功能:

  • 直接切换fade
  • 滚动切换slide

类划分:

  • 功能类:

    • Fade:自身setPage方法的实现fade切换,实例化时执行收集方法传入setPage方法
    • Slide :自身setPage方法的实现slide切换,实例化时执行收集方法传入setPage方法
  • 抽象类:

    • Base 管理公共的功能
    • 定义容器装载函数方法
    • 订阅模式:收集方法protected getMethod,将继承类的方法放入容器
    • 发布通知模式:protected notify,该方法在点击切换时触发,遍历容器里所有的方法并执行同时传参

源码地址: https://gitee.com/kevinleeeee/ts-plugin-tablist-demo

todolist

案例:todolist

在开发外观模式下做一个待办事项的案例

技术:

  • ts + vite + 外观模式

组件划分:

  • TodoList组件:Input + List,实例化子组件并传入节点容器和配置信息
  • TodoList组件是一个中介,集中管理InputList的功能,组装子组件最后挂载到todoList容器
  • TodoList组件最终交给index视图(子组件实例创建,渲染,事件绑定)

关于外观模式:

外观模式的基本用法,入口模块直接接收一个TodoList外观模块接口,去实现相应的功能,而不会去对子组件产生任何的依赖

  1. 组件化 -> 组件结构 -> 外观模式

项目目录:

  1. ├─index.html
  2. ├─package.json
  3. ├─src
  4. | ├─app.ts - 实例化后初始化todolist插件/根节点和静态数据传入实例
  5. | ├─typings
  6. | | index.ts - 接口
  7. | ├─components
  8. | | ├─TodoList - 根组件/管理子组件的渲染和事件绑定/共享初始化函数
  9. | | | ├─index.ts - 自身实现实例化创建子组件
  10. | | | ├─Subs
  11. | | | | ├─Component.ts - 抽象类/管理组件相关的公共方法/视图模板方法
  12. | | | | ├─Input.ts - 输入框组件类
  13. | | | | List.ts - 列表组件类

源码地址: https://gitee.com/kevinleeeee/ts-todolist-appearance-demo

案例:todolist

在开发程序设计模式(横向切割)下做一个待办事项的案例

希望将dom的问题交由后续处理,外层专门来操作数据,数据操作进而变更dom,由类的继承时驱动其他方法的加载,数据的处理和dom的处理是完全分开的

加入后端设计模式的应用,发挥最大的优势

技术:

  • ts + vite + 程序设计 + 后端 + 装饰器

传统写法:

绑定事件处理函数,数据的更改:

  • 增加项,列表数据增加一项{id,content,completed},每一项的视图增加到列表中
  • 删除项,根据id去删除一项,将对应项的视图从列表中删除
  • 改变完成状态,根据id去改变,将对应项的完成状态更改成是否完成

接口:

  • 列表请求数据(get):/todolist
  • 切换是否完成的状态(post):/toggle
  • 移除某一项(post):/remove
  • 新增某一项(post):/add

问题:如何不通过数据响应式,虚拟节点等底层数据绑定机制方法去实现程序设计方案?

  1. 面向对象的方式
  2. 需要类的继承方式
  3. 需要横向切割程序方式

问题:如何设计?

将程序进行分类:

  1. app.ts外层:浏览器的事件去调用方法,事件处理函数的绑定
  2. TodoEvent.ts操作数据(子类继承DOM类):抛出公共实例方法addTodoremoveTodo,toggleComplete,以便于数据的专门维护
  3. TodoDom.ts操作dom(父类同时继承模板类):抛出公共实例方法addItem,removeItem,changeCompleted,以便于dom的专门维护
  4. TodoTemplate.ts管理模板(祖先类):todoView,模板可以接收参数,以便于模板的专门维护

问题:为什么要使用装饰器?

es6写法的类里可以定义一个装饰器用来做副作用的程序加载,实现一个简单句柄的写法多次复用该函数里的副作用程序(如数据请求)

问题:如何使用装饰器?

  1. 1. 在使用装饰器的方法上面加入 句柄 @getTodoList
  2. 2. 定义一个装饰器函数getTodoList
  3. 参数1:使用该函数装饰器的类(包含类里面的所有方法不管共有还是私有)
  4. 参数2:使用该函数装饰器的方法名称 _init
  5. 参数3:是一个修饰符的配置对象
  6. 3. 保存原有descriptor.value里的函数
  7. 4. 重写descriptor.value里的函数,等待数据请求成功后去执行原有的函数

使用的装饰器:

  • @getTodoList:该装饰器添加请求列表数据
  • @removeTodo:该装饰器添加请求删除某一项数据
  • @toggleTodo:该装饰器添加请求修改某一项数据
  • @addTodo:该装饰器添加请求新增列表数据

项目目录:

  1. ├─src
  2. | ├─app.ts - 节点获取/静态数据/实例化数据类/事件处理函数绑定
  3. | ├─typings.ts
  4. | | index.ts
  5. | ├─js
  6. | | ├─TodoDom.ts - 专门操作处理dom的类
  7. | | ├─TodoEvent.ts - 专门操作数据的类/装饰器使用
  8. | | ├─TodoService.ts - 专门管理和定义装饰器的函数
  9. | | ├─TodoTemplate.ts - 专门管理模板的类
  10. | | utils.ts - 找到父节点/创建一项节点方法

继承关系:

项目总结:

这种设计适用于很多场景,原生或底层项目时,横向切割程序进行分类,有助于单独对某程序进行维护和更新

源码地址: https://gitee.com/kevinleeeee/ts-programming-todolist-demo

案例:todolist

react-hookuseReducer方式做一个待办事项的案例

技术:

  • ts + react脚手架 + hooks

问题:useReducer有什么存在意义?

它比useState的写法更为高级,也是一个更高级的解决方案,当使用多个方法操作一个状态数据的变化时,而且这些方法内部有相对比较复杂的逻辑时,可以用useReducer来处理

并且,使用useReducer还能给那些触发更新的组件做性能优化,因为开发者项子组件传递的是dispatch而不是回调函数

项目目录:

  1. ├─tsconfig.json
  2. ├─src
  3. | ├─App.tsx
  4. | ├─index.tsx
  5. | ├─typings
  6. | | index.ts
  7. | ├─components
  8. | | ├─TodoList
  9. | | | ├─index.tsx - todoList组件/定义useEffect/定义dispatch操作某项todo的方法并传子组件
  10. | | | ├─reducer.ts - 定义根据action对象的类型操作的具体方法
  11. | | | ├─List
  12. | | | | ├─index.tsx
  13. | | | | Item.tsx
  14. | | | ├─Input
  15. | | | | index.tsx
  16. ├─public

源码地址: https://gitee.com/kevinleeeee/ts-react-hooks-todolist

插件开发

案例:插件开发

面向对象的方式去开发一个插件库

技术:vite + ts + 面向对象 + 原生

插件:

  • MyAlert:弹出一个警告模态框

继承关系:

源码地址: https://gitee.com/kevinleeeee/ts-myplugin-demo

购物车

案例:购物车

移动版的购物车

技术:

TS + ReactHook + Express + redux

功能:

  • 拦截器封装axios
  • 商品列表
  • 商品详情颜色,版本,数量选择
  • 计算当前页面金额
  • 加入购物车

问题:减少http请求的使用分页方法的前后端选择?

后端分页的机制是前端发起请求,携带页码参数,后端接收请求和请求参数,根据页码从数据库获取相应页码数据,最后携带数据响应给客户端,前端拿到数据进行视图渲染

关于前端分页的必要性:

  • 对数据量较大时渲染少数的数据在一页中展示,如一些管理系统的管理员账号信息key
  • 假如开发业务需求的页面仅需要30条数据,而后端返回90条数据的情况时也需要做前端分页

后端接口:

  • 请求列表:/phones

问题:为什么在reduxvuex中加入action这个过程?

一般在编程过程中,事件触发一个行为,行为去响应一个函数,且action是可以用进行更多的副作用操作,但不推荐组件直接操作mutationreducer,因为它们是纯函数,且害怕开发者容易在纯函数里写副作用,所以在中间加了一层action,让action去调用reducermutation

redux执行过程:

  1. component -> store.dispatch -> action -> ctx.commit -> reducer methods -> state

redux编写顺序:

  1. state仓库初始化和定义类型export default { phoneList: []} as IState;
  2. actionTypes定义变量和该变量的类型并导出
  3. actions定义函数,该函数返回一个action对象(需定义类型)
  4. reducers定义函数,定义state数据默认值,根据action对象的type更改数据,返回一个新的state对象覆盖原有state数据
  5. index出口文件定义store方法

关于useEffectvue3watchEffect函数:

  • useEffect依赖外部数据,依赖数据一旦更改就会重新执行该函数内部的程序
  • watchEffect也是依赖外部数据,但内部本身实现了依赖自动收集,不需要手动写入,依赖数据一旦更改就会重新执行该函数内部的程序

问题:其他页面组件如何获取路由参数信息?

react项目中,通过react-router里的useParams方法去获取

  1. //引入
  2. import { useParams } from 'react-router';
  3. //参数接口定义
  4. interface IUrlParams{
  5. id: string | undefined;
  6. cid: string | undefined;
  7. vid: string | undefined;
  8. count: string | undefined;
  9. }
  10. //解构获取参数
  11. const { id, cid, vid, count }: IUrlParams = useParams();

项目目录:

  1. ├─tsconfig.json
  2. ├─src
  3. | ├─App.tsx - 根组件/定义路由规则切换页面
  4. | ├─index.tsx - 入口组件/引入store/引入路由
  5. | ├─views - 页面组件
  6. | | ├─Cart.tsx - 购物车
  7. | | ├─Detail.tsx - 商品详情/获取符合条件的商品信息
  8. | | Home.tsx - 首页/请求商品列表数据
  9. | ├─typings - 类型定义
  10. | | ├─index.ts
  11. | | ├─phone.ts
  12. | | store.ts
  13. | ├─store - 仓库
  14. | | ├─actions.ts
  15. | | ├─actionTypes.ts
  16. | | ├─index.ts
  17. | | ├─reducers.ts
  18. | | state.ts
  19. | ├─services - 请求函数TS封装
  20. | | index.ts
  21. | ├─libs
  22. | | http.ts - axios函数TS封装
  23. | ├─hooks - 公共自定义hooks
  24. | | ├─index.ts
  25. | | ├─useFlatPhoneList.ts - 数据扁平化
  26. | | ├─usePhoneDetail.ts - 请求详情列表
  27. | | ├─usePhoneList.ts - 请求商品列表
  28. | | useSelectorCount.ts - 根据限制数量决定点击购买数量
  29. | ├─components
  30. | | ├─Selector - 数量选择器
  31. | | | ├─index.scss
  32. | | | index.tsx
  33. | | ├─PhoneList - 商品列表
  34. | | | ├─index.scss
  35. | | | ├─index.tsx
  36. | | | Item.tsx
  37. | | ├─Header - 标题
  38. | | | ├─index.scss
  39. | | | index.tsx
  40. | | ├─Detail - 商品详情
  41. | | | ├─hooks.ts
  42. | | | ├─index.scss
  43. | | | ├─index.tsx
  44. | | | ├─VersionGroup - 版本按钮组件
  45. | | | | ├─index.scss
  46. | | | | index.tsx
  47. | | | ├─Title - 详情标题
  48. | | | | index.tsx
  49. | | | ├─Price - 价格
  50. | | | | index.tsx
  51. | | | ├─ColorGroup - 颜色按钮组件
  52. | | | | ├─index.scss
  53. | | | | index.tsx
  54. | | | ├─Bottom - 底部加入购物车
  55. | | | | ├─index.scss
  56. | | | | index.tsx
  57. | | | ├─Banner - Banner
  58. | | | | index.tsx

源码地址: https://gitee.com/kevinleeeee/ts-react-hook-express-shoppingcart-demo