Vue3 + TS 实现仿知乎专栏项目

Siona

Vue3 + TS 实现仿知乎专栏项目

Vue3 + TS 实现仿知乎专栏项目open in new window

你好!TypeScript

TypeScript 了解

编程语言的类型

什么是 TypeScript

  • JavaScript that scales
  • 静态类型风格的类型系统
  • 从 ES6 到 ES10 甚至是 ESNext 的语法支持
  • 兼容各种浏览器,各种系统,各种服务器,完全开源

为什么要使用 TypeScript

  • 程序更容易理解
    • 问题:函数或者方法输入输出的参数类型,外部条件等
    • 动态语言的约束:需要手动调试等过程
    • 有了 TypeScript:代码本身就可以回答上述问题
  • 效率更高
    • 在不同的代码块和定义中进行跳转
    • 代码自动补全
    • 丰富的接口提示
  • 更少的错误
    • 编译期间能够发现大部分错误
    • 杜绝一些比较常见错误
  • 非常好的包容性
    • 完全兼容 JavaScript【可直接将 js 文件命名为 .ts】
    • 第三方库可以单独编写类型文件
    • 大多数项目都支持 TypeScript
  • ❌ 一些小缺点
    • 增加了一些学习成本
    • 短期内增加了一些开发成本

Mac 命令行工具:iTerm2 推荐工具:VisualCode、WebStorm

安装 Typescript

Typescript 官网地址: https://www.typescriptlang.org/zh/open in new window

使用 nvm 来管理 node 版本: https://github.com/nvm-sh/nvmopen in new window

安装 Typescript:

# 全局安装 TypeScript
npm install -g typescript

使用 tsc 全局命令:

# 查看 tsc 版本
tsc -v
# 编译 ts 文件
tsc fileName.ts
// test.ts
const hello = (name: string) => {
    return `hello ${name}`
}

hello('siona')

运行 tsc test.ts 之后,会生成 test.js。用于向下兼容适配。文件内容如下:

// test.js
var hello = function (name) {
    return "hello" + name;
};
hello('siona');

TypeScript 只会进行类型检查,并不会改变生成的 test.js 的内容。

原始数据类型

Typescript 文档地址:Basic Typesopen in new window

Javascript 类型分类: MDN (1)7 种原始类型 - primitive values:

  • Boolean
  • Null
  • Undefined
  • Number
  • BigInt
  • String
  • Symbol
let isDone: boolean = false

// 接下来来到 number,注意 es6 还支持 2 进制和 8 进制,让我们来感受下
let age: number = 10
let binaryNumber: number = 0b1111

// 之后是字符串,注意 es6 新增的模版字符串也是没有问题的
let firstName: string = 'viking'
let message: string = `Hello, ${firstName}, age is ${age}`

// 还有就是两个奇葩兄弟两,undefined 和 null
let u: undefined = undefined
let n: null = null

// 注意 undefined 和 null 是所有类型的子类型。也就是说 undefined 类型的变量,可以赋值给 number 类型的变量:
let num: number = undefined

(2)any 类型

TS 官网 | anyopen in new window

let notSure: any = 4
notSure = 'maybe it is a string'
notSure = true
// 在任意值上访问任何属性都是允许的:
notSure.myName
// 也允许调用任何方法:
notSure.getName()

当有明确的类型时,应避免使用 any 类型。否则,TS 与 JS 将没有任何区别

Array 数组 和 Tuple 元组

Array 和 Tupleopen in new window

// 最简单的方法是使用「类型 + 方括号」来表示数组:
let arrOfNumbers: number[] = [1, 2, 3, 4]
// 数组的项中不允许出现其他的类型:
// 数组的一些方法的参数也会根据数组在定义时约定的类型进行限制:
arrOfNumbers.push(3)
arrOfNumbers.push('abc') // 报错

// 元组的表示和数组非常类似,只不过它将类型写在了里面 这就对每一项起到了限定的作用
let user: [string, number] = ['siona', 20]
// 但是当我们写少一项 就会报错 同样写多一项也会有问题
user = ['molly', 20, true]
// 元组本质还是数组,使用数组的方法可以添加元素,但是只能是限定元素类型的一种,是一种联合类型
user.push('hh')

Interface 接口

Typescript 文档地址:Interface 接口open in new window

  • 对对象的形状(shape)进行描述
  • Duck Typing(鸭子类型):对象的推断策略,关注对象本身,而不是对象中的内容。

Duck Typing: 如果某个东西长得像鸭子,像鸭子一样游泳,像鸭子一样嘎嘎叫,那它就可以被看成是一只鸭子。

Interface 不会转成 JS,只能用来作类型的动态检查。

// 我们定义了一个接口 IPerson,通常建议接口名规范为 I 开头
interface IPerson {
    name: string;
    age: number;
}

// 接着定义了一个变量 siona,它的类型是 IPerson。这样,我们就约束了 siona 的形状必须和接口 IPerson 一致。
let siona: IPerson = {
    name: 'siona',
    age: 20
}

// 有时我们希望不要完全匹配一个形状,那么可以用可选属性 ?:
interface IPerson {
    name: string;
    age?: number;
}

let siona_1: IPerson = {
    name: 'siona_1'
}

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

interface IPerson {
    readonly id: number;
    name: string;
    age?: number;
}

siona_1.id = 9527  // 报错,类似 const,readonly 用来属性上,const 用在变量上

Function 函数

Typescript 文档地址:Functionsopen in new window

  • 在 JS 中,函数是一等公民。
    • 函数可以作为参数,存入数组,可以被另外一个函数返回,可以被赋值给另一个变量……

函数主要由两部分构成:输入(参数)、输出(返回结果)。

// 来到我们的第一个例子,约定输入,约定输出。返回结果的类型在() 后面 → (): number {}
function add(x: number, y: number): number {
    return x + y
}

// 可选参数。注意可选参数后面 不能添加确定参数,否则会报错
function add(x: number, y: number, z?: number): number {
    if (typeof z === 'number') {
        return x + y + z
    } else {
        return x + y
    }
}

// 函数本身的类型【将函数赋值给另一个函数类型 => number 是 TS 声明返回值类型,而不是 ES6 的箭头函数】
// 注意,在 TS 中,冒号: 后面都是在声明类型,与实际的代码逻辑没有关系
const add2: (x: number, y: number, z?: number) => number = add

// interface 描述函数类型
// 【函数表达式写法】
const sum = (x: number, y: number) => {
    return x + y
}

// 注意,在 interface 中返回值类型是 : number,直接写 => number
interface ISum {
    (x: number, y: number): number
}

const sum2: ISum = sum

类型推论,联合类型 和 类型断言

Typescript 文档地址:类型推论 - Type Inferenceopen in new window

联合类型 - union typesopen in new window

// 联合类型(union types):只需要用中竖线来分割两个类型
let numberOrString: number | string
numberOrString = 'abc'
numberOrString = 123
// 当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里【共有的属性或方法】
numberOrString.length  // 报错,string 的属性
numberOrString.toString() // 不报错,共有属性

类型断言 - type assertionsopen in new window

// 类型断言(type assertions):使用 as 关键字
// 这里我们可以用 as 关键字,告诉typescript 编译器,你没法判断我的代码,
// 但是我本人很清楚,这里我就把它看作是一个 string,你可以给他用 string 的方法。
function getLength(input: string | number): number {
    const str = input as string
    if (str.length) {
        return str.length
    } else {
        const number = input as number
        return number.toString().length
    }
}

类型守卫 - type guardopen in new window

// 类型守卫(type guard):使用 typeof 关键字【推荐 ✅】
// typescript 在不同的条件分支里面,智能的缩小了范围,这样我们代码出错的几率就大大的降低了。
function getLength2(input: string | number): number {
    if (typeof input === 'string') {
        return input.length
    } else {
        return input.toString().length
    }
}

Class 类

面向对象编程的三大特点

  • 封装(Encapsulation):将对数据的操作细节隐藏起来,只暴露对外的接口。外界调用端不需要(也不可能)知道细节,就能通过对外提供的接口来访问该对象,
  • 继承(Inheritance):子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性。
  • 多态(Polymorphism):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。

类 - Classopen in new window

class Animal {
    name: string;

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

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

const snake = new Animal('lily')

// 继承的特性
class Dog extends Animal {
    bark() {
        return `${this.name} is barking`
    }
}

const xiaobao = new Dog('xiaobao')
console.log(xiaobao.run())
console.log(xiaobao.bark())

// 这里我们重写构造函数,注意在子类的构造函数中,必须使用 super 调用父类的方法,要不就会报错。
class Cat extends Animal {
    constructor(name) {
        super(name)
        console.log(this.name)
    }

    run() {
        return 'Meow, ' + super.run()
    }
}

const maomao = new Cat('maomao')
console.log(maomao.run())

类成员的访问修饰符open in new window

  • public 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public 的
  • private 修饰的属性或方法是私有的,不能在声明它的类的外部访问
  • protected 修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的

类与接口

类实现一个接口open in new window

interface Radio {
    switchRadio(trigger: boolean): void;
}

class Car implements Radio {
    switchRadio(trigger) {
        return 123
    }
}

class Cellphone implements Radio {
    switchRadio() {
    }
}

interface Battery {
    checkBatteryStatus(): void;
}

// 要实现多个接口,我们只需要中间用 逗号 隔开即可。
class Cellphone implements Radio, Battery {
    switchRadio() {
    }

    checkBatteryStatus() {

    }
}

枚举 Enums

枚举 Enumsopen in new window

枚举:一定范围内的一些列常量。例如,星期、三原色、方位等。

// 数字枚举,一个数字枚举可以用 enum 这个关键词来定义,
// 、我们定义一系列的方向,然后这里面的值,枚举成员会被赋值为从 0 开始递增的数字
enum Direction {
    Up,
    Down,
    Left,
    Right,
}

console.log(Direction.Up)  // 输出下标 0

// 还有一个神奇的点是这个枚举还做了反向映射,可以将枚举看作数组
console.log(Direction[0]) // 输出 Up 字符串


// tsc enums.ts 之后生成的 enums.js 文件
var Direction;
(function (Direction) {
    Direction[Direction["Up"] = 0] = "Up";
    Direction[Direction["Down"] = 1] = "Down";
    Direction[Direction["Left"] = 2] = "Left";
    Direction[Direction["Right"] = 3] = "Right";
})(Direction || (Direction = {}));
console.log(Direction.Up);
console.log(Direction[0]);

// -----------------------------------------------

// 字符串枚举
enum Direction {
    Up = 'UP',
    Down = 'DOWN',
    Left = 'LEFT',
    Right = 'RIGHT',
}

const value = 'UP'
if (value === Direction.Up) {
    console.log('go up!')
}

// 常量枚举,提高性能,内联代码
const enum Direction {
    Up = 'UP',
    Down = 'DOWN',
    Left = 'LEFT',
    Right = 'RIGHT',
}

// 枚举有两种:常量枚举、计算枚举

泛型 Generics -- TS 最难点 ‼️

泛型 Genericsopen in new window

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

function echo(arg) {
    return arg
}

const result_1 = echo(123)
// 此时,查看 result_1 类型,将鼠标移动到 result_1,显示 const result_1: any
// 这时候我们发现了一个问题,我们传入了数字,但是返回了 any

// 可以传入多种类型的值,使用泛型 xxx<T>,T 只是一种习惯写法
function echo<T>(arg: T): T {
    return arg
}

const result_2 = echo(123)  // result_2 是 number 类型,是由类型推论会推断出是 number 类型

// 泛型也可以传入多个值,并将值反转,Tuple 元组 [T,U]
function swap<T, U>(tuple: [T, U]): [U, T] {
    return [tuple[1], tuple[0]]
}

const result_3 = swap(['string', 123])  // [123, 'string']

泛型第二部分 - 泛型约束

在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法

function echoWithArr<T>(arg: T): T {
    console.log(arg.length)  // 报错,Property 'length' does not exist on type 'T'. 泛型 T 不一定包含属性 length
    return arg
}

// 上例中,泛型 T 不一定包含属性 length,我们可以给他传入任意类型,当然有些不包括 length 属性,那样就会报错

// 约束泛型
interface IWithLength {
    length: number;
}

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

echoWithLength('str')
const result3 = echoWithLength({ length: 10, width: 10 })  // 可以有更多的属性,只要有 length 即可,没有 length 就会报错
const result4 = echoWithLength([1, 2, 3])

泛型第三部分 - 泛型与类和接口

class Queue {
    private data = [];

    push(item) {
        return this.data.push(item)
    }

    pop() {
        return this.data.shift()
    }
}

const queue = new Queue()
queue.push(1)
queue.push('str')
console.log(queue.pop().toFixed())
console.log(queue.pop().toFixed())

//在上述代码中存在一个问题,它允许你向队列中添加任何类型的数据,当然,当数据被弹出队列时,也可以是任意类型。在上面的示例中,看起来人们可以向队列中添加string 类型的数据,但是那么在使用的过程中,就会出现我们无法捕捉到的错误,

class Queue<T> {
    private data = [];

    push(item: T) {
        return this.data.push(item)
    }

    pop(): T {
        return this.data.shift()
    }
}

const queue = new Queue<number>()

//泛型和 interface
interface KeyPair<T, U> {
    key: T;
    value: U;
}

let kp1: KeyPair<number, string> = {key: 1, value: "str"}
let kp2: KeyPair<string, number> = {key: "str", value: 123}

类型别名 和 交叉类型

类型别名 Type Aliasesopen in new window

类型别名,就是给类型起一个别名,让它可以更方便的被重用。

let sum: (x: number, y: number) => number
const result = sum(1, 2)
type PlusType = (x: number, y: number) => number
let sum2: PlusType

// 支持联合类型
type StrOrNumber = string | number
let result2: StrOrNumber = '123'
result2 = 123

// 字符串字面量
type Directions = 'Up' | 'Down' | 'Left' | 'Right'
let toWhere: Directions = 'Up'

交叉类型 Intersection Typesopen in new window

interface IName {
    name: string
}

type IPerson = IName & { age: number }
let person: IPerson = {name: 'hello', age: 12}

声明文件

声明文件open in new window

@tpyes 官方声明文件库open in new window

@types 搜索声明库open in new window

类型声明文件,里面没有任何的实际实现代码,只有类型声明 - 比如,interface, function, class 等。

① 简单使用 declare 声明

// axios.d.ts
declare function axios(url: string): string

// declaration-file.ts 使用
axios('http://localhost:8080/api/getUser');

② 使用 interface 声明

// axios.d.ts
interface IAxios {
    get: (url: string) => string;
    post: (url: string, data: any) => string;
} 

// declaration-file.ts 使用

内置类型

内置类型open in new window

const a: Array<number> = [1, 2, 3]
// 大家可以看到这个类型,不同的文件中有多处定义,但是它们都是 内部定义的一部分,然后根据不同的版本或者功能合并在了一起,一个interface 或者 类多次定义会合并在一起。这些文件一般都是以 lib 开头,以 d.ts 结尾,告诉大家,我是一个内置对象类型欧
const date: Date = new Date()
const reg = /abc/
// 我们还可以使用一些 build in object,内置对象,比如 Math 与其他全局对象不同的是,Math 不是一个构造器。Math 的所有属性与方法都是静态的。

Math.pow(2, 2)

// DOM 和 BOM 标准对象
// document 对象,返回的是一个 HTMLElement
let body: HTMLElement = document.body
// document 上面的query 方法,返回的是一个 nodeList 类型
let allLis = document.querySelectorAll('li')

//当然添加事件也是很重要的一部分,document 上面有 addEventListener 方法,注意这个回调函数,因为类型推断,这里面的 e 事件对象也自动获得了类型,这里是个 mouseEvent 类型,因为点击是一个鼠标事件,现在我们可以方便的使用 e 上面的方法和属性。
document.addEventListener('click', (e) => {
    e.preventDefault()
})

Utility Typesopen in new window

Typescript 还提供了一些功能性,帮助性的类型,这些类型,大家在 js 的世界是看不到的,这些类型叫做 utility types,提供一些简洁明快而且非常方便的功能。

// partial,它可以把传入的类型都变成可选
interface IPerson {
    name: string
    age: number
}

let viking: IPerson = {name: 'viking', age: 20}
type IPartial = Partial<IPerson>
let viking2: IPartial = {}

// Omit,它返回的类型可以忽略传入类型的某个属性

type IOmit = Omit<IPerson, 'name'>
let viking3: IOmit = {age: 20}

配置文件

配置文件的官方文档open in new window

配置示例

{
    "files": [
        "test.ts",
        "test2.d.ts"
    ],
    "compilerOptions": {
        "outDir": "./output",
        "module": "ESNext",
        "target": "ES5",
        "declaration": true
    }
}
Last Updated 5/24/2024, 9:46:21 AM