二:从零开始Vue3.2+TS+Pinia+ElementPlus后台管理系统(请求封装)

521 阅读5分钟
前言:

从搭建一个后台管理系统开始,通过这个过程来学习和熟悉 vue 3.2 + TypeScript 的使用。后续会持续更新内容,包括如何使用 Node.js 提供接口,以及如何选择服务器并进行部署上线。

项目部分功能与插件:

  • Vue3.2
  • vite
  • vue-router
  • TypeScript
  • Pinia
  • Axios
  • pinia-plugin-persistedstate (状态的持久化)
  • mitt (兄弟组件传值)
  • unplugin-vue-components (Vue 组件自动导入插件)
  • unplugin-auto-import (官方API 自动导入)
  • plugin-vue-jsx (JSX 插件)

一:封装loading

地址:src > config > loadingInstance.ts

作用:封装element-plus loading,使其可以全局调用

代码

import { ElLoading } from 'element-plus'
let loadingInstance: ReturnType<typeof ElLoading.service>;

const startLoading = () => {
    loadingInstance = ElLoading.service({
        lock: true,
        text: "Loading",
        fullscreen: true,
        background: "rgba(0, 0, 0, 0.7)"
    });
};

let count = 0;

export const showFullScreenLoading = () => {
    if (count === 0) startLoading();
    ++count;
};

export const hideFullScreenLoading = () => {
    if (count <= 0) return;
    --count;
    if (count === 0) loadingInstance.close();

};

解析

let loadingInstance: ReturnType<typeof ElLoading.service>;

通过 typeof ElLoading.service 获取到它的类型。然后使用 ReturnType 获取这个函数的返回值类型,也就是 ElLoading.service 函数的返回类型。

二:封装Axios

地址:src > request > http.ts

作用:封装使用 Axios发送HTTP请求的功能,并暴露了一些常用的请求方法

代码

import Cookie from 'js-cookie'
import { showFullScreenLoading, hideFullScreenLoading } from '@/config/loadingInstance'
import axios, { AxiosInstance, AxiosError, AxiosRequestConfig, InternalAxiosRequestConfig, AxiosResponse } from "axios";

// 接口:扩展了一个可选的 noLoading 属性,用于控制请求是否显示加载状态。
export interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig {
    noLoading?: boolean;
}
// 接口:响应的常规数据类型(响应码与响应提示)
interface Result {
    code: string;
    msg: string;
}
// 接口:返回的非常规数据类型,如数据/字符串/对象类型的数据,并继承常规数据类型。
interface ResultData<T = any> extends Result {
    data: T;
}
// 声明常量config,用于配置Axios实例的选项
const config = {
    timeout: 10000, // 设置请求超时时间,以毫秒为单位。默认值为 3000,表示请求将在 3000 毫秒后超时。
};

class RequestHttp {
    service: AxiosInstance;
    public constructor(config: AxiosRequestConfig) {
        this.service = axios.create(config);
        this.service.interceptors.request.use(
            (config: CustomAxiosRequestConfig) => {
                config.noLoading && showFullScreenLoading();
                if (config.headers && typeof config.headers.set === "function") {
                    config.headers.set("x-access-token", Cookie.get('token'));
                }
                return config;
            },
            (error: AxiosError) => {
                return Promise.reject(error);
            }
        );

        /**
         * @description 响应拦截器
         */
        this.service.interceptors.response.use(
            (response: AxiosResponse) => {
                const { data } = response;
                hideFullScreenLoading();
                if (data.code && data.code !== 200) {
                    return Promise.reject(data);
                }
                return data;
            },
            async (error: AxiosError) => {
                const { response } = error;
                hideFullScreenLoading();
                return Promise.reject(error);
            }
        );
    }

    /**
    * @description 常用请求方法封装
    */
    get<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {
        return this.service.get(url, { params, ..._object });
    }
    post<T>(url: string, params?: object | string, _object = {}): Promise<ResultData<T>> {
        return this.service.post(url, params, _object);
    }
    put<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {
        return this.service.put(url, params, _object);
    }
    delete<T>(url: string, params?: any, _object = {}): Promise<ResultData<T>> {
        return this.service.delete(url, { params, ..._object });
    }
    download(url: string, params?: object, _object = {}): Promise<BlobPart> {
        return this.service.post(url, params, { ..._object, responseType: "blob" });
    }
}
export default new RequestHttp(config);

逐行解析:

1.import Cookie from 'js-cookie'

导入 Cookie 存储,用于获取登陆时保存的token

2.import axios, {...} from "axios"

引入了 axios 库,并导入了一些特定的类型和接口。

  • axios: 这是 axios 库的默认导出,它是一个用于发送 HTTP 请求的库。
  • AxiosInstance: 这是 axios 库中定义的一个接口,表示一个 Axios 实例,它包含了可以发送请求的各种方法。
  • AxiosError: 这是 axios 库中定义的一个接口,表示一个 Axios 错误对象,它包含了发送请求过程中产生的错误信息。
  • AxiosRequestConfig: 这是 axios 库中定义的一个接口,表示一个 Axios 请求的配置对象,它包含了请求的各种配置选项,例如 URL、参数、请求头等。
  • InternalAxiosRequestConfig: 这是 axios 库中定义的一个内部接口,表示 Axios 请求的内部配置对象,包含了一些额外的配置选项。
  • AxiosResponse: 这是 axios 库中定义的一个接口,表示一个 Axios 响应对象,它包含了请求的响应数据。

3.export interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig

接口:扩展了一个可选的 noLoading 属性,用于控制请求是否显示加载状态。

4.interface Result

接口:响应的常规数据类型(响应码与响应提示)

5.interface ResultData<T = any> extends Result

接口:返回的非常规数据类型,如数据/字符串/对象类型的数据,并继承常规数据类型。

6.const config,timeout

声明常量config,用于配置Axios实例的选项。 timeout 设置请求超时时间,以毫秒为单位。默认值为 3000,表示请求将在 3000 毫秒后超时。

7.class RequestHttp

定义了RequestHttp类,它封装了使用Axios发送HTTP请求的功能,并暴露了一些常用的请求方法。

8.service: AxiosInstance

声明了一个名为service的成员变量,类型为 AxiosInstance。AxiosInstance是Axios库创建的实例类型,它具有发送 HTTP 请求的能力。

9.this.service.interceptors.request.use

注册了一个请求拦截器,对请求进行预处理。在这个拦截器中,可以进行一些公共的请求配置,比如在请求头中添加 token 等。检查 config.headers 是否存在且具有 set 方法。

10.this.service.interceptors.response.use

对响应数据进行处理。在这个拦截器中,首先获取响应数据 response.data,然后判断返回的数据是否符合预期的格式。不同的状态码,进行不同的处理。

11.``getpostputdeletedownload

(url: string, params?: object | string, _object = {})

分别对应了不同的 HTTP 请求方法,并使用 Axios 实例 service 来发送请求。这些方法都返回了一个 Promise 对象,用于处理异步操作,并将响应数据进行包装。url是请求的URL地址,params是一个可选参数,表示请求的查询参数,_object是一个可选参数,用于传递其他的配置项。

三:封装接口

地址:src > request > api.ts

作用:对HTTP请求进行处理

代码

import RequestHttp from "@/request/http";

namespace Login {
    export interface ReqLoginForm {
        username: string;
        password: string;
    }
    export interface ResLogin {
        token: string,
        customer_id: string
    }
}

/** @name 登录模块*/
export const loginApi = (params: Login.ReqLoginForm) => {
    return RequestHttp.post<Login.ResLogin>(`/api/admin_login.php?storeId=13`, params, { noLoading: true });
};

逐行解析:

1.namespace Login

定义了一个命名空间 Login,用于封装登录模块相关的接口和类型。

2.export const loginApi = (params: Login.ReqLoginForm)

这行代码导出了一个名为 loginApi 的函数,它接收一个名为 params 的参数,类型为 Login.ReqLoginForm。这个函数用于发送登录请求

3.RequestHttp.post

这行代码调用了 RequestHttp 模块中的 post 方法来发送 POST 请求。它传递了三个参数:请求的 URL、请求的参数(params),以及一个配置对象控制是否显示加载状态 { noLoading: true }。

四:登录页

地址:src > views > login > index.vue

代码

<template>
    <el-form ref="loginFormRef" :model="loginForm" :rules="rules" size="large">
        <div class="title">
            <h1>登陆</h1>
            <p>欢迎回来!请输入您的账户信息</p>
        </div>

        <el-form-item prop="username">
            <span>账号</span>
            <el-input v-model="loginForm.username" placeholder="用户名" />
        </el-form-item>
        <el-form-item prop="password">
            <span>密码</span>
            <el-input type="password" v-model="loginForm.password" placeholder="密码" show-password
                autocomplete="new-password" />
        </el-form-item>

        <div class="login_btn">
            <el-button color="#3438cd" @click="submitForm(loginFormRef)" size="large" :loading="loading">登录</el-button>
        </div>
    </el-form>
</template>
  
<script setup lang="ts">
import Cookies from "js-cookie";
import { store } from '@/store/index'
import { loginApi } from "@/request/api"
import { initDynamic } from '@/router/modules/dynamic'
import type { FormRules, FormInstance } from 'element-plus'

const loading = ref(false) // 登陆加载
const Store = store();
const loginFormRef = ref<FormInstance>() // 用于表单的引用

interface ReqLoginForm {
    username: string;
    password: string;
}

const loginForm = reactive<ReqLoginForm>({
    username: "admin",
    password: "6666",
})

const rules = reactive<FormRules>({
    username: [{ required: true, message: "请输入用户名", trigger: "blur" }],
    password: [{ required: true, message: "请输入密码", trigger: "blur" }]
})

// formEl是在submitForm方法中作为参数传递的表单实例,调用表单实例的方法和属性。
const submitForm = (formEl: FormInstance | undefined) => {
    if (!formEl) return
    formEl.validate(async (valid) => {
        if (!valid) return
        loading.value = true
        try {
            const { token, customer_id } = await loginApi(loginForm).then(res => res.data)
            Store.setID(customer_id); // 配置大菠萝
            Cookies.set('token', token);
            await initDynamic(); // 动态路由
        } finally {
            loading.value = false
        }
    })
}
</script>

五:配置大菠萝

安装:

npm i pinia

npm i pinia-plugin-persistedstate

export const store = defineStore

代码:

import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
interface GlobalState {
    customer_id: string;
}
export const store = defineStore({
    id: 'global-store',
    state: (): GlobalState => ({
        customer_id: '', // 角色ID
    }),
    getters: {},
    actions: {
        setID(id: string) {
            this.customer_id = id
        }
    }
})

const pinia = createPinia();
pinia.use(piniaPluginPersistedstate)
export default pinia;

逐行解析:

创建全局状态存储对象

const pinia = createPinia

创建了一个Pinia实例

使用了插件来实现状态的持久化,确保状态在刷新页面或重新加载应用后仍然保持

使用了插件来实现状态的持久化,确保状态在刷新页面或重新加载应用后仍然保持