前提
最近,Vue的全部生态已经全面转移到vue3版本,Vue3的相关生态也开始逐渐迁移。我们最常用的后台ui库element的vue3版本也在2月7号那天上线了稳定版,也就是说全面使用vue3时机已经成熟了。
本小弟也曾经在公司尝试过vue3.0的开发,但是碍于之前相关的生态很不成熟,开发起来确实磕磕碰碰的,一直提不起兴趣。
后来又陆续地短暂学习了一些TypeScript,vite相关的知识,但是一直没自己上手体验一下,所以这次下定决心记录一下自己搭建一个后台项目管理模版的过程。
本次搭建的后台模版只是一个简易版,后续会继续完善各种功能
初始化项目
这里先进行项目的初始化
使用vite2创建一个项目
//npm创建
npm init vite@latest
//选中vue ts版本
npx: 6 安装成功,用时 3.765 秒
✔ Project name: … sam-vue-admin-ts
✔ Select a framework: › vue
✔ Select a variant: › vue-ts
//安装依赖
cd sam-vue-admin-ts
npm i
// 本地跑起来
npm run dev
//默认的package.json
{
"name": "sam-vue-admin-ts",
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"serve": "vite preview"
},
"dependencies": {
"vue": "^3.2.31"
},
"devDependencies": {
"@vitejs/plugin-vue": "^1.9.3",
"typescript": "^4.4.3",
"vite": "^2.8.1",
"vue-tsc": "^0.31.3"
}
}
//初始化的tsconfig.json
{
"compilerOptions": {
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}
vite配置目录别名
进入配置文件 vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
// import形式
import { resolve } from "path";
// https://vitejs.dev/config/
export default defineConfig({
resolve: {
alias: {
"@": resolve(__dirname, "./src"),
},
},
plugins: [
vue()
],
});
这里ts会提示
找不到名称“__dirname”。ts(2304)
any
// 我们安装一下node相关的types
npm install @types/node --save-dev
//接着配置一下tsconfig.json
{
"compilerOptions": {
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"],
//这里配置路径
"baseUrl": ".",
"paths": {
"@/*": [
"src/*"
],
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}
安装css预处理器(sass)
npm install -D sass
修改.vue文件的style标签,添加 lang="scss" HelloWorld.vue
<style lang="scss" scoped>
a {
color: #42b983;
}
label {
margin: 0 0.5em;
font-weight: bold;
}
code {
background-color: #eee;
padding: 2px 4px;
border-radius: 4px;
color: #304455;
}
</style>
项目安装element-plus,使用unplugin-vue-components自动按需引入
npm install element-plus --save
npm install unplugin-vue-components -D
这个unplugin-vue-components插件还可以导入自己的组件,但是我在这里没有进行这样配置,
有兴趣的可以看看这位大佬的文章
vite.config.ts配置,unplugin-vue-components提供了很多知名ui库的resolvers可以直接使用
在这里我只使用ElementPlusResolver
import Components from "unplugin-vue-components/vite";
import ViteComponents, {
//AntDesignVueResolver,
ElementPlusResolver,
//VantResolver,
} from 'unplugin-vue-components/resolvers'
Components({
resolvers: [
//AntDesignVueResolver(),
ElementPlusResolver(),
//VantResolver(),
]
})
接着我们修改一下HelloWorld.vue看看效果
<el-button type="button" @click="count++">count is: {{ count }}</el-button>
可以看到连样式都自动引入了
使用unplugin-auto-import/vite自动引入vue相关的hooks
npm i -D unplugin-auto-import
支持
vue,vue-router,vue-i18n,@vueuse/head,@vueuse/core等自动引入有兴趣的可以看看这位大佬的文章
修改vite配置
plugins: [
vue(),
Components({
resolvers: [ElementPlusResolver()],
}),
AutoImport({
imports: ['vue', 'vue-router'],
dts: 'src/auto-imports.d.ts'
})
],
修改HelloWorld.vue
把之前的引入注释掉看看效果
<script setup lang="ts">
// import { ref } from 'vue'
defineProps<{ msg: string }>()
const count = ref(0)
</script>
安装会用到的一些依赖
全局状态管理
pinia(也可以使用vuex4,pinia在使用上更简便)
npm install vuex@next --save
npm install pinia@next
参考大佬:
路由
vue-router4
npm install vue-router@next
http请求相关
axios
npm install axios --save
@types/qs
npm install @types/qs -D
qs
npm install qs --save
完善项目目录
我们继续完善src文件夹的内容
App.vue
components.d.ts
http
plugins
views
assets
types
constants
layout
router
auto-imports.d.ts
env.d.ts
libs
store
components
hooks
main.ts
布局容器组件以及路由配置
布局容器
src/layout文件夹下分别创建baseLayout,baseView,components三个文件夹
components/BaseHeader.vue创建头部模块
<script setup lang="ts">
import useUserStore from '@/store/modules/user.module';
import { ElMessage } from 'element-plus';
const UserStore = useUserStore();
const router = useRouter();
const state = reactive({
circleUrl: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'
});
const userName = computed(() => UserStore.userInfo.userName);
const clickHandler_logout = () => {
UserStore.logout();
ElMessage.success('退出登录成功');
router.push('/Login');
};
const { circleUrl } = toRefs(state);
</script>
<template>
<div class="baseHeader">
<div class="baseHeader_right">
<el-dropdown>
<div class="baseHeader-memu">
<el-avatar :size="40" :src="circleUrl"></el-avatar>
<div class="baseHeader-name">{{ userName || '运营人员' }}</div>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="clickHandler_logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<style lang="scss">
.baseHeader {
height: 100%;
width: 100%;
background-color: #fff;
border-bottom: 1px solid #f1f1f1;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
&_right {
height: 100%;
float: right;
.baseHeader-memu {
height: 60px;
display: flex;
justify-content: center;
align-items: center;
.baseHeader-name {
padding: 0 10px;
}
}
}
}
</style>
components/BaseAside.vue创建菜单侧边栏模块
<script setup lang="ts">
</script>
<template>
<div class="baseAside">
<div class="baseAside-title">vite-admin</div>
<BaseSiderMenu />
</div>
</template>
<style lang="scss">
.baseAside {
height: 100vh;
width: 100%;
background-color: #545c64;
.baseAside-title {
height: 60px;
width: 100%;
color: #fff;
font-size: large;
line-height: 60px;
text-align: center;
}
}
</style>
layout/baseLayout创建一个index.vue文件,作为一种基础布局容器组件
<script setup lang="ts">
import BaseHeader from "../components/BaseHeader.vue";
import BaseAside from "../components/BaseAside.vue";
</script>
<template>
<el-container>
<el-aside width="200px">
<BaseAside />
</el-aside>
<el-container>
<el-header>
<BaseHeader />
</el-header>
<el-main>
<router-view />
</el-main>
</el-container>
</el-container>
</template>
<style></style>
layout/baseView创建一个index.vue文件,作为一种内容容器组件
<script setup lang="ts"></script>
<template>
<div class="baseView">
<router-view v-slot="{ Component, route }">
<transition name="fade-transform" mode="out-in">
<component :is="Component" :key="route.fullPath" />
</transition>
</router-view>
</div>
</template>
<style>
</style>
基础的布局模块就完成了
路由配置
src/router文件夹创建modules文件夹,以及index.ts,route.ts文件
src/types文件夹创建index.ts文件,
modules:存放子模块的相关路由
index.ts:负责路由初始化,路由钩子,路由挂载
route.ts:负责定义相关的路由表
types/index.ts:负责定义项目的types
先modules创建几个子模块路由
modules/home.module.ts
import BaseView from '@/layout/baseView/index.vue';
export default {
path: 'home',
component: BaseView,
meta: {
title: '首页'
},
children: [
{
path: '',
redirect: '/home/index'
},
{
path: 'index',
component: () => import('@/views/home/index.vue'),
meta: {
title: '首页'
}
},
{
path: 'edit',
component: () => import('@/views/home/edit.vue'),
meta: {
title: '编辑'
}
},
{
path: 'list',
component: () => import('@/views/home/list.vue'),
meta: {
title: '列表'
}
}
]
};
modules/other.module.ts
import BaseView from '@/layout/baseView/index.vue';
export default {
path: 'other',
component: BaseView,
meta: {
title: '其他'
},
children: [
{
path: '',
redirect: '/other/index'
},
{
path: 'index',
component: () => import('@/views/other/index.vue'),
meta: {
title: '其他'
}
},
{
path: 'edit',
component: () => import('@/views/other/edit.vue'),
meta: {
title: '编辑'
}
},
{
path: 'list',
component: () => import('@/views/other/list.vue'),
meta: {
title: '列表'
}
}
]
};
src/types/index.ts 添加路由相关type
import { RouteRecordRaw } from 'vue-router';
// 路由meta
export type TRouteMeta = {
title?: string; // 页面标题
icon?: string; // 侧边菜单栏图标
activeMenu?: string;
slideHidden?: boolean; // 侧边菜单栏中是否需要隐藏
breadcrumb?: boolean; // 页面面包屑中是否需要显示当前路由
auth?: string | number; // 权限
};
// 路由对象
export type TRouteRecordRaw = {
meta?: TRouteMeta;
} & RouteRecordRaw;
export type TMenuRaw = {
label:string | undefined,
path:string,
children?:TMenuRaw[] | undefined
}
router/route.ts 路由
const BaseLayout = () => import('@/layout/baseLayout/index.vue');
const Login = () => import('@/views/login/index.vue');
import home from './modules/home.module';
import other from './modules/other.module';
import { TRouteRecordRaw, TMenuRaw } from '@/types/index';
const mainRoute = [home, other];
export const baseRoute = [
{
path: '/login',
name: 'login',
component: Login
},
{
path: '/',
component: BaseLayout,
redirect: '/home',
children: [...mainRoute]
},
{
path: '/error/:status',
name: 'error',
component: () => import('@/views/error/index.vue')
},
/* 错误路由 */
{
path: '/:catchAll(.*)',
component: () => import('@/views/error/index.vue')
}
];
//转换成菜单
const transform_menu = (route: TRouteRecordRaw[]) => {
return route
.map(item => {
let menuItem: TMenuRaw = {
path: '/' + item.path,
label: item.meta?.title
};
item.children && (menuItem.children = transform_menu(item.children));
return menuItem;
})
.filter(item => item.path !== '/');
};
export const menuRoute: TMenuRaw[] = transform_menu(mainRoute);
router/index.ts
import type { App } from 'vue';
import { createRouter, createWebHashHistory } from 'vue-router';
import { baseRoute } from './route';
import useUserStore from '@/store/modules/user.module';
const routerHistory = createWebHashHistory();
// createWebHashHistory hash 路由
// createWebHistory history 路由
// createMemoryHistory 带缓存 history 路由
const router = createRouter({
history: routerHistory,
routes: baseRoute
});
let UserStore:any = null
router.beforeEach((to, form, next) => {
!UserStore&&(UserStore = useUserStore())
if (to.name == 'login') {
// 如果已登录则不会跳转到登录页
if (UserStore.loginInfo.isLogin) {
if (to.name == 'login') next('/');
else next();
} else {
next();
}
} else {
// 未登录无法打开除登录页以外的页面
if (UserStore.loginInfo.isLogin) {
next();
} else {
next('/login');
}
}
});
export function setupRouter(app: App<Element>): void {
app.use(router);
}
export default router;
路由表自动生成菜单
src/components/BaseSiderMenu/index.vue
创建测边栏菜单
<script setup lang="ts">
import { menuRoute } from '@/router/route';
const router = useRouter();
const route = useRoute();
const menuList = reactive(menuRoute);
const activeMenu = computed(() => {
const { path } = route;
return path;
});
const clickHandler_menu = (key: string, keyPath: string[]) => {
console.log(key, keyPath);
router.push({ path: key });
};
</script>
<template>
<el-menu active-text-color="#ffd04b" :default-active="activeMenu" background-color="#545c64" text-color="#fff" @select="clickHandler_menu" class="el-menu-vertical-demo">
<el-sub-menu v-for="menu in menuList" :key="menu.path" :index="menu.path">
<template #title>
<span>{{ menu.label }}</span>
</template>
<el-menu-item v-for="item in menu.children" :key="item.path" :index="menu.path + item.path">{{ item.label }}</el-menu-item>
</el-sub-menu>
</el-menu>
</template>
<style>
.el-menu-vertical-demo {
border-right: 0;
/* width: 100%; */
}
</style>
创建登录页、错误页面
view文件夹下创建login,error文件夹
登录与注册页
login/index.vue
<script setup lang="ts">
import { ElForm, ElMessage } from 'element-plus';
import loginApi from '@/http/api/loginApi';
import useUserStore from '@/store/modules/user.module';
const UserStore = useUserStore();
const router = useRouter();
const isLoginType = ref(true);
const clickHandler_Type = (type: boolean, formEl: InstanceType<typeof ElForm> | undefined) => {
isLoginType.value = type;
resetForm(formEl);
};
const resetForm = (formEl: InstanceType<typeof ElForm> | undefined) => {
if (!formEl) return;
formEl.resetFields();
};
// 登录
const loginObject = reactive({
name: '',
password: ''
});
const loginFormRef = ref<InstanceType<typeof ElForm>>();
const loginRules = reactive({
name: [
{
required: true,
message: 'Please input name',
trigger: 'blur'
}
],
password: [
{
required: true,
message: 'Please input password',
trigger: 'blur'
}
]
});
const post_login = async () => {
let params = {
phone: loginObject.name,
password: loginObject.password
};
// let [results, message, code]: any = await loginApi.login(params);
const results = {
createdTime: '111',
id: '111',
phone: '111',
userName: '111',
token:'mock-token'
};
const message = '登录成功';
if (results) {
ElMessage.success(message);
const { token } = results;
UserStore.login(results, { isLogin: true, token });
router.push('/');
}
};
const clickHandler_login = (formEl: InstanceType<typeof ElForm> | undefined) => {
if (!formEl) return;
formEl.validate(valid => {
if (valid) {
post_login();
} else {
return false;
}
});
};
// 注册
const registerObject = reactive({
name: '',
password: ''
});
const registerFormRef = ref<InstanceType<typeof ElForm>>();
const registerRules = reactive({
name: [
{
required: true,
message: 'Please input name',
trigger: 'blur'
}
],
password: [
{
required: true,
message: 'Please input password',
trigger: 'blur'
}
]
});
const clickHandler_register = (formEl: InstanceType<typeof ElForm> | undefined) => {
if (!formEl) return;
formEl.validate(valid => {
if (valid) {
} else {
return false;
}
});
};
</script>
<template>
<div class="login">
<div class="login-box">
<h1>vite-admin</h1>
<!-- 登录 -->
<el-form status-icon ref="loginFormRef" label-position="top" :model="loginObject" hide-required-asterisk :rules="loginRules" v-if="isLoginType">
<el-form-item>
<h2 class="form-title">用户登录</h2>
</el-form-item>
<el-form-item label="用户名" prop="name">
<el-input v-model="loginObject.name"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="loginObject.password"></el-input>
</el-form-item>
<el-form-item>
<el-button class="button-sub" type="primary" @click="clickHandler_login(loginFormRef)">登录</el-button>
</el-form-item>
<el-form-item>
<el-button class="button-sub" @click="clickHandler_Type(false, loginFormRef)" type="text">用户注册</el-button>
</el-form-item>
</el-form>
<!-- 注册 -->
<el-form status-icon ref="registerFormRef" label-position="top" :model="registerObject" hide-required-asterisk :rules="registerRules" v-if="!isLoginType">
<el-form-item>
<h2 class="form-title">用户注册</h2>
</el-form-item>
<el-form-item label="用户名" prop="name">
<el-input v-model="registerObject.name"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="registerObject.password"></el-input>
</el-form-item>
<el-form-item>
<el-button class="button-sub" type="primary" @click="clickHandler_register(loginFormRef)">注册账号</el-button>
</el-form-item>
<el-form-item>
<el-button @click="clickHandler_Type(true, registerFormRef)" type="text">返回登录</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<style lang="scss" scoped>
.login {
background: rgb(217, 236, 255);
height: 100vh;
width: 100vw;
.login-box {
width: 400px;
height: 500px;
padding: 20px;
background-color: #fff;
border-radius: 16px;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
text-align: center;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
.form-title {
width: 100%;
text-align: center;
}
.button-sub {
width: 100%;
margin-top: 20px;
}
}
}
</style>
错误页
error/index.vue
<script setup lang="ts">
const router = useRouter()
const clickHandler_back = () => {
router.go(-1)
}
</script>
<template>
<div class="error-page">
<el-result
icon="error"
title="找不到该路由"
sub-title=""
>
<template #extra>
<el-button @click="clickHandler_back" type="primary">返回上一页</el-button>
</template>
</el-result>
</div>
</template>
<style lang="scss" scoped>
.error-page{
width: 100%;
display: flex;
height: 500px;
justify-content: center;
align-items: center;
}
</style>
其他路由的模拟页面
都放在了github里自己copy,也可以自行模拟
全局状态管理(pinia)
项目中我们用到的是pinia进行全局的状态管理,这是尤大推荐的一个轻量级库
创建全局挂载入口
src/store/index.ts 创建一个入口,导出一个setupStore方法
import { createPinia } from 'pinia'; //全局管理
import type { App } from 'vue';
export function setupStore(app: App<Element>): void {
const pinia = createPinia();
app.use(pinia);
}
创建用户模块
src/store/modules/user.module.ts
import { ref, reactive, computed } from 'vue';
import { defineStore } from 'pinia';
import storage from '@/libs/storage';
// 对外部暴露一个 use 方法,该方法会导出我们定义的 state
interface UserInfoRaw {
createdTime: string;
id: number | string;
phone: string;
userName: string;
}
interface loginInfoRaw {
isLogin: boolean;
token: string;
}
const useUserStore = defineStore('user', function () {
const userInfo: UserInfoRaw | null = reactive<UserInfoRaw>(
storage.get('userInfo') || {
createdTime: '',
id: '',
phone: '',
userName: ''
}
);
const loginInfo: loginInfoRaw = reactive<loginInfoRaw>({
isLogin: false || storage.get('isLogin'),
token: '' || storage.get('token')
});
// 设置登录状态
const setLogin = (data: loginInfoRaw) => {
loginInfo.isLogin = data.isLogin;
loginInfo.token = data.token;
storage.set('isLogin', true);
storage.set('token', data.token);
};
const resetLogin = () => {
loginInfo.isLogin = false;
loginInfo.token = '';
};
// 设置用户状态
const setUserInfo = (data: UserInfoRaw) => {
userInfo.createdTime = data.createdTime;
userInfo.id = data.id;
userInfo.phone = data.phone;
userInfo.userName = data.userName;
storage.set('userInfo', data);
};
// 清空用户状态
const resetUserInfo = () => {
userInfo.createdTime = '';
userInfo.id = '';
userInfo.phone = '';
userInfo.userName = '';
};
const login = (user: UserInfoRaw, data: loginInfoRaw) => {
setLogin(data);
setUserInfo(user);
};
const logout = () => {
resetLogin();
resetUserInfo();
storage.clear();
};
return {
userInfo,
setUserInfo,
resetUserInfo,
loginInfo,
setLogin,
resetLogin,
login,
logout
};
});
export default useUserStore;
登录页中使用store
仅提取部分代码展示
import useUserStore from '@/store/modules/user.module'; //导入
const UserStore = useUserStore(); //初始化
UserStore.login(results, { isLogin: true, token }); //使用
网络请求封装(axios)
网络请求方面依然是使用axios库
在http文件夹下创建request.ts文件以及api文件夹
api文件夹文件夹下创建一个loginApi.ts写入登录模块相关的请求
封装请求
这里我做了以下处理,初始化axios的实例,简单封装post,get,postJSON三种请求方式,添加全局loading设置,根据状态码处理请求响应的数据
import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import qs from 'qs';
import { Path, TIME_OUT, HTTP_REQUEST_CODE, HTTP_REQUEST_STA } from '@/constants/http';
import { ElMessage } from 'element-plus';
import router from '@/router';
import { showLoading, hideLoading } from '@/components/baseFullScreenLoading';
import useUserStore from '@/store/modules/user.module';
let UserStore: any = null;
// 配置新建一个 axios 实例
const instance: AxiosInstance = axios.create({
baseURL: Path.HTTP_URL,
timeout: TIME_OUT
});
// 添加请求拦截器
instance.interceptors.request.use(
(config: AxiosRequestConfig) => {
!UserStore && (UserStore = useUserStore());
const token = UserStore.loginInfo.token;
if (token && config.headers) {
config.headers['token'] = `${UserStore.loginInfo.token}`;
}
return config;
},
(error: AxiosError) => {
return Promise.reject(error);
}
);
// 添加响应拦截器
instance.interceptors.response.use(
(response: AxiosResponse) => {
hideLoading();
switch (response.status) {
case HTTP_REQUEST_STA.LOGIN_ERROR:
return [null];
case HTTP_REQUEST_STA.SUCCESS:
return handle_response(response);
default:
return [null];
}
},
(error: AxiosError) => {
hideLoading();
ElMessage.error('请求异常,稍后重试!');
return Promise.reject(error);
}
);
export default {
/**
* @param {String} url 接口地址
* @param {Object} data 参数
* @param {Boolean} isLoading 是否显示loading
*/
post(url: string, data = {}, isLoading: boolean = true) {
if (isLoading) {
showLoading();
}
return instance({
url,
method: 'post',
data: qs.stringify(data),
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
},
/**
* @param {String} url 接口地址
* @param {Object} data 参数
* @param {Boolean} isLoading 是否显示loading
*/
postJSON(url: string, data: any, isLoading = true) {
if (isLoading) {
showLoading();
}
return instance({
url,
method: 'post',
data: data,
headers: {
'Content-Type': 'application/json'
}
});
},
/**
* @param {String} url 接口地址
* @param {Object} data 参数
* @param {Boolean} isLoading 是否显示loading
*/
get(url: string, data: object, isLoading = true) {
if (isLoading) {
showLoading();
}
return instance({
url,
method: 'get',
params: data
});
}
};
// 处理响应回来的数据
function handle_response(response: any) {
switch (response.data.code) {
case HTTP_REQUEST_CODE.SUCCESS:
return [response.data.results ? response.data.results : true, response.data.message, response.data.code];
case HTTP_REQUEST_CODE.LOGIN_OUT:
!UserStore && (UserStore = useUserStore());
UserStore.logout();
ElMessage.error(response.data.message);
router.push('Login');
return [null, response.data.message, response.data.code];
default:
ElMessage.error(response.data.message);
}
}
封装全局loading
创建loading文件,这里我们使用el-loading
src/components/baseFullScreenLoading/index.ts
import { ElLoading } from "element-plus";
let loadingInstance:any;
const showLoading = () => {
loadingInstance = ElLoading.service({
fullscreen: true,
text: "loading...",
background: "rgba(0,0,0,0.6)",
});
};
const hideLoading = () => {
loadingInstance && loadingInstance.close();
};
export { showLoading, hideLoading };
创建请求模块
这里我们创建一个登录模块
import request from '../request'
const prefix = '/question-Server'
const login = {
// 登录接口
login: (params: any) => {
return request.postJSON(`${prefix}/user/login`, params)
},
}
export default {
...login
}
在页面中使用
import loginApi from '@/http/api/loginApi';
let [results, message, code]: any = await loginApi.login(params);
样式管理
我们在src/assets下styles文件夹,管理全局样式
全局样式定义
这部分scss代码已放置到github中,不管是vue2还是vue3项目都是通用的
elementPlus全局样式重置及全局API样式引入
这里主要是参考了elementPlus官网的写法
覆盖scss变量的写法(说实话是有点麻烦)
创建src/assets/styles/element.scss
$--header:
(
'padding': 0 0,
'height': 60px,
);
@forward "element-plus/theme-chalk/src/common/var.scss" with
(
$header:$--header
);
修改vite.config.ts
css: {
preprocessorOptions: {
scss: {
additionalData: `@use "@/assets/styles/element.scss" as *;`,
},
},
},
我们在用api的方式调用el-message时,会发现没有加载对应的样式,这是因为unplugin-vue-components是加载时引入的,所以像一些elloading,elmessage的组件样式在第一次使用的时候并不会加载进来,所以官网还是建议全局单独引入对应的scss
src/main.ts
import { createApp } from "vue";
import App from "./App.vue";
import router, { setupRouter } from "./router"; // 路由
import { setupStore } from "./store"
import "element-plus/theme-chalk/src/message.scss"
import "element-plus/theme-chalk/src/loading.scss"
import './assets/styles/index.scss'
const app = createApp(App);
setupStore(app)
setupRouter(app); // 引入路由
router.isReady().then(() => {
app.mount("#app");
});
环境变量配置
在以往使用webpack开发过程中,我们都会设置.env文件进行开发,测试,生产环境的配置
在vite同样也是使用.env文件
配置env环境变量
我们在项目根目录创建
.env.development
.env.production
.env.staging
这代表着三种模式
接着以.env.development为例子
VITE_MODE_NAME=development
VITE_BASE_URL= 'http://xxxx'
VITE_APP_TITLE=vite-admin-ts-dev
修改src/env.d.ts提供ts支持
/// <reference types="vite/client" />
declare module '*.vue' {
import { DefineComponent } from 'vue'
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>
export default component
}
interface ImportMetaEnv {
VITE_MODE_NAME: string,
VITE_BASE_URL: string,
VITE_APP_TITLE: string
}
接着修改package.json
"scripts": {
"dev": "vite --host --mode development",
"build": "vue-tsc --noEmit && vite build",
"serve": "vite preview --host"
},
代码中使用环境变量
下面举一个配置不同环境下访问服务端地址的例子
src/constants/http.ts
/**
* @description 路径常量
*/
export const Path = {
HTTP_URL: import.meta.env.VITE_BASE_URL as string
};
这样在开发和打包的时候,就会根据你执行的scripts命令去切换不同的环境变量
小结
这次开发的一个后台模版个人认为是比较粗糙的hhh,像很多功能都还没有加上去,一些ts用法非常混乱,一些架构上还是没好好设计,这个我打算是放在后期再慢慢根据需求继续完善吧,先把一个最小可运行版本放出来,后续每次完善一个模块就记录一篇。在使用elementPlus上一直都有踩坑,因为elementPlus一直在更新版本,其对应的一些自动按需引入的包也在同步更新,所以导致了一直在修改。希望大佬们如果有修改意见的话,可以多多给小弟提提hhh。