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

科技   科技   2024-09-16 09:09   北京  
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<T> {  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<BaseResponse<LoginResponse>>} 返回一个包含登录响应数据的 Promise * @throws {Error} 请求失败时抛出错误 */export const login = async (params: LoginRequest): Promise<BaseResponse<LoginResponse>> => {  try {    const response: AxiosResponse<BaseResponse<LoginResponse>> = 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<string>('blogadmin');        const pass = ref<string>('blogadmin');
        // 定义用于显示 token 的变量        const token = ref<string | null>(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<LoginRequest>({    name: '',    pass: '',});const loading = ref(false);const errorMessage = ref<string | null>(null);
/** * 登录表单提交处理函数 */const onSubmit = async () => {    loading.value = true;    errorMessage.value = null;
    try {        const response: BaseResponse<LoginResponse> = 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。此外,它封装了持久化逻辑,使代码结构清晰,易于维护和扩展。

调整登录逻辑



<script setup lang="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<LoginRequest>({    name: '',    pass: '',});const loading = ref(false);const errorMessage = ref<string | null>(null);
/** * 登录表单提交处理函数 */const onSubmit = async () => {    loading.value = true;    errorMessage.value = null;
    try {        const response: BaseResponse<LoginResponse> = 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;    }};</script>


然后在完善登录页:

<template>  <div class="about">    <h1>This is an about page</h1>    <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实例,实现拦截器用法。

BCVP代码创新社
专注于 NetCore 相关技术栈的推广,致力于前后端之间的完全分离,从壹开始,让每一个程序员都能从这里学有所成。
 最新文章