放假不停歇,趁着假期学习下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>