搭建基于 Vite + Vue3 + TS 项目模板

一、项目概述

该项目是一个基于 Vite + Vue3 + TS 的项目模板,目的是为了节省每次开发新项目用于配置环境的时间,并附有简单的登录、注册及导航页。

项目集成:

项目启动

项目地址:

拉取项目并安装相关依赖包(此处以 gitee 为例):

git clone https://gitee.com/ljh210107/vite-vue3-ts-template.git
cd vite-vue3-ts-template
yarn install

启动项目(登录、注册功能需要自行搭建后端配合使用):

yarn run dev

项目演示

内附简单的登录、注册及导航页,需要自行搭建后端配合使用:

项目演示


二、创建项目

使用 yarn 创建项目:

yarn create vite vite-vue3-ts-template --template vue-ts

此处选用 vue-ts 模板,项目名称为:vite-vue3-ts-template

安装相关依赖并启动项目:

cd vite-vue3-ts-template
yarn install
yarn run dev
三、配置路径别名

为了使用 node 相关模块,可安装:

yarn add @types/node -D

打开 vite.config.ts 文件,添加路径别名及启动端口的配置:

// vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { resolve } from "path";

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [vue()],
    server: {
        host: "0.0.0.0",
        port: 5173,
    },
    resolve: {
        alias: {
            // 配置文件路径别名
            "@": resolve(__dirname, "src"),
        },
    },
});

四、配置路由

安装 vue-router,默认安装 4.x 版本:

yarn add vue-router

src 目录下创建文件夹 router,并创建 index.ts 文件:

// src/router/index.ts
import { createRouter, createWebHashHistory } from 'vue-router'

const router = createRouter({
    history: createWebHashHistory(),
    routes: [
        {
            path: '/',
            redirect: '/home'
        },
        {
            path: '/home',
            component: () => import('@/views/home/Home.vue'),
        },
        {
            path: '/login',
            component: () => import('@/views/login/Login.vue'),
        },
    ]
})

export default router;

src 目录下创建文件夹 views,并在该文件夹内创建 homelogin 文件夹,分别在文件夹内创建 Home.vueLogin.vue 文件。

可能会出现报错:找不到模块“@/views/home/Home.vue”或其相应的类型声明

此时只需在 vite-env.d.ts 文件中添加以下内容即可:

// vite-env.d.ts
/// <reference types="vite/client" />

declare module '*.vue' {
    import { ComponentOptions } from 'vue'
    const componentOptions: ComponentOptions
    export default componentOptions
}

main.ts 中挂载路由配置:

import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from '@/router/index'

const app = createApp(App)
// 挂载路由配置
app.use(router)

app.mount('#app')

可能出现报错:找不到模块“@/router/index”或其相应的类型声明。

此时需要在 tsconfig.app.json 文件中配置路径别名即可:

{
  "compilerOptions": {
    /* Bundler mode */
    ...
    /* Linting */
    ...

    // 设置基础路径
    "baseUrl": ".",
    // 路径别名配置
    "paths": { "@/*": ["src/*"] },
  }
}

注意: 可能需要重启 IDE 才能生效。

修改 App.vue 文件为以下内容,测试路由:

<template>
  <router-link to="/home"> Home </router-link>
  <router-link to="/login"> Login </router-link>
  <router-view></router-view>
</template>

<script setup lang="ts"></script>
<style scoped></style>

Vue Router 参考资料:https://router.vuejs.org/zh/introduction.html


五、引入状态管理工具 Pinia

安装 pinia 用于跨组件共享状态, pinia-plugin-persistedstate 则用于数据的持久化:

yarn add pinia pinia-plugin-persistedstate

main.ts 文件中引入:

import { createApp } from 'vue'
import './style.css'
import App from './App.vue'

// 状态管理工具 Pinia
import { createPinia } from 'pinia'
import persistedstate from 'pinia-plugin-persistedstate'

// -----------------
const app = createApp(App)
// 创建 pinia 实例
const pinia = createPinia()

// 使用持久化存储插件
pinia.use(persistedstate)
app.use(pinia)

app.mount('#app')

src 目录下创建文件夹 stores,并创建文件 user.ts

// src/stores/user.ts
import { defineStore } from "pinia";
import { ref } from "vue";

interface User {
    // 用户ID
    id: number
    // 用户名称
    username: string
    // 登录凭证
    token: string
}

export const useUserStore = defineStore("user", () => {

    // 定义用户信息
    const userInfo = ref<User>();
    // 保存用户信息
    const setUserInfo = (val: User) => {
        userInfo.value = val
    }
    // 清空用户信息
    const clearUserInfo = () => {
        userInfo.value = undefined
    }

    return {
        userInfo,
        setUserInfo,
        clearUserInfo
    }
},{
    // 数据持久化
    persist: {
        // 默认存储至 localStorage
        storage: localStorage,
        // 指定需要被持久化的数据
        paths: ["userInfo"],
    }
});

App.vue 中使用:

<template>
    <!-- 路由测试 -->
    <router-link to="/home"> Home </router-link>
    <router-link to="/login"> Login </router-link>
    <router-view></router-view>
</template>

<script setup lang="ts">
import { useUserStore } from "@/stores/user";
import { onMounted } from "vue";
const userStore = useUserStore();

onMounted(() => {
    userStore.setUserInfo({
        id: 1,
        username: "tom",
        token: "123",
    });
    console.log(userStore.userInfo);
});
</script>

查看数据是否持久化:

查看数据是否持久化

Pinia 参考资料:https://pinia.web3doc.top/introduction.html
pinia-plugin-persistedstate 参考资料:https://prazdevs.github.io/pinia-plugin-persistedstate/zh/guide/


六、引入 UI 框架 Element-plus

安装 element-plus

yarn add element-plus

安装 unplugin-vue-componentsunplugin-auto-import 这两款插件,以实现按需自动导入:

yarn add unplugin-vue-components unplugin-auto-import -D

修改 vite.config.ts 配置文件如下:

// vite.config.ts
import { defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

export default defineConfig({
  // ...
  plugins: [
    // ...
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    }),
  ],
})

添加全局配置:

// src/main.ts
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'

import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import zhCn from 'element-plus/es/locale/lang/zh-cn'

// -----------------
const app = createApp(App)
...
app.use(ElementPlus, { size: 'small', zIndex: 3000, locale: zhCn })
app.mount('#app')

注: size 用于设置表单组件的默认尺寸,zIndex 用于设置弹出组件的层级,locale 用于设置组件文字的默认语言。

Element-plus 参考资料:https://element-plus.org/zh-CN/guide/installation.html


七、添加全局接口配置文件

在根目录下分别创建 .env.development (开发环境) 、.env.production (生产环境)、.env.test (测试环境)文件。

.env.development 开发环境配置文件:

# .env.development 开发环境配置文件
NODE_ENV = 'development'
VITE_ENV = 'development'
VITE_APP_BASE_API = '/api'
VITE_SERVER = 'http://127.0.0.1:3000'

.env.production 生产环境配置文件:

# .env.production 生产环境配置文件
NODE_ENV = 'production'
VITE_ENV = 'production'
VITE_APP_BASE_API = 'http://xx.xx.xx.xx'
VITE_SERVER = 'http://xx.xx.xx.xx'

.env.test 测试环境配置文件:

# .env.test 测试环境配置文件
NODE_ENV = 'test'
VITE_ENV = 'test'
VITE_APP_BASE_API = 'http://xx.xx.xx.xx'
VITE_SERVER = 'http://xx.xx.xx.xx'

修改 package.json 文件中的 scripts,根据不同命令打包不同环境项目:

"scripts": {
    "dev": "vite",
    "build": "vue-tsc -b && vite build",
    "build:production": "vue-tsc && vite build --mode production",
    "build:test": "vue-tsc && vite build --mode test",
    "preview": "vite preview"
}
八、集成 Axios

安装 axios

yarn add axios

src 目录下新建 utils 文件夹,并新建 request.ts 文件,用于封装 axios

import axios, { InternalAxiosRequestConfig, AxiosResponse, AxiosRequestConfig } from "axios";
import { useUserStore } from "@/stores/user";
import { ElNotification } from "element-plus";

// import { useRouter } from "vue-router"
// 由于 useRouter 必须写在 setup 中,所有无法在此使用
import Vrouter from "@/router"
// const route = Vrouter.currentRoute.value
const router = Vrouter


const http = axios.create({
    // 根据全局的环境变量来设置请求 api 的 baseURL,例如在 development (开发环境),import.meta.env.VITE_APP_BASE_API 的值 => '/api'
    // 注意:如果 VITE_APP_BASE_API 的值是一个相对地址,如:'/api',则请求会经过 server.proxy 代理拼接完整路径。
    //      如果 VITE_APP_BASE_API 的值是完整的地址,如:http://xx.xx.xx.xx/,则会直接发送请求而不经过 server.proxy 代理
    baseURL: import.meta.env.VITE_APP_BASE_API,
    // 请求超时时间
    timeout: 5000,
});

// 请求拦截
http.interceptors.request.use(
    (config: InternalAxiosRequestConfig) => {
        // 获取用户信息中的 token 信息
        const userStore = useUserStore();
        const token = userStore.userInfo?.token;
        if (token) {
            // 添加 token 请求头标识(注意 Bearer 后面还有一个空格!!!)
            config.headers.Authorization = 'Bearer ' + token;
        }
        return config;
    },
    (error) => {
        console.log(error);
        ElNotification({
            title: "Error",
            message: error.message,
            type: "error",
        });
        return Promise.reject(error);
    }
);

// 响应拦截
http.interceptors.response.use(
    (response: AxiosResponse) => {
        // 请求成功
        // 处理成功响应
        if (response.status >= 200 && response.status < 300) {
            return response;
        } else {
            ElNotification({
                title: "Error",
                message: "Request error.",
                type: "error",
            });
            return Promise.reject(new Error("*Request error."));
        }
    },
    (error) => {
        // 请求失败
        console.log(error);
        // 状态提示
        let msg = "";
        let status = error.response.status;
        console.log(status);

        switch (status) {
            // token 过期处理
            case 401:
                msg = "Login expired, please log in again.";
                // 清理用户数据,跳转至登录页面
                const userStore = useUserStore();
                userStore.clearUserInfo();
                // 跳转至登录页面
                router.push("/login");
                break;
            case 404:
                msg = "The network request does not exist.";
                break;
            case 500:
                msg = "Server error.";
                break;
            default:
                msg = error.response.data.message;
                break;
        }
        ElNotification({
            title: "Error",
            message: msg,
            type: "error",
        });
        return Promise.reject(error);
    }
);

// 后端数据响应格式
type ResponseResult<T> = {
    success: boolean;
    code: number;
    msg: string;
    data: T;
};

// 封装 http 请求函数
const httpRequest = async <T>(config: AxiosRequestConfig): Promise<ResponseResult<T>> => {
    const response: AxiosResponse<ResponseResult<T>> = await http.request(config);
    // 返回整个 ResponseResult 对象
    return response.data;
};

export default httpRequest;

配置代理以处理跨域问题,在 vite.config.ts 文件中添加配置:

// vite.config.ts
import { defineConfig, loadEnv } from "vite";

// https://vitejs.dev/config/
export default defineConfig(({ command, mode }) => {

    // 根据当前工作目录中的 `mode` 加载 .env 文件
    // 设置第三个参数为 '' 来加载所有环境变量,而不管是否有 `VITE_` 前缀。
    const env = loadEnv(mode, process.cwd(), "");

    return {
        plugins: [
            ...
        ],
        server: {
            host: "0.0.0.0",
            port: 5174,

            // 配置代理 - 解决跨域问题
            proxy: {
                // 如果 env.VITE_APP_BASE_API 的值为相对地址,如:'/api',则会经过代理
                [env.VITE_APP_BASE_API]: {
                    // 请求接口前缀
                    target: env.VITE_SERVER,
                    changeOrigin: true,
                    // 将请求中的相对路径(env.VITE_APP_BASE_API) 替换成空字符串,
                    // 例如:开发环境下 process.env.VITE_APP_BASE_API 的值如果为 '/api',
                    // 请求的路径为 '/user',拼接后则变成 target + '/user' => 127.0.0.1:3000/user
                    // 总结:实际上就是将请求接口的 '/api' 字符串去掉,当然也不一定是 '/api' 这个字符串,根据的是 env.VITE_APP_BASE_API 的值进行处理的
                    rewrite: (path) => path.replace(new RegExp('^' + (process.env.VITE_APP_BASE_API as string)), '')
                },
            },
        },
        resolve: {
            ...
        },
    };
});

注意:如果 VITE_APP_BASE_API 的值是一个相对地址,如:'/api',则请求会经过 server.proxy 代理拼接完整路径。
如果 VITE_APP_BASE_API 的值是完整的地址,如:http://xx.xx.xx.xx/,则会直接发送请求而不经过 server.proxy 代理

src 目录下新建 api 文件夹,并新建 user.ts 文件,定义请求接口:

// src/api/user.ts
import httpRequest from "@/utils/request";

// 登录成功后返回的结果
export type LoginResult = {
    // 用户ID
    id: number
    // 用户名称
    email: string
    // 登录凭证
    token: string
}

// 登录验证
export const userLoginAPI = (email: string, password: string) => {
    return httpRequest<LoginResult>({
        method: 'POST',
        url: '/users/login',
        data: {
            email,
            password
        }
    })
}

Login.vue 测试登录功能:

// src/views/login/Login.vue
<template>
    <el-form class="form" :model="userForm" ref="userFormRef">
        <div class="input-group">
            <label for="email">Email</label>
            <el-form-item prop="email">
                <el-input type="text" v-model="userForm.email" placeholder="Enter your email" />
            </el-form-item>
        </div>
        <div class="input-group">
            <label for="password">Password</label>
            <el-form-item prop="password">
                <el-input type="password" v-model="userForm.password" show-password placeholder="Enter your password" />
            </el-form-item>
        </div>
        <el-button class="sign" @click="login">Sign in</el-button>
    </el-form>
</template>

<script setup lang="ts">
import { reactive } from "vue";
import { userLoginAPI } from "@/api/user";
import { useRouter } from "vue-router";
import type { FormInstance } from "element-plus";

import { useUserStore } from "@/stores/user";
const userStore = useUserStore();

type UserForm {
    email: string;
    password: string;
}

const userFormRef = ref<FormInstance>();
const userForm = reactive<UserForm>({
    email: "",
    password: "",
});

// 登录
const router = useRouter()
const login = async () => {
        // 登录验证
    const res = await userLoginAPI(userForm.email, userForm.password);
    console.log(res);
    if (res.success) {
        // 保存用户信息
        userStore.setUserInfo(res.data)
        setTimeout(() => {
            // 登录成功后跳转至首页
            router.push('/home')
        }, 3000);
    }
};
</script>

参考链接:


九、集成 VueUse

VueUse 是基于合成 API 的实用函数集合,借助它可以快速实现一些常见的功能。

安装 VueUse

yarn add @vueuse/core

使用,此处以监听键盘事件为例:

import { onKeyStroke } from '@vueuse/core'

// 此处以 Backspace 键为例
onKeyStroke('Backspace', (e) => {
    // 阻止默认事件,例如 Backspace 键的删除功能
    // e.preventDefault()
    console.log(1);
})

可将以上相关代码写入任意页面组件,当用户在页面中按下 Backspace 将触发相应事件。

更多方法参见:https://vueuse.nodejs.cn/functions.html


十、集成 Less
yarn add less -D

使用时只需修改 stylelang 即可:

<style lang="less">
@com_font_family: Comic Sans MS, Comic Sans, cursive, Arial, sans-serif;

.el-form {
    font-family: @com_font_family;

    &:hover {
        background-color: #212121;
    }
}
</style>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值