BCVP.VUE3系列第二课:基于泛型基类封装Axios请求

艺帆风顺 发布于 2025-04-03 20 次阅读


BCVP 开发者社区出品BCVP V3开发数字化服务化绿色化

放假不停歇,趁着假期学习下VUE3相关的内容,一方面是自己保持活力,另一方面也是工作需要,本系列是我的自学教程,如果有从0开始学习VUE3的,可以跟着一起练习下,毕竟前端我也是泥腿子出身,这一系列会使用Vite、TS、Pinia、Element-Plus等新知识点,既是查漏补缺,也是知识分享。

代码地址:

https://github.com/anjoy8/bcvp.vue3.git

这是每篇文章一节课一个分支,方便大家学习,会慢慢的将blog.admin项目进行翻新,使用的后端接口还是BlogCore。

系列文章:

BCVP.VUE3系列第一课:项目初始化与核心知识点说明

0、本文介绍

通常,我们开发是肯定离不开一个话题——就是使用axios发起接口请求,

下面我将提供一个完整的 Vue3 + TypeScript 项目示例,展示如何使用 axios 调用接口,并在页面上显示获取到的 token。我们将定义三个类,分别用于请求参数、基础响应类以及具体响应类,并严格遵循 TypeScript 的规范。

首先就需要安装依赖,确保你已经安装了 Vue3 和 TypeScript,并安装了 axios,你可以通过以下命令进行安装:

npm install axios

1、定义api请求类

在项目的 src 目录下,创建一个 api 文件夹,然后在该文件夹中创建loginApi.ts文件,用于封装请求逻辑,定义了完整的接口请求和响应类,可以理解是后端我们平时开发的Entity、DTO、VO、POJO,定义好后,还是很清晰的,如果怕麻烦这个就是下一个话题了,我个人还是建议如果多人团队开发,该有的模型还是需要有的:

    import axios from 'axios';import type { AxiosResponse } from 'axios';
    /** * 请求的入参接口 * @interface LoginRequest * @property {string} name - 用户名 * @property {string} pass - 密码 */export interface LoginRequest {  name: string;  pass: string;}
    /** * 基础响应接口,使用泛型 T 来表示响应体 * @template T * @interface BaseResponse * @property {number} status - HTTP 响应状态码 * @property {boolean} success - 请求是否成功 * @property {string} msg - 响应的消息 * @property {string | null} [msgDev] - 开发用的详细信息,可能为空 * @property {T} response - 具体的响应数据 */export interface BaseResponse {  status: number;  success: boolean;  msg: string;  msgDev?: string | null;  response: T;}
    /** * 登录响应接口 * @interface LoginResponse * @property {boolean} success - 是否登录成功 * @property {string} token - JWT token * @property {number} expires_in - token 的有效时长(秒) * @property {string} token_type - token 类型,通常为 "Bearer" */export interface LoginResponse {  success: boolean;  token: string;  expires_in: number;  token_type: string;}

    /** * 发起登录请求 * @function login * @param {LoginRequest} params - 登录请求的参数 * @returns {Promise>} 返回一个包含登录响应数据的 Promise * @throws {Error} 请求失败时抛出错误 */export const login = async (params: LoginRequest): Promise> => {  try {    const response: AxiosResponse> = await axios.get('http://localhost:9291/api/Login/JWTToken3.0', {      params: {        name: params.name,        pass: params.pass,      },    });    return response.data;  } catch (error) {    throw new Error('请求失败');  }};

    TypeScript 类定义:

    LoginRequest:用于封装接口的请求参数。

    BaseResponse:基础响应类,使用泛型 T 表示具体的响应体。

    LoginResponse:具体响应类,用于表示登录成功时的响应体结构。

    Axios 封装:

    使用 axios.get 发起 GET 请求,并通过 params 传递请求参数。

    捕获请求错误并抛出异常。

    从语法上来看,简直就和c#是一模一样的,毕竟ts的作者和c#是一个人。

    2、在页面内调用

    接下来,在 src/views 目录下创建一个 Login.vue 文件,这个组件将调用登录接口,并在页面上展示 token

      template>    div>        h1>登录页面h1>        div>            label for="name">用户名:label>            input v-model="name" id="name" type="text" placeholder="请输入用户名" />        div>        div>            label for="pass">密码:label>            input v-model="pass" id="pass" type="password" placeholder="请输入密码" />        div>        button @click="handleLogin">登录button>
              div v-if="token">            h2>Token:h2>            p>{{ token }}p>        div>    div>template>
      script lang="ts">import { defineComponent, ref } from 'vue';import { login } from '@/api/loginApi';import type { LoginRequest } from '@/api/loginApi';
      export default defineComponent({    name: 'Login',    setup() {        // 定义两个响应式变量,用于用户名和密码输入        const name = ref('blogadmin');        const pass = ref('blogadmin');
              // 定义用于显示 token 的变量        const token = refnull>(null);
              // 登录请求函数        const handleLogin = async () => {            try {                const params: LoginRequest = {                    name: name.value,                    pass: pass.value,                };
                      // 发起请求并处理响应                const response = await login(params);                if (response.success && response.response.token) {                    token.value = response.response.token;                } else {                    alert('登录失败: ' + response.msg);                }            } catch (error) {                console.error('请求错误:', error);                alert('请求失败');            }        };
              return {            name,            pass,            token,            handleLogin,        };    },});script>
      style scoped>div {    margin-bottom: 20px;}
      button {    margin-top: 10px;}style>

      这种写法对于习惯vue2的开发来说,还是不太舒服,直接用setup的语法糖更好理解一些:

        template>    div class="login">        h1>登录h1>        form @submit.prevent="onSubmit">            div>                label for="name">用户名label>                input v-model="loginForm.name" id="name" type="text" required />            div>            div>                label for="pass">密码label>                input v-model="loginForm.pass" id="pass" type="password" required />            div>            button type="submit" :disabled="loading">                {{ loading ? '登录中...' : '登录' }}            button>        form>        p v-if="errorMessage" class="error">{{ errorMessage }}p>    div>template>

        script setup lang="ts">import { ref } from 'vue';import { useRouter } from 'vue-router';import { login } from '@/api/loginApi';import type { LoginRequest, BaseResponse, LoginResponse } from '@/api/loginApi';
        const router = useRouter();const loginForm = ref({    name: '',    pass: '',});const loading = ref(false);const errorMessage = refnull>(null);
        /** * 登录表单提交处理函数 */const onSubmit = async () => {    loading.value = true;    errorMessage.value = null;
            try {        const response: BaseResponse = await login(loginForm.value);
                if (response.success) {            // 登录成功,跳转到首页或者其他页面            router.push({ name: 'Home' });        } else {            // 登录失败,显示错误信息            errorMessage.value = response.msg;        }    } catch (error) {        // 请求错误处理        errorMessage.value = '登录失败,请重试';    } finally {        loading.value = false;    }};script>
        style scoped>.login {    max-width: 400px;    margin: 0 auto;    padding: 1rem;}
        .error {    color: red;    margin-top: 1rem;}style>

        然后修改路由,访问页面:

        接下来就是配置跨域信息,可以使用绝对路径,后端BlogCore中配置CORS白名单:

        也可以在前端配置代理,用相对路径:

        在vite.config.ts中:

          import { fileURLToPath, URL } from 'node:url'import { defineConfig } from 'vite'import vue from '@vitejs/plugin-vue'import vueJsx from '@vitejs/plugin-vue-jsx'import vueDevTools from 'vite-plugin-vue-devtools'
          // https://vitejs.dev/config/export default defineConfig({  plugins: [    vue(),    vueJsx(),    vueDevTools(),  ],  resolve: {    alias: {      '@': fileURLToPath(new URL('./src', import.meta.url))    }  },  server: {    proxy: {      '/api': {        target: 'http://localhost:9291',  // 请替换为你的后端服务器地址        changeOrigin: true,  // 是否改变源        // rewrite: (path) => path.replace(/^/api/, ''),  // 重写路径      },    }  }})

          可以看到登录成功:

          3、使用Pinia存储token

          完善登录逻辑,把token存到pinia里,用localstorage持久化

          在store文件夹中,新增auth.ts。

            import { defineStore } from 'pinia';
            export const useAuthStore = defineStore('auth', {    state: () => ({        token: localStorage.getItem('token') || '',  // 初始化时从 localStorage 读取 token    }),    actions: {        setToken(newToken: string) {            this.token = newToken;            localStorage.setItem('token', newToken);  // 保存 token 到 localStorage        },        clearToken() {            this.token = '';            localStorage.removeItem('token');  // 清除 localStorage 中的 token        },    },});

            使用 Pinia结合

            localStorage可以实现响应式的数据绑定,使得状态更新时界面自动刷新,同时集中管理状态,避免在多个组件中手动操作

            localStorage。此外,它封装了持久化逻辑,使代码结构清晰,易于维护和扩展。

            调整登录逻辑



              "ts">import { ref } from 'vue';import { useRouter } from 'vue-router';import { login } from '@/api/loginApi';import { useAuthStore } from '@/stores/auth';import type { LoginRequest, BaseResponse, LoginResponse } from '@/api/loginApi';
              const router = useRouter();const authStore = useAuthStore();const loginForm = ref({    name: '',    pass: '',});const loading = ref(false);const errorMessage = refstring | null>(null);
              /** * 登录表单提交处理函数 */const onSubmit = async () => {    loading.value = true;    errorMessage.value = null;
                  try {        const response: BaseResponse = await login(loginForm.value);        if (response.success) {            // 保存 token 到 Pinia            authStore.setToken(response.response.token);            router.push({ name: 'about' });        } else {            // 登录失败,显示错误信息            errorMessage.value = response.msg;        }    } catch (error) {        // 请求错误处理        errorMessage.value = '登录失败,请重试';    } finally {        loading.value = false;    }};

              然后在完善登录页:

                template>  div class="about">    h1>This is an about pageh1>    br />    p v-if="token" class="token">Token: {{ token }}p>    p v-else>No token available.p>  div>template>
                script setup lang="ts">import { useAuthStore } from '@/stores/auth';
                const authStore = useAuthStore();const token = authStore.token;script>

                最终效果:

                目前还是一个半成品,下篇文章会继续封装axios实例,实现拦截器用法。