vue3 组合式API与TypeScript的入门教程

554 阅读12分钟

1. vue3的组合API函数

1.1. setup函数

在script标签里面,声明的变量或者函数写在setup函数里面,要return返回,模版当中才能使用

<script lang="ts">
import { ref } from 'vue'

export default {
  setup() {
    const age = ref('123')
    const fn = () => {}
    return {
      age
    }
  }
}
</script>

假设声明了一个fn函数,但是没有return,在模版中使用了,就会报错

image.png

1.2 setup语法糖

如果在script标签上面写了setup关键字,就开启了script语法

1.2.1 响应式数据在模板中能直接使用

<script lang="ts" setup>
import { ref } from 'vue'
+const age = ref('123')
+const fn = () => {} // 下面template要使用,既不需要return,也不需要setup函数了
</script>
<template>
  <div id="App">
    {{ age }}
    <button @click="fn">点击触发fn</button>
    <router-view></router-view>
  </div>
</template>

1.2.2 import导入的内容也能够在顶层使用

setup语法糖,引入组件,template里面能够直接用,不需要注册

<script lang="ts" setup>
import Helloworld from '@/components/Helloworld.vue'
</script>
<template>
  <div id="App">
      <Helloworld></Helloworld> // 这里能够直接使用
  </div>
</template>

如果不是用setup语法糖,用的是setup函数,引入组件,还是需要注册

<script lang="ts">
import { ref } from 'vue'
+import helloworld from './views/helloworld.vue'
export default {
+  components: {
    helloworld
  },
  setup() {
    const age = ref('123')
    const fn = () => {}
    return {
      age,
      fn
    }
  }
}
</script>

1.2.3 props和emits的使用

(1) setup函数的props

setup钩子函数里面,要在setup(props)这样去接受props,然后打印props.变量名的方式来打印,

<script lang="ts">
export default {
+  props: {
    name: {
      type: String,
      default: '小航哥'
    }
  },
+  setup(props) {
    console.log('name', props.name)
    return {}
  }
}
</script>

<template>
  <div class="hello-page">hellohellohellohellohellohello</div>
</template>

<style lang="scss" scoped></style>

解构变量会让变量失去响应式,使用toRefs能够让每个属性都变成ref的数据

<script lang="ts">
export default {
  props: {
    name: {
      type: String,
      default: '小航哥'
    }
  },
  setup(props) {
+    const {name} = props
+    const {name} = toRefs(props)
+    console.log(name.value) // 后续访问,就要这样去访问,是ref数据
    return {}
  }
}
</script>

<template>
  <div class="hello-page">hellohellohellohellohellohello</div>
</template>

<style lang="scss" scoped></style>

(2) setup函数里面的emits

在子组件helloworld.vue里面setup函数的第二个参数去接受,context是一个对象,里面有emit属性

<script lang="ts">
import { toRefs } from 'vue'

export default {
  props: {
    name: {
      type: String,
      default: '小航哥'
    }
  },
  setup(props, context) {
+    context.emit('fn')
    return {}
  }
}

在父组件App.vue里面,@自定义事件名="事件函数",这个还是和以前类似的操作

<script lang="ts" setup>
import helloworld from './views/helloworld.vue'
const name = '123'
const fn = () => {
  console.log('123123')
}
</script>
<template>
  <div id="App">
    <helloworld :name="name" @fn="fn"></helloworld>
    <router-view></router-view>
  </div>
</template>

<style scoped lang="scss">
#App {
  display: flex;
  height: 100vh;
}
</style>

(3) setup语法糖里面的props的使用

js的写法

<script setup>
const props = defineProps({
    name: String
    age: Number
})
</script>

ts的写法:

  1. ts写法,defineProps<>()跟上了一个尖括号,然后在尖括号里面使用了{}花括号,defineProps<{}>(),然后在里面写类型
  2. 注意,string在下面是小写,在上面的js写法是大写
<script setup lang='ts'>
const props = defineProps<{
    name: string
    age: number
}>()
</script>

(4) setup语法糖里面的emit的使用

js的写法

<script setup>
const emits = defineEmits(['change', 'delete'])
</script>

ts的写法:

  1. ts写法,defineEmits后面跟上了一个尖括号,在尖括号里面使用了{}花括号,defineEmits<{}>(),在里面写类型
  2. 注意,e:'自定义事件名',是固定写法,自定义事件名可以任意的取
<script setup lang='ts'>
const emits = defineEmits<{
    (e: 'changeName', id: string):void
}>()

emits('changeName')// 触发这个自定义事件名
emits('changeAge') // changeAge没有在上面的defineEmits注册,会报错
</script>

(5) emits和props的注意点

  1. setup语法糖里面的父传子,和子传父,可以直接使用definePropsdefineEmits的API,不需要额外的引入。并且defineProps和defineEmits只能在setup语法糖里面去写
  2. defineProps里面的变量,在template里面是可以直接用的
<script setup lang='ts'>
const props = defineProps<{
    name: string
    age: number
}>()
// 但是注意,在script里面要使用props里面的变量,必须用props.变量名的方式
console.log(props.name)
</script>

<template>
    <!--这里name可以直接使用-->
    <div>{{name}}</div>
</template>
  1. 父传子如果用defineProps,如何给默认值?目前最好是用withDefaults

使用泛型,定义props的时候,上面这样我们无法给出默认值

const props = defineProps<{ name: string age: number }>()

withDefaults方法,不需要引入 withDefaults方法的参数1,就是defineProps<{ age: number; name: string; }>(),,参数2是对象,里面写默认值

const props = withDefaults(defineProps<{ age: number; name: string; }>(), 
{ age: 123, name: '小航哥' })

具备的优势:

  1. 代码更加的简洁
  2. 能够使用纯ts来声明props和编译事件(下面我们会讲props的内容)
  3. 更好的运行时性能

2. 生命周期钩子函数

setup钩子函数第一
onBeforeMount
onMounted
onBeforeUpdate
onUpdated
onBeforeUnmounted
onUnmounted
onActivated
onDeactivated

因为有setup语法糖,现在一般在onMounted()钩子函数里面去调用获取数据的接口

3. ref函数

vue3,最常用的是通过ref函数来声明响应式的数据,类似于vue2里面的data里面写的数据

let num = ref(1231)
num.value = 456

注意: 1.修改ref声明的变量的值的时候一定用.value的形式

ref能够声明各种的数据类型

let str = ref(string)
let bool = ref(false)
let und = ref(undefined)
let nul = ref(null)
let arr = ref([])
let obj = ref({name: '小航哥'})

注意:在模板中,ref的数据,会被自动解包,直接使用,不需要.value

4. reactive函数

该函数用来定义对象,是响应式数据

let obj = reactive({
    name: '小航哥',
    age: 21
})

明确一个变量是对象就这样定义。有时候我们觉得不确定是什么数据类型,就用ref来定义

let num = ref(null)

reactive定义的变量,修改不需要通过.value的形式

let obj = reactive({
    name: '小航哥',
    age: 21
})
obj.name = '小帅哥' // 这样可以直接修改

5. toRefs函数

如果解构reactive函数里面的属性,会导致变量失去响应式,使用toRefs函数能够恢复响应式

let obj = reactive({
  name: '123',
  age: 19
})
const { name } = obj // 解构name变量,放到template里面去,

假设我们用一个按钮,去修改obj的值,发现会修改失败

{{ name }} <button @click="obj.name = '456'">点击修改obj.name</button>

修改后

let obj = reactive({
  name: '123',
  age: 19
})
+const { name } = toRefs(obj) // 这样修改后就好了

使用ref定义的对象,解构,也会失去响应式,用toRefs,也能够恢复响应式

let obj = ref({ // 用的是ref
  name: '123',
  age: 19
})
const { name } = toRefs(obj.value)

6. toRef函数

把对象里面的单个属性拎出来,包装成为ref响应式数据,但是这个数据修改后,视图不会更新。就不是响应式数据了

父组件定义了

<script setup lang="ts">
const name1 = '我是name1'
const obj = {
  age: 456,
  name: '1234'
}
</script>

<template>
  <main>
    <TheWelcome />
+    <SonView :name="name1" :obj="obj"></SonView>
  </main>
</template>

子组件接受

<script setup lang="ts">
import { toRef } from 'vue';

const props = defineProps<{
  name: string
  obj: {name: string, age:number}
}>()
+const age = toRef(props.obj, 'age')
console.log(age.value, 'age');
const changeAge = () => {
+  age.value = 555
  console.log(age.value, 'age');
}
</script>

<template>
+  <div class="sonPage">son我是儿子组件{{ name }}---{{ age }}</div>
  <button @click="changeAge">点击修改name的值</button>
</template>

<style lang="scss" scoped></style>

当试图点击按钮,修改age的值的时候,值修改成功,但是页面上的age没能够成功修改

7.计算属性 computed

简单写法

const startIndex = computed(() => {
  return (currentPage.value - 1) * pageSize.value
}) // 开始的页码

复杂写法

<script setup>
import { ref, computed } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

const fullName = computed({
  // getter
  get() {
    return firstName.value + ' ' + lastName.value
  },
  // setter
  set(newValue) {
    // 注意:我们这里使用的是解构赋值语法
    [firstName.value, lastName.value] = newValue.split(' ')
  }
})
</script>

注意:

  1. 计算属性,返回的也是ref数据。模板中会自动解包,script中需要通过.value来访问

  2. 复杂写法,才可以,修改计算属性的值。

8. watch侦听器

侦听器可以侦听:

  1. ref数据
  2. getter函数
  3. 数组,里面有多个数据

侦听ref数据

const x = ref(0)
watch(x, (newVal, oldVal) => {
    console.log(newVal, oldVal)
})

侦听整个对象

const obj = reactive({
  count: 123
})
watch(obj, (newVal, oldVal) => {
  console.log(newVal, oldVal, newVal === oldVal) // newVal和oldVal是一样的值
})
obj.count = 456

如果是替换整个对象

let obj = reactive({
  count: 123
})
watch(
  obj,
  (newVal, oldVal) => {
    console.log(newVal, oldVal, newVal === oldVal)
  },
  {
    immediate: true // 需要开启深度监听,会监听里面的所有的嵌套属性,开销也许会很大
  }
)
obj = reactive({
  count: 456 // 把整个对象都替换了
})

侦听getter函数

const obj = reactive({
    count: 0
})
// 错误写法
watch(obj.count, (newVal, oldVal) => {
    console.log(newVal, oldVal)
})
// 正确写法 -> 这样写能够返回一个getter函数
watch(() => obj.count, (newVal, oldVal) => {
    console.log(newVal, oldVal)
})

侦听多个数据源

const x = ref(0)
const obj = reactive({
    count: 0
})
// 正确写法 -> 这样写能够返回一个getter函数
watch(() => [x, obj.count], (newVal, oldVal) => {
    console.log(newVal, oldVal)
})

2. TypeScript的基础知识

如果是vue项目,记得插件安装: TypeScript Vue Plugin

2.0 为什么要用TypeScript

  • 能够提供很多的类型提示(具体后面会展开),写代码更加的高效
  • 先编译,后执行。编译阶段,写代码时就能减少很多错误,进而减少排错的成本。Uncaught TypeError这个错误,能够极大程度避免

2.1 简单数据类型

声明类型,采用格式:变量名:类型名的方式来声明

2.1.1 数字number

let num:number
num = 1

// 或者
let num:number = 1

2.1.2 字符串string

let str:string
str = '123'
// 或者
let str:string = '123'

2.1.3 布尔值boolean

let bool:boolean = true

2.1.4 undefined

let und:undefined = undefined // 就只能赋值为undefined 不能赋值为其它值

2.1.5 null

let na:null = null // 只能赋值为null

number,string,boolean,undefined,NaN跟在变量名的后面,表示类型,必须是小写

2.2 联合类型

如果我们希望一个变量,既可以是字符串,也可以是数字

let num:number | string = '1'
num = 2

image.png

2.3 数组的声明方式

第一种

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

这种方式,数组里面只能是数字,不能是字符串

image.png

如果希望数组里面既可以是字符串,也可以是数字,或者其它,依然可以用联合类型

let arr: (number | string)[] = [1, 2, 3, '4']

注意,可别写成这样啦

let arr: number | string[] = [1, 2, 3, '4'] // 这样表示,数字或者是“字符串型的数组”

image.png

第二种方式

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

image.png

联合类型

let arr: Array<number | string> = [1, 2, 3, '123']

2.4 对象

对象的类型的格式就是{变量键: 变量值;变量键:变量值}

let obj: {name: string} = {
    name: '小航哥'
}

如果我把name的类型改为数字,就会报错

let obj: {name: number} = {
    name: '小航哥'
}

image.png

对象有多个属性,如果还有函数呢

let obj: { name: string; age: number; fn: Function } = {
  name: '123',
  age: 19,
  fn: () => {}
}

但是我们一般使用type关键字声明一个类型,再赋值给变量名

// type关键字 
// 变量名之间换行的时候可以没有标点符号
type objType = {
  name: string
  age: number
  fn: Function
}
let obj: objType = {
  name: '123',
  age: 19,
  fn: () => {}
}

2.5 type关键字实现继承

如果我们已经写了一个typeA,里面有很多类型,一个新的类型typeB有很多和typeA相似的,同时又有不一样的类型,简言之就是B包含A,可以使用继承

type typeA = {
  name: string
  age: number
  fn: Function
}
type typeB = typeA & {
  height: number
}
let obj: typeB = {
  name: '123',
  age: 19,
  fn: () => {},
  height: 180
}

当我们把鼠标经过typeB的时候会有提示 image.png

2.6 函数类型

函数类型,第一是要给函数的参数写类型,第二是要给函数的返回值类型。

2.6.1 箭头函数的写法

如下是,限制函数的num1和num2参数类型为number,同时限制它们的返回值必须是number类型

const fn = (num1:number, num2: number):number => {
  return num1 + num2 // 这里会有类型推断,num1和num2我们都限制为number,所以当我们返回num1 + num2的时候,会自动推断为number类型
}

上面的函数如果这样写

const fn = (num1:number, num2: number):number => {
  return 1 + '1'
}

显示如下:

image.png

2.6.2 函数声明的写法

function fn(num1:number, num2:number):number {
    return num1 + num2
}

2.6.3 函数的类型别名

type fnType = (num1: number, num2: number) => number // 定义一个类型
const fn:fnType = (num1, num2) => {
  return 2
} // 类型直接跟在函数名的后面

2.6.4 函数的可选参数

在参数名字后面加一个问号,就是可选参数,表示这个参数可以不传递。

const fn = (num1:number, num2?: number):number => {
  return 1 + '1'
}

可选参数必须在必选参数的后面,下面这样就是错误的

const fn = (num1?:number, num2: number):number => {
  return 1 + '1'
}

image.png

2.6.5 函数返回void和返回undefined

如果函数返回类型啥也没有限定,默认返回的是void,默认是返回任何值都可以

const fn = (num1:number, num2: number) => {
  return 1 + '1'
}

image.png

注意:在没有写ts的时候,我们的函数里面没有return任何值时,返回的就是undefined,但是在写ts的时候,我们不能给函数指定返回undefined,不然函数就只能返回undefiend

const fn = (num1: number, num2: number): undefined => {
  return 1 + '1'
}

image.png

2.7 泛型

如下泛型的写法,当我们传入number时,number给到T,num形参的值就是number类星星,返回值也是number类型,是string就会报错(如图) Fn(num:T):T

function Fn<T>(num: T): T {
  return num
}
Fn<number>(1)
Fn<number>(['123'])

image.png

我们可以传入复杂一点的类型:

type fnType = {
  name: string
  age: number
}
function Fn<T>(num: T): T {
  return num
}
Fn<fnType>({
  name: '123',
  age: 19
})

如果定义的类型里面没有height属性,就会出现报错, image.png

2.8 type关键字和interface的区别

1.type 可以基本的数据类型,interface不可以

2.type 可以定义联合类型,interface不可以

3.type 定义类型,继承用&,interface定义类型,继承用extends

2.9 类型推断和类型断言

含义:明确告诉,ts,当前变量是什么类型 => 我们比ts更加清楚,这个是什么的类型。

let a = document.querySelector('a')  // 使用这个选择器,我们能够明确
// 它的类型

鼠标经过a变量,有如下提示

image.png

但是,如果我给一个a标签添加了class值,通过getElementByClassName获取这个元素

image.png

最终鼠标经过a变量,类型就是,HTMLCollectionOf

image.png

类型断言

在封装一些通用的组件的时候,一个组件被很多页面使用,那么变量是一个对象的话,可能属性是不一样的,我们不能轻易的写死,这个时候,类型断言就非常重要的

使用as关键字,进行类型断言

let a = document.getElementsByClassName('target') as HTMLAnchorElement 
console.log(a)

2.10 组合式API如何写ts

2.10.1 ref

const name = ref<string>('123')

2.10.2 reactive

type objType = {
    name: string
}
const name = reactive<objType>({
    name: '123'
})

2.10.3 provide

import echarts from 'echarts'
provide('echarts', 'echarts')

子组件接受

const name = inject<echarts>('echarts')

2.11 axios如何封装ts

// 引入axios
// 配置基地址和超时时间
// 配置请求拦截器和相应拦截器
// 配置实例方法
// 导出

import axios, { type Method, type ResponseType } from 'axios'
import { ElMessage } from 'element-plus'
const instance = axios.create({
  baseURL:
    process.env.NODE_ENV === 'development'
      ? '/ss'
      : 'http://' + window.location.host,
  timeout: 30000
})
const language = localStorage.getItem('language')
instance.interceptors.request.use(
  (config) => {
    if (!language && config.headers) {
      // 如果本地存储没有记录语言,就用浏览器的默认习惯
      config.headers['Accept-Language'] = navigator.language
    } else if (config.headers) {
      config.headers['Accept-Language'] = language
    }
    return config
  },
  (err) => {
    return Promise.reject(err)
  }
)

instance.interceptors.response.use(
  (response) => {
    return response
  },
  (error) => {
    // 如果有status属性,判断是否是403 404 500 401的状态码,如果是,返回data.detail提示;
    // 如果没有,直接返回error.response属性
    if (error.response && error.response.status) {
      const { status } = error.response
      if (
        status === 403 ||
        status === 404 ||
        status === 500 ||
        status === 401
      ) {
        ElMessage({
          type: 'error',
          message: error.response.data.detail
        })
        return Promise.reject(error.response.data.detail)
      }
    } else if (error.response) {
      return Promise.reject(error)
    } else {
      return Promise.reject(error)
    }
  }
)

+type Data<T> = {
  data: T
  status: number
}
+const request = <T>(
  url: string,
  method: Method = 'GET',
  submitData?: object | string,
  type?: ResponseType,
  headers?: object
) => {
+  return instance.request<T, Data<T>>({
    url,
    method,
    [method.toLowerCase() === 'get' ? 'params' : 'data']: submitData,
    responseType: type,
    headers: headers
  })
}

export default request

T会在调用接口的时候传递进来,给到request函数,形成Data image.png

鼠标按住点击instance.request,进入到axios的index.d.ts文件,我们发现,泛型T是可以传递进来,最终影响返回值promise的值 image.png

封装接口的时候

// 获取摄像机
getCameraParams是请求的参数类型
CameraResponse是我们希望的返回值的类型
export const getCameraDataApi = (params: getCameraParams) => {
  return request<CameraResponse>('/xxx/xxx/xxx', 'get', params)
} 

2.12 枚举

某种场合下,参数只有数字,导致不是很语义化。

<template>
    <!-- 我们就不知道1是什么意思了 -->
    <div v-if="id === 1">
    </div>
</template>
export enum OrderType {
  // 问诊订单
  /** 待支付 */
  ConsultPay = 1,
  /** 待接诊 */
  ConsultWait = 2,
  /** 问诊中 */
  ConsultChat = 3,
  /** 问诊完成 */
  ConsultComplete = 4,
  /** 取消问诊 */
  ConsultCancel = 5,
  // 药品订单
  /** 待支付 */
  MedicinePay = 10,
  /** 待发货 */
  MedicineSend = 11,
  /** 待收货 */
  MedicineTake = 12,
  /** 已完成 */
  MedicineComplete = 13,
  /** 取消订单 */
  MedicineCancel = 14
}
<template>
    <!-- 我们就不知道1是什么意思了 -->
    <div v-if="id === OrderType.ConsultPay">
    </div>
</template>