放假不停歇,趁着假期学习下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,你可以通过以下命令进行安装:
在项目的 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
LoginResponse:具体响应类,用于表示登录成功时的响应体结构。
Axios 封装:
使用 axios.get 发起 GET 请求,并通过 params 传递请求参数。
捕获请求错误并抛出异常。
从语法上来看,简直就和c#是一模一样的,毕竟ts的作者和c#是一个人。
接下来,在 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/, ''), // 重写路径
},
}
}
})
可以看到登录成功:
完善登录逻辑,把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实例,实现拦截器用法。