Vue3——对TypeScript的支撑

1、创建支撑TypeScript的Vue项目

Vue3框架本身就是基于TypeScript开发的,它对于TypeScript非常友好。而且TypeScript有强大的类型约束能力,可以将项目开发中出现的编译与逻辑错误大幅减少,因此在Vue项目中结合使用TypeScript可以为开发者提供极大的便利。特别是随着项目的业务功能越来越丰富,逐步从小型功能系统发展成中大型项目时,需要进行团队化开发,在Vue项目中使用TypeScript则可以增强开发约束、优化代码结构、强化后期维护。同时因为TypeScript具有类型约束、语法检查、自动友好提示等优点,所以使用TypeScript不仅可以降低项目的复杂度,还可以让开发者形成语法习惯,从而提高项目的开发速度。

想要在Vue3中得到TypeScript的支撑,项目在创建时采用的模板类型就不能是Vue,而是需要切换成vue-ts。下面创建一个以TypeScript语法结构为基础的Vue3项目,代码如下。

npm create vite@latest vue3-book-typescript -- --template vue-ts 

此时将创建一个名称为“vue3-book-typescript”​,以“vue-ts”模板为基础的项目,并且该项目中的语法结构将使用TypeScript。

在项目创建后,会发现许多文件的后缀都从原来的“.js”变成了“.ts”​,包括项目的配置文件vite.config.ts、入口文件main.ts,并且会多出相应的描述类型文件vite-env.d.ts。读者可以自行观察它们与之前的“.js”文件是否存在语法区别,此时文件结构如图所示。

在这里插入图片描述

首先在观察配置文件vite.config.ts中的配置代码后,可以发现该文件除了修改了后缀,初始配置的代码与原来“.js”文件的内容完全一致,没有发生任何变化。

vite.config.ts文件代码如下。

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vite.dev/config/
export default defineConfig({
  plugins: [vue()],
})

然后比较入口文件main.ts和main.js,初始配置的代码也没有任何差异。

main.ts文件代码如下。

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

createApp(App).mount('#app')

接着比较根组件文件App.vue,这里可以发现该文件发生了一些变化,主要集中在script部分。在

App.vue文件代码如下。

<script setup lang="ts">
import HelloWorld from './components/HelloWorld.vue'
</script>

<template>
  <HelloWorld />
</template>

2、Vue3组合函数对TypeScript的支撑

2.1、reactive对TypeScript的支撑

当我们使用reactive函数定义响应式数据时,如果没有通过泛型来指定目标对象的类型,Vue3就会根据reactive函数调用传入的参数,也就是目标对象的初始值来给目标对象推断一个对应的类型,如下面代码所示。

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

  const person = reactive({
    name: 'tom',
    age: 0
  })
</script>

此时目标对象的类型就会被推断为包含string类型的name属性和包含number类型的age属性的对象。如果我们要更新name或age属性,就必须指定为其对应类型的值,否则VSCode编辑器会直接提示类型错误。

  //下面时更新的正确写法
  person.name = 'Jack'
  person.age = 3

  //下面是更新的错误写法
  person.name = 3
  person.age = 'Jack'

此时就可以利用泛型来进行精准约束,代码如下。

  const person = reactive<{
    name: string;
    age: number;
  }>({
    name: 'tom',
    age: 0
  })

上面的代码在reactive函数调用时,在函数名右侧尖括号中通过大括号来包裹对象中的属性名和对应的属性值来进行类型约束。这样无论是初始值,还是更新后的值都有了类型约束,也就是name属性值必须是string类型的,age属性值必须是number类型的。如果name或age属性的初始值类型不正确,VSCode编辑器就会直接给出错误提示。比如下面的代码就是错误的。

  const person = reactive<{
    name: string;
    age: number;
  }>({
    name: 0, //name属性的初始值只能是string类型
    age: 'Tom' //age属性的初始值只能是number类型的
  })

在指定泛型约束时,除了直接在尖括号中通过大括号来进行类型约束,还可以利用TypeScript的接口来进行类型约束,代码如下。

  //定义接口
  interface PersonState {
    name: string;
    age: number;
  }

  //使用接口来指定泛型的具体类型
  const person = reactive<PersonState>({
    name: 'tom',
    age: 0
  })

在上面的代码中,首先定义接口来指定属性值类型,然后在泛型的尖括号中通过接口名来约束目标对象的类型。

这种方式的编码要麻烦一些,但它的好处是定义的接口可以被多次复用。如果我们再定义一个具有同样结构的reactive对象,就可以直接通过接口名来进行泛型约束,而不用重复定义,来看下面的代码。

  //定义一个新的包含name和age属性的reactive响应式对象
  const person2 = reactive<PersonState>({
    name: 'Jack',
    age: 3
  })

2.2、ref对TypeScript的支撑

ref与reactive函数类似,Vue3对于ref对象内部的value值也有类型约束。value值的类型默认是通过ref函数调用传入的初始值来推断的,但用得更多的是通过函数调用的泛型来指定value值的类型。

比如,现在想要定义两个ref对象:一个是数量,初始值为1;另一个是名称的列表,初始值为空列表。如果不使用泛型,就会写出下方代码。

  const count = ref(1)
  const names = ref([])

此时count的value值被推断为number类型,names值的value被推断为never[​]类型(never[​]类型表示那些永不存在的值的类型。因为初始值是空数组,所以无法推断出元素的类型)​。而根据我们的需求,names应该是string类型的数组,因此第2个类型推断是不能满足我们的要求的,此时就必须使用泛型来明确指定value值的类型。比如下方代码。

  const count = ref<number>(1) //指定value值的类型必须是number
  const names = ref<string[]>([]) //指定value值的类型必须时string[]

这样无论是ref函数传入的value初始值参数,还是将来更新后的value值,其类型都必须是泛型指定的对应类型。

当ref对象中的value是对象时,既可以直接在泛型尖括号中指定各个属性值的类型,也可以在利用接口定义类型约束后,在泛型尖括号中使用定义好的类型接口。这一特点其实与reactive函数是非常类似的,比如现在想把count和names使用一个ref函数定义,就可以写出下方代码。

  1. 不使用接口的方式。
  const refData = ref<{
    count: number;
    names: string[];
  }>({
    count: 1,
    names: []
  })
  1. 使用接口的方式。
  interface RefData {
    count: number;
    names: string[];
  }

  const refData = ref<RefData>({
    count: 1,
    names: []
  })

当更新count或names时,不管使用上面哪种编码方式,效果都是相同的,代码如下。

refData.value.count += 2;
refData.value.names.push('tom');

ref函数除了用于定义响应式数据,还可以用于保存组件内部的HTML标签对象或子组件对象,那怎么对其进行类型约束呢?下面分两种情况进行相关讲解。

(1)组件内部的HTML标签对象

比如,在显示输入框时其自动获取焦点,下面通过定义测试组件文件components/ChildHtml.vue进行代码实现。

<template>
    <h3>输入框自动获取焦点</h3>
    <input type="text" ref="inputRef">
</template>

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

    //用于保存input标签的ref对象
    const inputRef = ref()
    //在组件挂载后,让输入框获取焦点
    onMounted(()=>{
        inputRef.value.focus()
    })
</script>

当前并没有利用泛型来进行ref的类型约束,这就导致了一个问题:inputRef的value值被推断为any类型,通过inputRef.value得到input标签对象后,在通过“.”调用focus方法时不会出现任何补全提示,或者不小心写错了focus的名称,也不会有任何语法错误提示。此时就可以通过泛型来指定value值的类型为input标签的类型,从而得到focus方法调用的补全提示和语法错误提示。这里要注意的是,input标签的类型为HTMLInputElement,通过泛型来指定该类型可以写出下方代码。

    const inputRef = ref<HTMLElement>() //进行泛型约束

读者可能会发现,在“.”的左侧出现了一个“​?​”​,这个“​?​”是VSCode编辑器在补全时自动生成的。inputRef.value的值有可能是undefined,而“​?​”的意思是只有当value有值时,才会调用focus方法。

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

    //用于保存input标签的ref对象
    const inputRef = ref<HTMLElement>() //进行泛型约束
    //在组件挂载后,让输入框获取焦点
    onMounted(()=>{
        inputRef.value?.focus()
    })
</script>

(2)子组件对象。

通过ref函数可以得到任意子组件对象,后面就可以访问子组件通过defineExpose向外暴露的方法。下面定义一个简单的子组件文件components/ChildComp.vue,并通过defineExpose向外暴露fn方法,代码如下。

<template>
    <h3>ChildComp组件标题</h3>
</template>

<script setup lang="ts">
    const fn = () => {
        alert(`fn()`)
    }

    defineExpose({
        fn
    })
</script>

在App组件中可以使用ChildComp组件,并通过ref函数得到ChildComp组件的对象,之后就可以调用其暴露的fn方法了。

<script setup lang="ts">
  import { onMounted, ref } from 'vue';
  import ChildComp from './components/ChildComp.vue';

  const childRef = ref()
  onMounted(() => {
    childRef.value.fn()
  })
</script>

<template>
  <ChildComp ref="childRef"/>
</template>

在这里插入图片描述
此时出现的问题与组件内部的HTML标签对象代码演示出现的问题类似:在调用fn方法时,代码没有补全提示,方法名写错了也不会有语法错误提示。这是因为childRef的value值被默认推断为any类型。

其实我们可以添加针对ChildComp组件的类型约束,代码如下。

  const childRef = ref<InstanceType<typeof ChildComp>>()

上面代码中的泛型稍微有点复杂,用到了TypeScript的两个语法:第1个是typeof,可以得到某个组件的类型;第2个是InstanceType,可以得到指定组件类型的组件对象类型。对于当前代码来说,​“typeof ChildComp”得到了ChildComp组件的类型,可以通过“InstanceType<typeof ChildComp>”得到ChildComp组件对象的类型。这样当通过childRef.value调用其内部暴露的fn方法时,就会有补全提示或语法错误提示了,代码如下。

  onMounted(() => {
    childRef.value?.fn()
  })

上面代码中“​?​”的含义与组件内部的HTML标签对象中“​?​”的含义相同,这里不多做赘述。

2.3、computed对TypeScript的支撑

computed函数对TypeScript的支撑分为两种情况:如果不指定类型,则computed函数内部会根据计算属性函数返回值的类型推断出计算属性值的类型;反之,可以通过泛型精确地指定计算属性值的类型,这也意味着计算属性函数的返回值不能返回别的类型。

(1)不使用泛型,代码如下。

  import { computed, ref } from 'vue';

  const count = ref(1)
  //不使用泛型的写法
  const double = computed(()=>{
    return count.value * 2
  })

我们知道,computed函数返回的计算属性值本质是一个ref对象,所有计算属性值都是保存在double对象的value属性上的,而当前计算属性函数的返回值明显是number类型的,所有计算属性值(double的value值)也就被推断为number类型的。

(2)使用泛型。

方式(1)并不能对计算属性函数的返回值类型进行约束,如果想将其约束为只能返回number类型,就必须给computed函数指定泛型类型为number,代码如下。

  const double = computed<number>(()=>{
    return count.value * 2
  })

2.4、props对TypeScript的支撑

在Vue3中可以通过defineProps给组件定义接收的props数据的类型。请读者思考下方代码。

  //定义接收属性
  defineProps({
    msg: {
      type: [String, Number],
      required: false
    },
    setMsg: {
      type: Function,
      required: true 
    }
  })

上面代码直接使用Vue3本身约束的JavaScript写法,定义接收两个属性:一个是msg,它是可选的,但值必须是string或number类型,也可以不传递;另一个是setMsg,它是必需的,且必须是函数。

如果想约束setMsg函数接收的参数或返回值类型,比如限定接收的参数是字符串,且没有返回值,就只能使用泛型来约束props数据的类型了,实现代码如下。

  defineProps<{
    msg?: string | number;
    setMsg: (val: string) => void;
  }>();

当然也可以通过预先定义接口来实现,代码如下。

  interface Props {
    msg?: string | number;
    setMsg: (val: string) => void;
  }
  defineProps<Props>();

上面的代码通过“​?​”来约束msg属性是可选的,并通过联合类型来指定属性值是string或number类型中的一种,还通过函数类型来约束msg接收的参数为字符串,且没有返回值。至此我们可以看到:泛型的代码更简洁,功能也更强大。

如果需要指定属性默认值,泛型写法就需要使用一个全局函数withDefaults来实现。例如,给可选的msg属性指定默认值为“atguigu”​,实现代码如下。

  //指定属性默认值
  withDefaults(defineProps<Props>(), {msg: 'Tom'});

2.5、emits对TypeScript的支撑

一个组件如果要分发Vue自定义事件,则需要通过调用defineEmits函数来返回用于分发自定义事件的函数,示例代码如下。

  const emit = defineEmits(['increment', 'setMsg']);
  //分发事件,事件名是特定的,但传递的数据是任意的
  emit('increment', 3);
  emit('setMsg', 'Tom');

上方代码通过调用defineEmits函数,指定了包含两个事件名increment和setMsg的数组,这样就声明了可以分发increment和setMsg两个自定义事件,同时返回可以分发这两个事件的函数emit,后面就可以通过调用函数emit来分发increment或setMsg事件,并携带要传递的数据。

这种方式的缺点在于,没有约束要传递的数据的类型和必要性,如果想要约束它们,就需要通过泛型来实现,代码如下。

  const emit = defineEmits<{
    (e: 'increment', num: number): void;
    (e: 'setMsg', value?: string): void;
  }>();

上方代码通过泛型中的函数类型声明来约束自定义事件的事件名和传递数据的类型及其必要性。函数的第1个参数指定的是事件名,第2个参数指定的是传递数据的类型,并在其中通过“​?​”来指定数据是可选的。

分发事件的代码如下。

  //分发事件的正确写法
  emit('increment', 3);
  emit('setMsg', 'Tom');

3、vue-router对TypeScript的支撑

vue-router从V4版本开始,全面支持TypeScript语法。其实整个vue-router需要进行TypeScript类型约束的地方并不多,主要集中在路由的配置和自定义扩展配置的约束上。在src目录下新建一些文件,如图所示。

在这里插入图片描述

将Home.vue和About.vue定义成最简单的路由组件,如下所示。

views/Home.vue文件代码如下。

<template>
    <h1>Home页</h1>
</template>

<script setup lang="ts">
</script>

views/About.vue文件代码如下。

<template>
    <h1>About页</h1>
</template>

<script setup lang="ts">
</script>

在router/index.ts文件中创建路由器,注册路由,代码如下。

import {createRouter, createWebHashHistory} from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'

//定义路由表
const routes = [
    {
        name: 'Home',
        path: '/',
        component: Home
    },
    {
        name: 'About',
        path: '/about',
        component: About
    }
]

//创建路由
const router = createRouter({
    history: createWebHashHistory(),
    routes
})

export default router;

在App.vue文件中指定路由视图来显示路由组件页面。

<script setup lang="ts">

</script>

<template>
  <div>
    <router-link to="/about">去About页</router-link>
    &nbsp;
    <router-link to="/">去Home页</router-link>
    <hr/>
    <router-view></router-view>
  </div>
</template>

在main.ts文件中注册路由器,代码如下。

import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router'
createApp(App).use(router).mount('#app')

在这里插入图片描述

此时已经实现简单的路由页面切换效果,但路由表中路由配置的属性并没有被约束。也就是说,在编写配置时,缺少精准的补全提示。vue-router提供了路由配置对象的接口类型RouteRecordRaw,可以给路由表routes指定类型为RouteRecordRaw[​]​,这样在编写name、page、component、meta等配置时,就会有非常精准的补全提示,也避免了配置名称写错的情况。

此时修改后的router/index.ts文件代码如下。

import {createRouter, createWebHashHistory} from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'

//定义路由表
const routes: RouteRecordRaw[] = [
    {
        name: 'Home',
        path: '/',
        component: Home
    },
    {
        name: 'About',
        path: '/about',
        component: About
    }
]

//创建路由
const router = createRouter({
    history: createWebHashHistory(),
    routes
})

export default router;

但这里有一个问题需要解决,具体如下。

RouteRecordRaw只是约束了meta属性是一个对象,并没有约束这个对象中的属性,应该如何约束meta属性对象中的属性呢?

要想解决这个问题,可以在项目src目录下创建一个扩展vue-router的类型声明文件src/vue-router.d.ts,并在其中编写下方代码。

import 'vue-router'

declare module 'vue-router' {
    //扩展路由配置的meta属性对象中的自定义属性,也就是在写meta属性对象中
    //的属性时会有补全提示
    interface _RouteRecordBase {
        hidden: boolean;
    }
    //扩展路由对象的meta属性对象中的自定义属性,也就是在写route.meta.时会有补全提示
    interface RouteMeta {
        hidden: boolean
    }
}

有了上面的配置,在配置路由表编写路由配置的meta配置对象中的属性时,就有了hidden属性的补全提示,代码如下。

    {
        name: 'Home',
        path: '/',
        component: Home,
        meta: {
            hidden: true
        }
    },

此时在About组件中读取meta属性对象中的属性,也有了hidden属性的补全提示,代码如下。

<template>
    <h1>About页</h1>
</template>

<script setup lang="ts">
    import {useRoute} from 'vue-router'

    const route = useRoute()
    //读取meta属性对象中的hidden属性时会有补全提示
    const hidden = route.meta.hidden
    console.log(hidden)
</script>

4、Pinia对TypeScript的支撑

在Pinia中应用TypeScript主要是对state中的数据、getters中的计算属性和actions中方法的参数进行类型约束。这样当进行Pinia中某个store的相关编码时,比如在action方法中和在getter计算属性中操作状态数据,就会有类型约束。不仅如此,在组件中读取Pinia管理的状态数据或getters数据,以及在组件中调用某个store的action方法传递参数时也会有类型约束。组件与Pinia的交互过程如图所示。

在这里插入图片描述
下面以一个简单的TodoList为例来说明Pinia与TypeScript配合使用的方法。

需要由Pinia管理的state是todo对象的列表,因此需要对todo对象列表的结构进行TypeScript的类型约束定义,当然也需要对state对象的结构进行类型约束定义。

首先在src目录下创建stores子目录,然后在其中创建类型约束文件types.ts,stores/types.ts文件代码如下。

//todo 对象的类型
export interface Todo {
    id?: number;
    title: string;
    completed: boolean;
}

//todo对象列表的类型别名
export type TodoList = Todo[];

//todos state对象的类型
export interface TodosState {
    todos: TodoList;
}

在stores目录下创建管理todo的Pinia模块文件todos.ts文件,stores/todos.ts文件代码如下。

import type {Todo, TodosState} from './types'
import {defineStore} from 'pinia'

const useTodosStore = defineStore('todos', {
    //定义返回初始化state对象函数
    //指定返回类型为TodosState类型
    state: (): TodosState => ({
        todos: [],
    }),
    actions: {
        //根据传入的title添加一个新的todo
        //指定传入的title为string类型
        addTodo(title: string) {
            const todo: Todo = {
                id: Date.now(),
                title: title,
                completed: false
            }
            this.todos.unshift(todo)
        },

        //切换指定id的todo的完成状态
        //指定传入的id为number类型
        toggleTodo(id: number) {
            this.todos.some((todo) => {
                if(todo.id === id) {
                    todo.completed = !todo.completed
                    return true
                }
                return false
            })
        },

        //删除指定id的todo
        //指定传入的id为number类型
        deleteTodo(id: number) {
            this.todos = this.todos.filter((todo) => todo.id !== id)
        },

        //删除所有已经完成的todo
        deleteCompleteTodos() {
            this.todos = this.todos.filter((todo) => !todo.completed)
        }
    },
    getters: {
        //完成的数量
        //指定返回值类型为number类型
        completedSize(): number {
            return this.todos.reduce(
                (pre, todo) => pre + (todo.completed ? 1 : 0),
                0
            )
        }
    }
})

export default useTodosStore

在上面的代码中,首先给返回state对象的箭头函数指定返回值类型为TodosState类型,然后给action方法中传递的参数定义对应的类型,最后给getters中的计算属性指定返回值类型。

在App.vue文件中利用Pinia进行todos数据管理,代码如下。

<template>
  <div>
    <input type="text" 
      v-model="title" 
      placeholder="按Enter键添加"
      @keyup.enter="addTodo">
      <ul>
        <li v-for="todo in todosStore.todos" :key="todo.id">
          <input type="checkbox" @click="toggleTodo(todo.id!)"/>
          <span style="display: inline-block; width: 200px;">
            {{ todo.title }}
          </span>
          <span @click="deleteTodo(todo.id!)">X</span>
        </li>
      </ul>
      <div>
        完成数量:{{ useTodosStore.completedSize }}
        <button @click="deleteAllCompleted">删除所有完成的</button>
      </div>
  </div>
</template>

<script lang="ts">
  export default {
    name: 'Todos'
  }
</script>

<script setup lang="ts">
  import {ref} from 'vue'
  import useTodosStore from './stores/todos'

  const todosStore = useTodosStore()
  const title = ref('')

  //添加todo
  const addTodo = () => {
    todosStore.addTodo(title.value)
    title.value = ""
  }

  //切换指定id的todo的完成状态
  const toggleTodo = (id: number) => {
    todosStore.toggleTodo(id)
  }

  //删除指定id的tudo
  const deleteTodo = (id: number) => {
    todosStore.deleteTodo(id)
  }

  //删除所有已完成的todo
  const deleteAllCompleted = () => {
    todosStore.deleteCompleteTodos()
  }
</script>

main.ts文件代码如下。

import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router'
import {createPinia} from 'pinia'

const pinia = createPinia()
createApp(App).use(pinia).use(router).mount('#app')

运行代码,测试项目的功能效果。

在这里插入图片描述

5、axios库对TypeScript的支撑

axios库对于TypeScript的支撑主要体现在对请求参数和响应体数据的类型约束上,实现的途径是通过泛型来约束。axios库本身提供了一些接口来约束与axios库相关的一些对象的结构,比如请求配置对象、响应对象等。

下面简单地介绍两个axios库内置的重要类型接口,具体如下。

(1)请求配置对象对应的是AxiosRequestConfig接口,如图所示。
在这里插入图片描述
AxiosRequestConfig接口的主要属性如表所示。

属性名类型说明
urlstring请求地址
methodstring或各种请求方式名称请求方式
baseURLstring基础路径
headerskey(string)与value(string/number/boolean)组成的对象请求头
params任意类型query参数
data接口的泛型类型D,默认为任意类型请求体参数
timeoutnumber超时时间

(2)响应对象对应的是AxiosResponse接口,如图10-8所示。

在这里插入图片描述

AxiosResponse接口的主要属性如表所示。

属性名类型说明
data接口的第一个泛型类型,默认为任意类型响应体数据
statusnumber响应状态码
statusTextstring状态提示文本
headerskey(string)与value(string)组成的对象响应头

同时,axios库为发送请求的静态方法提供了3个泛型,通过它们可以来约束请求体参数和响应体数据结构,下面展示4个常用静态方法的泛型,如图所示。

在这里插入图片描述

从图中可以看出,这4个常用静态方法都可以依次接收T、R和D这3个泛型,下面对这3个泛型分别进行介绍。

  • 泛型T:T传递给了AxiosResponse的泛型,用来指定响应体数据data的类型,默认为any类型。
  • 泛型R:默认是响应对象AxiosResponse类型,但如果响应拦截器成功回调返回其他值,比如返回response.data,R的类型就是响应体数据的类型。也就是说,R对应的是响应拦截器成功回调返回值的类型,而默认的返回值类型就是响应对象,所以R默认是响应对象AxiosResponse类型。
  • 泛型D:默认是any类型,D传递给了AxiosRequestConfig的泛型,用来指定请求体数据data的类型。

在项目开发中通过axios库发送请求时,就可以利用泛型来约束请求体参数和响应体数据结构。

下面通过代码进行演示,使用json-server快速搭建一个REST API(REST是一种接口风格)​,整个过程分为3步。

(1)在终端中执行下方命令,全局安装json-server。

npm i -g json-server

(2)在项目根目录下创建db.json文件,并指定下方数据。

{
    "students": [
        {
            "id": 1,
            "username": "Tom",
            "age": 23,
            "salary": 15000
        },
        {
            "id": 2,
            "username": "Jack",
            "age": 24,
            "salary": 14000
        },
        {
            "id": 3,
            "username": "Bob",
            "age": 22,
            "salary": 12000
        }
    ]
}

数据说明:students代表参加学习的多名毕业学生,每名学生都有id(唯一标识)​、username(姓名)​、age(年龄)和salary(每月薪资)4个属性。

(3)在终端中执行下方命令,在项目根目录下启动json-server服务器。

json-server --watch db.json

此时可以通过“http://localhost:3000/students”来发送请求,请求的响应体数据如图所示。

在这里插入图片描述
通过“http://localhost:3000/students”接口,我们可以发送POST、PUT、DELETE和GET请求来实现对students数据的增、删、改、查操作。

我们需要为响应体数据定义类型接口,在main.ts文件中添加下方代码。

//学生的类型接口
export interface Student {
    id: number;
    username: string;
    age: number;
    salary: number;
}
//学生列表的类型别名
export type StudentList = Student[];

在App.vue文件中直接通过axios库请求此接口,此时App.vue文件代码如下。

<template>
  <h1>首页</h1>
  <button @click="getStudentList">查询学生列表</button>
  <button @click="addStudent">添加学生</button>
  <button @click="updateStudent">更新学生</button>
  <button @click="deleteStudent">删除学生</button>
</template>

<script setup lang="ts">
import axios from 'axios'

//学生的类型接口
export interface Student {
    id?: number;
    username: string;
    age: number;
    salary: number;
}
//学生列表的类型别名
export type StudentList = Student[];
//指定基础路由
axios.defaults.baseURL = 'http://localhost:3000';

//请求获取学生列表
const getStudentList = async () => {
  //发送GET请求获取所有学生列表
  //通过泛型指定响应体数据的类型为StudentList类型
  const response = await axios.get<StudentList>('/students');
  //响应对象的data的类型就是StudentList类型
  const studentList = response.data;
  console.log(studentList);
}

//请求添加一名学生
const addStudent = async () => {
  //创建一个不包含id的新的学生对象
  const s = {
    username: 'AAA',
    age: 23,
    salary: 15000
  }
  //发送POST请求添加学生
  //通过泛型指定返回的响应体数据为Student
  const response = await axios.post<Student>('/students', s);
  //得到请求返回的响应体数据:新添加的学生对象(包含id)
  const student = response.data;
  console.log(student);
}

//更新学生
const updateStudent = async () => {
  //创建一个带id的学生对象(假设它是接口后台当前存在的对象)
  const s = {
    id: 2,
    username: 'BBB',
    age: 23,
    salary: 15000
  }
  const response = await axios.put<Student>(`/students/${s.id}`, s);
  const student = response.data;
  console.log(student);
}

//删除学生
const deleteStudent = async () => {
  const id = 2;
  const response = await axios.delete(`/students/${id}`);
  console.log(response.data);
}
</script>

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值