放假不停歇,趁着假期学习下VUE3相关的内容,一方面是自己保持活力,另一方面也是工作需要,本系列是我的自学教程,如果有从0开始学习VUE3的,可以跟着一起练习下,毕竟前端我也是泥腿子出身,这一系列会使用Vite、TS、Pinia、Element-Plus等新知识点,既是查漏补缺,也是知识分享。
代码地址:
https://github.com/anjoy8/bcvp.vue3.git
这是每篇文章一节课一个分支,方便大家学习,会慢慢的将blog.admin项目进行翻新,使用的后端接口还是BlogCore。
系列文章:
通常,我们开发是肯定离不开一个话题——就是使用axios发起接口请求,
下面我将提供一个完整的 Vue3 + TypeScript 项目示例,展示如何使用 axios 调用接口,并在页面上显示获取到的 token。我们将定义三个类,分别用于请求参数、基础响应类以及具体响应类,并严格遵循 TypeScript 的规范。
首先就需要安装依赖,确保你已经安装了 Vue3 和 TypeScript,并安装了 axios,你可以通过以下命令进行安装:
npm install axios

在项目的 src 目录下,创建一个 api 文件夹,然后在该文件夹中创建loginApi.ts文件,用于封装请求逻辑,定义了完整的接口请求和响应类,可以理解是后端我们平时开发的Entity、DTO、VO、POJO,定义好后,还是很清晰的,如果怕麻烦这个就是下一个话题了,我个人还是建议如果多人团队开发,该有的模型还是需要有的: TypeScript 类定义: LoginRequest:用于封装接口的请求参数。 BaseResponse:基础响应类,使用泛型 T 表示具体的响应体。 LoginResponse:具体响应类,用于表示登录成功时的响应体结构。 Axios 封装: 使用 axios.get 发起 GET 请求,并通过 params 传递请求参数。 捕获请求错误并抛出异常。 从语法上来看,简直就和c#是一模一样的,毕竟ts的作者和c#是一个人。 接下来,在 src/views 目录下创建一个 Login.vue 文件,这个组件将调用登录接口,并在页面上展示 token 这种写法对于习惯vue2的开发来说,还是不太舒服,直接用setup的语法糖更好理解一些: 然后修改路由,访问页面: 接下来就是配置跨域信息,可以使用绝对路径,后端BlogCore中配置CORS白名单: 也可以在前端配置代理,用相对路径: 在vite.config.ts中: 可以看到登录成功: 完善登录逻辑,把token存到pinia里,用localstorage持久化 在store文件夹中,新增auth.ts。 使用 Pinia结合 localStorage可以实现响应式的数据绑定,使得状态更新时界面自动刷新,同时集中管理状态,避免在多个组件中手动操作 localStorage。此外,它封装了持久化逻辑,使代码结构清晰,易于维护和扩展。 调整登录逻辑 然后在完善登录页: 最终效果: 目前还是一个半成品,下篇文章会继续封装axios实例,实现拦截器用法。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('请求失败'); }};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>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>


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/, ''), // 重写路径 }, } }})
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 }, },});"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>
