Vite + Vue3 + Vue router + Axios + Ant design + TS + Pinia - 从0开始搭建后台管理系统

743 阅读6分钟

通过 vite 搭建一个 Vue3 基础项目

yarn create vite

选择 Vue + TS

image.png

配置选好后

  • 进入项目目录: cd Evse
  • 安装依赖: yarn
  • 启动: yarn dev

image.png

浏览器打开: http://127.0.0.1:5173/

image.png

到此 vite 搭建的 vue-ts 基础项目完成

搭建基础目录结构

在 src 目录下几个文件夹

  • apis: 接口请求文件
  • directives:自定义指令文件
  • layout: 布局文件
  • router: 路由文件
  • store: 状态管理
  • utils: 工具库
  • views: 页面组件

image.png

vite 配置

默认创建出来的项目, 配置文件 vite.config.ts 如下

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/ 
export default defineConfig({ 
    plugins: [vue()] 
})

配置别名:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
const resolve = (dir: string) => path.join(__dirname, dir)

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [vue()],
    resolve: {
        alias: {
          '@': resolve('src'),
          '@comps': resolve('src/components'),
          '@apis': resolve('src/apis'),
          '@views': resolve('src/views'),
          '@router': resolve('src/router'),
          '@layout': resolve('src/layout'),
        }
    }
})

这里的别名可根据自己的需求随意配置

此时 TS 可能会报以下这个错误

image.png

安装 @types/node 即可 yarn add @types/node

image.png

但是在 vue 中的 script ts 中使用别名还是会一下报错

image.png

这其实模块已经正常引入了,但是 ts 不认,我们需要在 tsconfig.json 中增加如下配置

image.png

配置服务

server: {
    // host: '',
    // 端口号
    port: 8888,
    // 设为 true, 若端口被占用则会直接退出, 而不是尝试下一个可用端口
    strictPort: false, 
    // 服务器启动时自动在浏览器中打开应用程序
    open: true,
    // 自定义代理规则
    proxy: {
      
    }
  }

集成 Vue Router

yarn add vue-router@4

执行命令安装完成后, 在 src 目录下新建一个 router 目录,里面创建一个 index.ts, 写入以下配置

import type { App } from 'vue'
import { createRouter, createWebHashHistory } from "vue-router";
import { routes } from './routes';

export const router =  createRouter({
    history: createWebHashHistory(),
    routes
})

// config router 
export function setupRouter(app: App<Element>) {
    app.use(router)
}

同层级新建一个 routes.ts 文件,进行 routes 配置, 用于 router/index.ts 引入使用

const Layout = () => import('@layout/Index.vue')
const Home = () => import('@views/home/Index.vue')

export const routes = [
    {
        path: '/',
        component: Layout,
        redirect: '/home',
        children: [
            {
                path: 'home',
                name: 'home',
                component: Home
            }
        ]
    }
]

由于 router 我们采用 hooks 的写法,所以需要改造下 main.ts, 定义一个启动方法 bootstrap 方法,在方法里面进行创建 app 引用, 设置路由, 然后挂载

image.png

根组件是 App.vue, 路由匹配的结果会显示在根组件的 标签中,所以在 App.vue 中需要增加该标签;

Home 是 layout 子组件, 子组件也会渲染在父组件的 标签中,所以在 Layout 中也需要增加该标签

  • app.vue

    image.png

  • layout.vue

    image.png

页面效果如下

![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/af3f561d72ae40b9ac2622b5b889a039~tplv-k3u1fbpfcp-watermark.image?)

集成 Pinia 状态管理库

安装 yarn add pinia

配置 src/store/index.ts 文件,

import type { App } from 'vue'
import { createPinia } from 'pinia'

const store = createPinia()

export function setupStore(app: App<Element>) {
    app.use(store)
}

export { store }

在 main.js 中引入 setupStore 然后进行初始化

image.png

声明一个仓库有多种写法,如下图所示

image.png

我们采用第三种方式 在store 文件夹中定义一个 app.ts 声明一个 app 仓库,

import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useAppStore = defineStore('app', () => {
    const count = ref(100)

    function getDoubleCount() {
        return count.value * 2
    }

    function setCount(val: number) {
        count.value = val
    }

    return { count, getDoubleCount, setCount }
}) 

在 Home 组件中使用 pinia

<template>
    <h3>这个是Home组件</h3>
    <p>count: {{ count }}</p>
    <p>doubleCount: {{ appStore.getDoubleCount() }}</p>
    <button @click="changCount">修改 count</button>
</template>


<script setup lang="ts">
    import { storeToRefs } from 'pinia'
    import { useAppStore } from '@store/app'

    const appStore = useAppStore()
    const { count } = storeToRefs(appStore)

    function changCount() {
        appStore.setCount(count.value + 2)
    }
</script>

image.png

集成 Ant design UI 框架

安装 ant-design-vue

main.ts 中进行全局引入

image.png

Home 组件直接使用组件

image.png

效果如下

image.png

集成 Axios

安装 yarn add axios

封装 Axios

需求:想要通过如下方式进行调用接口

  • 单独一个文件维护接口地址, 内部通过不同模块名进行划分, 页面通过 $api.moduleName.url 进行引入对应的接口

  • 所有接口方法都可以通过 $http.methods名 进行调用,常用的 $get 和 $post 可通过 $get, $post 直接调用

  • 可直接通过 { data, err } 进行接口数据的接受,data, err 分别代表成功与失败的信息

const { data, err } = await $get($api.moduleName.url)
const { data, err } = await $post($api.moduleName.url, [params, config])
const { data, err } = await $http.get($api.moduleName.url)
const { data, err } = await $http.post($api.moduleName.url, [params, config])
const { data, err } = await $http.delete($api.moduleName.url, [params, config])
const { data, err } = await $http.put($api.moduleName.url, [params, config])

接口单独维护

api/api.ts

// api 集中式管理
export default {
    sysLogin: '/xxx/xxx/sysLogin3', // 登陆接口
    station: {
        list: '/xxx/xxx/getList',
        add: 'xxx',
        del: 'xxx',
        update: 'xxx'
    },
    evse: {
        list: 'xxx',
        add: 'xxx',
        del: 'xxx',
        update: 'xxx'
    }
} 

Axios 全局配置

api/axios.ts

// 此处封装主要是 实现请求拦截,实现响应拦截,常见错误信息处理,请求头设置

import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError  } from "axios"
import config from '../../config'
import { handleRequestHeader, handleAuth } from './requestHandle' // 请求拦截回调
import { handleNetworkError } from './responseHandle' // 响应拦截回调
import { message } from 'ant-design-vue'

axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'

const MODE = import.meta.env.MODE
// axios 实例全局配置
export const service: AxiosInstance = axios.create({
    baseURL: config[MODE].baseUrl,
    withCredentials: true,
    timeout: 15000,
    headers: {
    }
})

// 请求拦截器
service.interceptors.request.use((config: AxiosRequestConfig) => {
    //这里会最先拿到你的请求配置
    config = handleRequestHeader(config)
    config = handleAuth(config)
    return config
}, (error: AxiosError) => {
    // 这里极少情况会进来,暂时没有找到主动触发的方法,估计只有浏览器不兼容时才会触发,欢迎后面同学补充
    // 看了几个GitHub的issue,有人甚至提出了这个方法是不必要的(因为没有触发的场景),不过还是建议大家按照官方的写法,避免不必要的错误
    // 进来之后没法发起请求
    return Promise.reject(error)
})

// 响应拦截器
service.interceptors.response.use((response: AxiosResponse) => {
    // 这里会最先拿到你的response
    // 只有返回的 http 状态码是2xx,都会进来这里
    const res = response.data
    if (res.code !== 200) { // 业务状态码判断
        handleNetworkError(res.code)
        return null
    }

    return response.data
}, (error: AxiosError) => {
    // 目前发现三种情况会进入这里:
    // 1. http状态码非2开头的都会进来这里,如404,500等
    // 2. 取消请求也会进入这里,CancelToken,可以用axios.isCancel(err)来判断是取消的请求
    // 3. 请求运行有异常也会进入这里,如故意将headers写错:axios.defaults.headers = '123',或者在request中有语法或解析错误也会进入这里
    // 进入这里意味着请求失败,axios会进入catch分支
    message.error(error.message)
    return Promise.reject(error)
})

便捷调用方式

api/http.ts

import { service as axios } from './axios'
import qs from "qs"

function get(url: string, config?: object) {
  return new Promise((resolve, reject) => {
    axios
      .get(url, config)
      .then(res => {
        resolve({ data: res.data, err: null })
      })
      .catch(err => {
        resolve({data: null, err})
      })
  })
}
function post(url: string, params?: object, config?: object) {
  return new Promise((resolve, reject) => {
    axios
      .post(url, qs.stringify(params), config)
      .then(res => {
        resolve({ data: res.data, err: null })
      })
      .catch(err => {
        resolve({data: null, err})
      })
  })
}


export const http = {
  get,
  post
}

全局注册

    // main.ts
    //引入
    import { registerGlobVariable } from  '@/utils/registerGlobVariable'
    
    // bootstrap() 中注册
    // 注册全局变量
    registerGlobVariable(app)
    // 注册全局组件
    registerGlobComp(app)
    
   
    
    // utils/registerGlobVariable.ts
    import type { App } from 'vue'

    import { http } from '@/api/http'
    import api from '@/api/api'

    export function registerGlobVariable(app: App) {
        app.config.globalProperties.$api = api
        app.config.globalProperties.$http = http
        app.config.globalProperties.$get = http.get
        app.config.globalProperties.$post = http.post
    }
    
    

页面使用

 // home.vue

import { getCurrentInstance } from 'vue'

// axios 测试
const { proxy, proxy: { $http, $get, $post, $api } } = getCurrentInstance() as any
async function axiosTest() {
    const { data, err } = await $get($api.sysLogin)
    console.log(data, err)
}

image.png

其他

axios.ts 文件中将请求拦截回调和响应拦截回调提出到了一个单独的文件, 还有 config.ts 用于区分不同的打包环境

// requestHandle.ts
// 通过判断是否存在 token 判断用户登陆情况
// 即使 token 存在, 也有可能 token 过期,所需需要在每次的请求头中携带 token, 让后端根据 token 判断用户登陆情况,并返回我们对应的状态码
export const handleRequestHeader = (config: any) => {
    config['xxxx'] = 'xxx'
    return config
}

export const handleAuth = (config: any) => {
    config.header['token'] = localStorage.getItem('token') || ''
    return config
}
    
    
    
// responseHandle.ts
    
import { message } from 'ant-design-vue'

export const handleNetworkError = (errStatus?: any) => {
    let errMessage = '未知错误'
    if (errStatus) {
        switch (errStatus) {
            case 400:
                errMessage = '错误的请求'
                break
            case 401:
                errMessage = '未授权,请重新登录'
                break
            case 403:
                errMessage = '拒绝访问'
                break
            case 404:
                errMessage = '请求错误,未找到该资源'
                break
            case 405:
                errMessage = '请求方法未允许'
                break
            case 408:
                errMessage = '请求超时'
                break
            case 500:
                errMessage = '服务器端出错'
                break
            case 501:
                errMessage = '网络未实现'
                break
            case 502:
                errMessage = '网络错误'
                break
            case 503:
                errMessage = '服务不可用'
                break
            case 504:
                errMessage = '网络超时'
                break
            case 505:
                errMessage = 'http版本不支持该请求'
                break
            default:
                errMessage = `其他连接错误 --${errStatus}`
        } 
    } else {
        errMessage = '无法连接到服务器!'
    }

    message.error(errMessage)
}