Vue3+TS快速入门

794 阅读11分钟

TypeScript

编程语言的类型

1.动态类型语言(Dynamically Typed Language)

特点:运行期间进行数据类型检查

在使用动态类型语言编程期间,不用给变量指定数据类型,如:JavaScript、Python、Ruby

2.静态类型语言(Statically Typed Language)

特点:编译阶段进行数据类型检查

在使用静态类型语言编程期间,需要手动的指定数据类型,如:C、C++、C#、Java

什么是Typescript?

可以理解为可扩展的JavaScript,即JavaScript 的超集。

提供了:

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

为什么要学typescript?

  • 程序更容易理解
  • 效率更高
  • 更少的错误

安装typescript

typescript官网:www.typescriptlang.org/zh/

npm i -g typescript

2.使用tsc全局命令

//查看tsc版本
tsc -v
//编译ts文件
tsc fileName.ts

原始数据类型和Any类型

原始类型

  • Number
  • String
  • Boolean
  • Undefined
  • Null
  • BigInt(ES6)
  • Symbol(ES6)

any类型: 允许赋值为任意类型,可以访问任何属性和方法(如果有明确的类型应避免使用这个类型)

数组和元组

数组(Array)

声明数字类型的数组:

let arrOfNumbers:number[] = [1,2,3]

元组(Tuple)

即特殊类型的数组,可以使用数组的api,可以自定义多种数据类型,但新增的数据必须是已经定义好的数据类型

let user:[string,number] = ['viking',20]

Interface 接口

  • 用于对「对象的形状(Shape)」进行描述
  • 用 implements 的方法来抽象 类的属性和方法
  • 定义函数类型
  • Duck Typing(鸭子类型):是动态编程语言的一种对象推断策略,它更关心对象如何被使用而不是对象本身

Interface是不存在于JavaScript中的概念,所以在ts编译后不会被转换过去,只能做类型的静态检查

interface Person {//用interface关键字定义了一个接口

   readonly id: number //readonly代表只读属性

    name:  string,

    age?: number  //属性后跟?代表是可选参数

}

let viking: Person = {
    id = 1,
    name: 'viking'
    age:22  //可以不声明
}

Function 函数

  • 输入:声明参数的类型
  • 输出:声明返回值类型
  • 可选参数:在参数后加 (注:可选参数后面不要加确定参数,不然程序对参数的判断会发生混乱)

最后一行的这个箭头并不是箭头函数,而是ts中声明函数类型返回值的方法

  • interface描述函数类型

类型推论、联合类型、类型断言

类型推论:

在没有明确指定类型的时候推测出一个类型

let str = 'str';

联合类型:

可以指定多种类型(但只能访问联合类型所有类型里共有的属性或方法)

let numberOrString:number | string

类型断言:

TypeScript 类型断言用来告诉编译器你比它更了解这个类型

不是类型转换,断言成一个联合类型中不存在的类型是会出现错误的

在不确定类型的时候访问其中一个类型的属性或方法

1.用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
  }
}

2.类型守卫(type-guard)

当遇到一个联合类型的时候,使用条件语句,它可以自动帮你来缩小类型的范围

// typescript 在不同的条件分支里面,智能的缩小了范围,这样我们代码出错的几率就大大的降低了。
function getLength2(input: string | number): number {
  if (typeof input === 'string') {
    return input.length
  } else {
    return input.toString().length
  }
}

枚举(Enums)

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

  • 枚举默认从0开始赋值,举里的项会自动递增(+1),若手动赋值,则会接着手动赋值的枚举项递增
  • 如果某个属性赋值,其他属性也要赋值,否则报错
  • js的赋值语句返回被赋的值
  • 定义枚举时加const则为常量枚举,常量枚举可提升性能
enum Direction {
  Up,
  Down,
  Left,
  Right,
}
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!')
}

泛型(Generics)

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

可以先看做一个占位符,在使用时才动态控制其类型

function echo(arg) {
  return arg
}
const result = echo(123)
// 这时候我们发现了一个问题,我们传入了数字,但是返回了 any
//<T>为泛型 名字可自定义
function echo<T>(arg: T): T {
  return arg
}
const result = echo(123)

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

const result = swap(['string', 123])

泛型约束

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

function echoWithArr<T>(arg: T): T {
  console.log(arg.length)
  return arg
}

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

interface IWithLength {
  length: number;
}
//泛型约束 通过 extends 关键字来设置约束条件,而不是想传入啥就传入啥
function echoWithLength<T extends IWithLength>(arg: T): T {
  console.log(arg.length)
  return arg
}

echoWithLength('str')
const result3 = echoWithLength({length: 10})
const result4 = echoWithLength([1, 2, 3])

泛型与类和接口

1.创建一个拥有特定类型的容器,比如:class 和interface上的泛型仿佛给你一个容器贴标签一样,比如:

let arrTwo:Array<number>=[1,2,3]

给这个数组贴了一个number的标签,告诉arrTwo,是一个装满number类型的数组或者是装着数字的队列

2.灵活约束参数的类型,传入的参数必须是xx方法xx属性,否则就会报错

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}

类型别名,字面量和交叉类型

1.类型别名

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

关键字:type

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

2.字面量

常量作为类型写在冒号后面,此时冒号前面的变量只能赋值为该常量;

const str:'name' = 'name'//只能等于name 否则报错
const number1 =1

字面量可以用 | 设置多个,形成固定的赋值范围

// 字符串字面量
type Directions = 'Up' | 'Down' | 'Left' | 'Right'
let toWhere: Directions = 'Up'//只能为其中的四个值

3.交叉类型 (Intersection Types)

对Interface进行扩展 &

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

Vue3

vue3新特性

性能提升(得益于重写虚拟dom的实现和tree-shaking的优化)

  • 打包大小减少41%
  • 初次渲染快55%,更新快133%
  • 内存使用减少54%

Composition API(一系列api的合集,解决复杂组件难以维护的问题)

  • ref和reactive
  • computed和watch
  • 新的生命周期函数
  • 自定义函数 - Hooks

其他新增特性

  • Teleport - 瞬移组件的位置
  • Suspense - 异步加载新组件的新福音
  • 全局API的修改和优化
  • 其他试验中的新特性

更好的ts支持

启始准备

使用 vue-cli 配置 vue3 开发环境

Vue cli

// 安装或者升级 
npm install -g @vue/cli 
# OR 
yarn global add @vue/cli 
// 保证 vue cli 版本在 4.5.0 以上 
vue --version 
// 创建项目 
vue create my-project 

之后的步骤

  • Please pick a preset - 选择 Manually select features
  • Check the features needed for your project - 多选择上 TypeScript,特别注意点空格是选择,点回车是下一步
  • Choose a version of Vue.js that you want to start the project with - 选择 3.x (Preview)
  • Use class-style component syntax - 输入 n,回车
  • Use Babel alongside TypeScript - 输入 n,回车
  • Pick a linter /formatter config - 直接回车
  • Pick additional lint features - 直接回车
  • Where do you prefer placing config for Babel, ESLint, etc.? - 直接回车
  • Save this as a preset for future projects? - 输入 n,回车

也可以直接启动图形化界面创建

vue ui

image.png 推荐安装的插件

如果 eslint 不生效,可以在根目录创建 .vscode 文件夹,然后在文件夹中创建 settings.json
然后输入

{ "eslint.validate": [ "typescript" ] }

为什么有vue3?

vue2遇到的难题

  • 随着功能的增长,复杂组件的代码变得难以维护

Option API是以逻辑考虑来分配代码位置,Composition API以功能用途来分配代码位置

  • vue2对于ts支持有限
  • 使用Mixin可以解决按特征归类的问题,但是也有各种缺点:
  1. 命名冲突
  2. 不清楚暴露出来的变量的作用
  3. 重用到其他component会经常出现问题

Ref 语法

setup 方法

  • setup的意思是准备,它是在props、data、computed、methods 、生命周期函数之前运行的。
  • 在setup方法中,无法访问this
  • 在setup中返回的数据,我们要精确的控制哪些数据和方法要被导出使用
  • 在vue2 方法都是写在methods中的,vue3则是直接写在setup上

ref 函数

声明一个基本类型的响应数据声明

<template>
// 在vue中,对一个ref对象在模板中引用的时候,它可以直接帮忙把里面的值展示出来
  <h1>{{count}}</h1>
  <h1>{{double}}</h1>
  <button @click="increase">+1</button>
</template>

import { ref } from "vue"

setup() {
  // ref 是一个函数,它接受一个参数,返回的就是一个神奇的响应式对象 
  //我们初始化的这个 0 作为参数包裹到这个对象中去,
  //在未来可以检测到改变并作出对应的相应。
  const count = ref(0)
  const double = computed(() => {
    return count.value * 2
  })
  const increase = () => {
    count.value++
  }
  return {
    count,
    increase,
    double
  }
}

Reactive 函数

声明一个引用类型的响应数据(如果将里面的值抽出单独使用会丢失响应特性)

import { ref, computed, reactive, toRefs } from 'vue'

interface DataProps {
  count: number;
  double: number;
  increase: () => void;
}

setup() {
  const data: DataProps  = reactive({
    count: 0,
    increase: () => { data.count++},
    double: computed(() => data.count * 2)
  })
  const refData = toRefs(data)
  return {
    ...refData
  }
}

使用 ref 还是 reactive 可以选择这样的准则

  • 第一,就像刚才的原生 js的代码一样,像你平常写普通的 js 代码选择原始类型和对象类型一样来选择是使用ref还是reactive
  • 第二,所有场景都使用reactive,但是要记得使用toRefs保证reactive对象属性保持响应性。

vue3响应式变化

vue3响应式的高明之处,内部依赖了ES6的proxy对象,改变了原来object.defineProPerty的弊端,完美支持数组和对象的修改操作,让$set成为过去时。

Vue3生命周期

在 setup 中使用的 hook 名称和原来生命周期的对应关系

setup() {
  onMounted(() => {
    console.log('mounted')
  })
  onUpdated(() => {
    console.log('updated')
  })
  onRenderTriggered((event) => {
    console.log(event)
  })
}

Watch

// watch 简单应用
watch(data, () => {
  document.title = 'updated ' + data.count
})
// watch 的两个参数,代表新的值和旧的值
watch(refData.count, (newValue, oldValue) => {
  console.log('old', oldValue)
  console.log('new', newValue)
  document.title = 'updated ' + data.count
})

// watch 多个值,返回的也是多个值的数组
watch([greetings, data], (newValue, oldValue) => {
  console.log('old', oldValue)
  console.log('new', newValue)
  document.title = 'updated' + greetings.value + data.count
})

// 使用 getter 的写法 watch reactive 对象中的一项
watch([greetings, () => data.count], (newValue, oldValue) => {
  console.log('old', oldValue)
  console.log('new', newValue)
  document.title = 'updated' + greetings.value + data.count
})

模块化开发

鼠标追踪器

// 在单组件内添加对应逻辑
const x = ref(0)
const y = ref(0)
const updateMouse = (e: MouseEvent) => {
  x.value = e.pageX
  y.value = e.pageY
}
onMounted(() => {
  document.addEventListener('click', updateMouse)
})
onUnmounted(() => {
  document.removeEventListener('click', updateMouse)
})

// 将组件内逻辑抽象成可复用的函数
function useMouseTracker() {
  // const positions = reactive<MousePostion>({
  //   x: 0,
  //   y: 0
  // })
  const x = ref(0)
  const y = ref(0)
  const updatePosition = (event: MouseEvent) => {
    x.value = event.clientX
    y.value = event.clientY 
  }
  onMounted(() => {
    document.addEventListener('click', updatePosition)
  })
  onUnmounted(() => {
    document.removeEventListener('click', updatePosition)
  })
  return { x, y }
}

export default useMouseTracker

vue3 这种实现方式的优点

  • 第一:它可以清楚的知道 xy 这两个值的来源,这两个参数是干什么的,他们来自 useMouseTracker 的返回,那么它们就是用来追踪鼠标位置的值。
  • 第二:我们可以xy 可以设置任何别名,这样就避免了命名冲突的风险。
  • 第三:这段逻辑可以脱离组件存在,因为它本来就和组件的实现没有任何关系,我们不需要添加任何组件实现相应的功能。只有逻辑代码在里面,不需要模版。

模块化难度上升 - useURLLoader

axios 文档地址

// 安装 axios 注意它是自带 type 文件的,所以我们不需要给它另外安装 typescript 的定义文件
npm install axios --save
import { ref } from 'vue'
import axios from 'axios'
// 添加一个参数作为要使用的 地址
const useURLLoader = (url: string) => {
// 声明几个ref,代表不同的状态和结果
  const result = ref(null)
  const loading = ref(true)
  const loaded = ref(false)
  const error = ref(null)

// 发送异步请求,获得data
// 由于 axios 都有定义,所以rawData 可以轻松知道其类型
  axios.get(url).then((rawData) => {
    loading.value = false
    loaded.value = true
    result.value = rawData.data
  }).catch((e) => {
    error.value = e
  })
  // 将这些ref 一一返回
  return {
    result,
    loading,
    error,
    loaded
  }
}
export default useURLLoader

免费获取狗狗图片的 API 地址

// 使用 urlLoader 展示狗狗图片
const { result, loading, loaded } = useURLLoader('https://dog.ceo/api/breeds/image/random')

...
<h1 v-if="loading">Loading!...</h1>
<img v-if="loaded" :src="result.message" >

模块化结合typescript - 泛型改造

// 为函数添加泛型
function useURLLoader<T>(url: string) {
  const result = ref<T | null>(null)
// 在应用中的使用,可以定义不同的数据类型
interface DogResult {
  message: string;
  status: string;
}
interface CatResult {
  id: string;
  url: string;
  width: number;
  height: number;
}

// 免费猫图片的 API  https://api.thecatapi.com/v1/images/search?limit=1
const { result, loading, loaded } = useURLLoader<CatResult[]>('https://api.thecatapi.com/v1/images/search?limit=1')

defineComponent包裹组件

用于使用类型推断定义Vue组件的类型助手

Teleport - 瞬间移动

Teleport 文档地址

vue3 新添加了一个默认的组件就叫 Teleport,我们可以拿过来直接使用,它上面有一个to的属性,它接受一个css query selector作为参数,这就是代表要把这个组件渲染到哪个 dom 元素中

<template>
  <teleport to="#modal">
    <div id="center">
      <h1>this is a modal</h1>
    </div>
  </teleport>
</template>
<style>
  #center {
    width: 200px;
    height: 200px;
    border: 2px solid black;
    background: white;
    position: fixed;
    left: 50%;
    top: 50%;
    margin-left: -100px;
    margin-top: -100px;
  }
</style>

Suspense - 异步请求好帮手

定义一个异步组件,在 setup 返回一个 Promise,AsyncShow.vue

<template>
  <h1>{{result}}</h1>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
  setup() {
    return new Promise((resolve) => {
      setTimeout(() => {
        return resolve({
          result: 42
        })
      }, 3000)
    })
  }
})
</script>

在 App 中使用

<Suspense>
  <template #default>
    <async-show />
  </template>
  <template #fallback>
    <h1>Loading !...</h1>
  </template>
</Suspense>

Suspense 中可以添加多个异步组件

全局 API 修改

Vue2 的全局配置

import Vue from 'vue'
import App from './App.vue'

Vue.config.ignoredElements = [/^app-/]
Vue.use(/* ... */)
Vue.mixin(/* ... */)
Vue.component(/* ... */)
Vue.directive(/* ... */)

Vue.prototype.customProperty = () => {}

new Vue({
  render: h => h(App)
}).$mount('#app')

Vue2 这样写在一定程度上修改了 Vue 对象的全局状态。

  • 第一,在单元测试中,全局配置非常容易污染全局环境,用户需要在每次 case 之间,保存和恢复配置。有一些 api (vue use vue mixin)甚至没有方法恢复配置,这就让一些插件的测试非常的困难。
  • 第二,在不同的 APP 中,如果想共享一份有不同配置的 vue 对象,也变得非常困难。

Vue3 的修改

import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)
// 这个时候 app 就是一个 App 的实例,现在再设置任何的配置是在不同的 app 实例上面的,
//不会像vue2 一样发生任何的冲突。

app.config.isCustomElement = tag => tag.startsWith('app-')
app.use(/* ... */)
app.mixin(/* ... */)
app.component(/* ... */)
app.directive(/* ... */)

app.config.globalProperties.customProperty = () => {}

// 当配置结束以后,我们再把 App 使用 mount 方法挂载到固定的 DOM 的节点上。
app.mount(App, '#app')