前面几篇文章我们介绍的都是Vite+Vue3+TypeScript项目中环境相关的配置,接下来我们开始进入系统搭建部分。本篇我们来介绍登录界面搭建及动态路由配置,大家一起撸起来......
搭建登录界面
登陆接口api
项目登陆接口是通过mockjs前端来模拟的
模拟服务接口LoginApi
首先在src/mock文件夹下新建login.ts文件,模拟两个服务接口(验证码获取+用户登录)
import { MockMethod } from 'vite-plugin-mock';
export const LoginApi: Array<MockMethod> = [
{
url: '/api/captchaImage',
method: 'get',
response: () => {
return {
msg: 'OK',
img: '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',
code: 200,
uuid: '37e7a189a9b14be6a5cbae80af43abaa',
};
},
},
{
url: '/api/login',
method: 'post',
response: () => {
return {
msg: 'OK',
code: 200,
token:
'eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6Ijc2YzYzNzczLWY2ZWEtNDlkMC05MDIyLTg4ZmUxNjI1NmMzNyJ9.XUeTaH-VZu_Mm0Rm1m_lST4YH1-ovX5Gg9w_Z4nA04agzxzeTdb5XxKCIhMr8pPatCKmiCql9E7afMY96oGYfQ',
};
},
},
];
LoginApi引入
修改src/mock文件夹下的index.ts文件,添加内容如下
import { LoginApi } from './login';
const mock: Array<MockMethod> = [...LoginApi];
创建登录API
在src/api文件夹下新建login.ts文件,创建获取验证码接口captchaImage和用户登录api接口login
import request from '@/utils/request';
export const captchaImage = () => {
return request.get('/api/captchaImage');
};
export const login = () => {
return request.post('/api/login');
};
登录界面Login.vue
在src/views路径下新建Login.vue文件,这个文件就是我们的登录界面文件
<template>
<div class="login-main">
<el-form ref="ruleFormRef" :model="ruleForm" status-icon :rules="rules" label-width="120px" class="ruleForm">
<el-form-item prop="user">
<el-input :prefix-icon="User" v-model="ruleForm.user" clearable />
</el-form-item>
<el-form-item prop="pass">
<el-input :prefix-icon="Lock" v-model="ruleForm.pass" type="password" />
</el-form-item>
<el-form-item prop="code">
<el-input :prefix-icon="Lock" v-model="ruleForm.code" class="code-value" />
<img :src="img" alt="" class="code-img">
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm(ruleFormRef)">登录</el-button>
<el-button @click="resetForm(ruleFormRef)">重置</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang='ts'>
// 方法引入
import { reactive, ref, onMounted } from 'vue'
import router from '@/router';
import { setLocalStorage } from "@/utils/localstorage";
import type { FormInstance } from 'element-plus'
// 组件引入
import { User, Lock } from '@element-plus/icons-vue'
// 接口引入
import { captchaImage, login } from "@/api/login";
onMounted(() => {
captchaImage().then(datas => {
console.log(datas)
img.value = 'data:image/gif;base64,' + datas.img
uuid.value = datas.uuid
})
})
let img = ref<any>("")
let uuid = ref<string>("")
const ruleFormRef = ref<FormInstance>()
const validateUser = (rule: any, value: any, callback: any) => {
if (value === '') {
callback(new Error('用户名不能为空'))
} else {
callback()
}
}
const validatePass = (rule: any, value: any, callback: any) => {
if (value === '') {
callback(new Error('密码不能为空'))
} else {
callback()
}
}
const validateCode = (rule: any, value: any, callback: any) => {
if (value === '') {
callback(new Error('验证码不能为空'))
} else {
callback()
}
}
const ruleForm = reactive({
user: 'wangjianlei',
pass: '123456',
code: '4'
})
const rules = reactive({
pass: [{ validator: validatePass, trigger: 'blur' }],
user: [{ validator: validateUser, trigger: 'blur' }],
code: [{ validator: validateCode, trigger: 'blur' }],
})
const submitForm = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.validate((valid) => {
if (valid) {
login()
.then(res => {
console.log(res)
setLocalStorage("LH_TOKEN", res.token)
router.push("/")
})
.catch(err => {
throw new Error(err);
})
} else {
return false
}
})
}
const resetForm = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.resetFields()
}
</script>
<style lang="scss" scoped>
.login-main {
display: flex;
padding: 25px;
.ruleForm {
width: 500px;
.code-value {
width: 260px;
}
.code-img {
margin-left: 10px;
width: 75px;
height: 30px;
}
}
}
</style>
配置动态路由
动态路由接口
项目动态路由接口同样是通过mockjs前端来模拟的
模拟服务接口HomeApi
首先在src/mock文件夹下新建home.ts文件,模拟动态路由返回服务接口
import { MockMethod } from 'vite-plugin-mock';
export const HomeApi: Array<MockMethod> = [
{
url: '/api/routerList',
method: 'get',
response: () => {
const routes = [
{
path: '/main/PageOne',
name: 'PageOne',
component: 'PageOne.vue',
},
{
path: '/main/PageTwo',
name: 'PageTwo',
component: 'PageTwo.vue',
},
{
path: '/main/PageThree',
name: 'PageThree',
component: 'PageThree.vue',
},
];
return {
msg: 'OK',
code: 200,
data: routes,
};
},
},
];
这里我们可以看到共返回了三个路由“PageOne”,“PageTwo”和“PageThree”。
HomeApi引入
修改src/mock文件夹下的index.ts文件,添加内容如下
import { MockMethod } from 'vite-plugin-mock';
import { HomeApi } from './home';
import { LoginApi } from './login';
const mock: Array<MockMethod> = [...LoginApi, ...HomeApi];
export default mock;
创建动态路由API
在src/api文件夹下新建home.ts文件,创建获取动态路由接口GetDynamicRoutes
import request from '@/utils/request';
export const GetDynamicRoutes = () => {
return request.get('/api/routerList');
};
新增路由界面
根据我们利用mockjs模拟的动态路由接口返回的数据,在src/views文件夹下创建modules文件夹,新建PageOne.vue,PageTwo.vue和PageThree.vue界面文件。
动态路由配置
我们的动态路由数据应由一个公共的地方进行管理,所以呢这里我选择利用vue的状态管理器pinia来实现这个功能。
首先我们在pinia的state中添加一个路由项routes(RouteRecordRaw类型的数组):
state: (): storeHome => {
return {
//路由表
routes: [],
};
},
然后在action中还需要添加一个根据路由数据加载动态路由的方法(updateRoutes),方法所需的路由数据和router对象由外部传入。(这里用外部传入router是为了避免循环调用router,毕竟需要进行加载动态路由的地方基本都有个router的示例对象,而外部传入的话,只需要调用然后传入一次router就可以了)
在src/store/type/home.ts添加状态项routes类型定义:
import { RouteRecordRaw } from 'vue-router';
export type storeHome = {
routes: Array<RouteRecordRaw>;
};
加载路由的思路其实也很简单,首先解析咱调用接口后传入的路由数据,根据路由的数据类型生成对应的路由表,并存储到pinia中,然后直接遍历这个pinia中的路由表,使用router.addRoute()方法将路由加载进去。router.addRoute()方法支持传如两个参数,方便我们在指定位置的路由中插入children,这种情况下第一个参数是父级路由的name,第二个参数就是要添加的children路由对象。
需要我们注意的是:vite使用动态路由,在动态导入路由组件的时候,需要特别注意不能将页面路径直接作为component导入,虽然开发环境一般是能正常加载,但是打包到生产环境的时候十有八九会报错,所以我们需要添加以下代码:
//根据自己项目实际目录结构组织,注意这里的“../../”不能用“@”别名代替
let modules = import.meta.glob('../../views/modules/*.vue');
然后用modules形式引入,完整动态路由的pinia代码如下:
import { defineStore } from 'pinia';
import { storeHome } from '../types/home';
let modules = import.meta.glob('../../views/modules/*.vue');
export const useHomeStore = defineStore('index', {
state: (): storeHome => {
return {
//路由表
routes: [],
};
},
getters: {},
actions: {
updateRoutes(data: Array<any>, router: any) {
this.routes = [];
data.forEach((el) => {
this.routes.push({
path: el.path,
name: el.name,
component: modules[`../../views/modules/${el.component}`],
});
});
this.routes.forEach((el) => {
router.addRoute('Home', el);
// router.addRoute();
});
},
},
});
加载动态路由
上面我们已经配置好了路由接口和加载路由的方法,加载动态路由的思路也很简单,在我们的初始页面中调用路由的数据接口,在获取到数据之后调用加载的方法即可。
// 方法引入
import router from '@/router';
// 接口引入
import { GetDynamicRoutes } from "@/api/home"
// 状态管理器引入
import { useHomeStore } from "@/store/modules/home";
const homeStore = useHomeStore()
onBeforeMount(() => {
if (!getLocalStorage("LH_TOKEN")) {
router.push("/login")
} else {
alert("登陆成功")
GetDynamicRoutes().then(res => {
homeStore.updateRoutes(res.data, router)
})
finish.value = true
}
})
为验证我们的路由是否被加载成功,我们可以在调用动态路由同时创建对应的按钮,以便我们进行路由跳转。
<el-button v-for="item in routes" :key="item.name" @click="handleClick(item.path)">{{ item.name }}</el-button>
const routes = computed(() => homeStore.routes)
// 路由按钮点击事件
const handleClick = (path: string) => {
router.push(path)
}
完整页面代码如下:
<template>
<div class="home-main" v-if="finish">
<div>
<el-button v-for="item in routes" :key="item.name" @click="handleClick(item.path)">{{ item.name }}</el-button>
</div>
<RouterView />
</div>
</template>
<script setup lang='ts'>
// 方法引入
import { reactive, ref, onBeforeMount, onMounted } from 'vue'
import { computed } from "@vue/reactivity";
import { getLocalStorage } from '@/utils/localstorage'
import router from '@/router';
// 组件引入
import HomeHeader from './home/HomeHeader.vue';
// 接口引入
import { GetDynamicRoutes } from "@/api/home"
// 状态管理器引入
import { useHomeStore } from "@/store/modules/home";
const homeStore = useHomeStore()
onBeforeMount(() => {
if (!getLocalStorage("LH_TOKEN")) {
router.push("/login")
} else {
alert("登陆成功")
GetDynamicRoutes().then(res => {
homeStore.updateRoutes(res.data, router)
})
finish.value = true
}
})
const routes = computed(() => homeStore.routes)
// 路由按钮点击事件
const handleClick = (path: string) => {
router.push(path)
}
let finish = ref(false)
</script>
<style lang="scss" scoped>
.home-main {}
</style>
动态路由效果
配置路由守卫
其实截止上面,我们的动态路由已经加载成功了。不过仔细测试会发现,还会有一个问题bug,假如我们刷新跳转后的页面,或者直接使用动态路由的路径进行跳转,就会出现报错“no match found for location with path '/PageOne' ”。添加的动态路由失效了,页面也没有显示,这是因为我们的路由和状态管理器pinia在刷新之后都会被重置,而我们加载路由的方法是在系统初始页面被调用的,当我们直接F5刷新页面或者直接输入路由路径的时候,初始页面其实并没有被加载,也就是说我们的动态路由并没有被加载上去,自然这个动态的页面也就丢失了。
这里我们可以通过添加路由守卫的方式来解决这个问题,大致思路:假如我们的页面请求路径不是我们定义的初始路径的时候,我们就在路由守卫中要求在跳转之前先去查询状态管理器中是否存在我们的动态路由,或者该动态路由是否满足我们的初始页面跳转要求,若不满足则请求动态路由接口并加载我们的动态路由,在加载完成后再继续执行页面跳转操作。
路由守卫代码如下:
// 路由守卫
router.beforeEach((to, from, next) => {
if (to.path !== '/main' && to.path !== '/') {
const store = useHomeStore();
if (store.routes.length < 1) {
GetDynamicRoutes()
.then((res) => {
store.updateRoutes(res.data, router);
next({ path: to.path, replace: true });
})
.catch((_) => {
next();
});
} else {
next();
}
} else {
next();
}
});
最终效果
至此,登录界面和动态路由基本搭建就完成了。
我相信,每天学习一点点,收获成长亿点点!