Springboot+Vue架构设计(一)
项目中的文件来自B站视频(程序员青戈) https://www.bilibili.com/video/BV1U44y1W77D
前端设计
assets
文件夹通常用于存放静态资源文件,例如图像、CSS、字体等。components
文件夹通常用于存放 Vue.js 组件。组件是Vue.js应用程序中的可复用代码块,可以被其他组件或应用程序使用。router
文件夹通常用于存放 Vue Router 相关的文件。Vue Router 是Vue.js官方提供的路由管理器,它能够让开发者通过声明式的方式配置应用程序的路由,从而实现页面跳转、参数传递等功能。store
文件夹通常用于存放 Vuex 相关的文件。Vuex 是Vue.js官方提供的状态管理库,它能够帮助开发者管理应用程序中的各种状态,例如用户登录状态、购物车中的商品列表等。utils
文件夹通常用于存放通用的工具函数和模块,例如日期格式化、字符串处理、HTTP 请求等。views
文件夹通常用于存放应用程序中的页面组件,每个页面通常对应一个独立的Vue组件。App.vue
是Vue.js应用程序的主组件,通常包含应用程序的整体布局和逻辑。main.js
是Vue.js应用程序的入口文件,通常用于创建Vue实例、注册组件、配置插件等。babel.config.js
是Babel编译器的配置文件,通常用于配置编译器的插件和预设,从而支持特定的JavaScript语法和功能。
介绍components与views的区别
在Vue.js中,组件(components)和视图(views)都可以是Vue文件,它们的主要区别在于它们所代表的概念和它们在应用程序中的用途。
组件通常表示应用程序中的可复用UI组件,例如按钮、卡片、表单控件等等。在Vue应用程序中,可以创建一个组件来表示这些UI元素,并在应用程序的不同部分重复使用它们。组件可以具有自己的状态和行为,这些状态和行为可以通过父组件传递下来进行配置和控制。
视图通常表示应用程序中的不同页面或页面的部分。视图可以通过一个或多个组件来组成,但是它们的主要目的是提供一种组织和显示用户界面的方式。视图可以包含逻辑来管理用户交互,并且通常包含一些由组件组成的UI元素。
因此,组件和视图在Vue应用程序中具有不同的角色和用途。组件用于表示可重用的UI元素,视图用于组织和显示页面或页面的部分。
介绍components下的文件
- Aside(侧边栏菜单组件)
组件中使用了v-for指令对menus数组进行遍历,根据每个item是否包含path属性决定是添加el-menu-item还是el-submenu,其中el-menu-item代表菜单项,el-submenu代表子菜单。
<div v-for="item in menus" :key="item.id">
<div v-if="item.path">
<el-menu-item :index="item.path">
<i :class="item.icon"></i>
<span slot="title">{{ item.name }}</span>
</el-menu-item>
</div>
<div v-else>
<el-submenu :index="item.id + ''">
<template slot="title">
<i :class="item.icon"></i>
<span slot="title">{{ item.name }}</span>
</template>
<div v-for="subItem in item.children" :key="subItem.id">
<el-menu-item :index="subItem.path">
<i :class="subItem.icon"></i>
<span slot="title">{{ subItem.name }}</span>
</el-menu-item>
</div>
</el-submenu>
</div>
</div>
- Header(头部组件)
该组件通过一个 flex 布局来实现左右两侧内容的对齐。左侧包括一个展开/折叠菜单的按钮以及一个面包屑导航,右侧包括一个下拉框,用于展示用户信息和一些操作。
可以通过传递不同的user对象来显示不同的用户信息,如头像、昵称等。这种灵活性使得该组件在不同的场景下都能够得到合适的应用。
<template>
<div style="line-height: 60px; display: flex">
<div style="flex: 1;">
<span :class="collapseBtnClass" style="cursor: pointer; font-size: 18px" @click="collapse"></span>
<el-breadcrumb separator="/" style="display: inline-block; margin-left: 10px">
<el-breadcrumb-item :to="'/'">首页</el-breadcrumb-item>
<el-breadcrumb-item>{{ currentPathName }}</el-breadcrumb-item>
</el-breadcrumb>
</div>
<el-dropdown style="width: 150px; cursor: pointer; text-align: right">
<div style="display: inline-block">
<img :src="user.avatarUrl" alt=""
style="width: 30px; border-radius: 50%; position: relative; top: 10px; right: 5px">
<span>{{ user.nickname }}</span><i class="el-icon-arrow-down" style="margin-left: 5px"></i>
</div>
<el-dropdown-menu slot="dropdown" style="width: 100px; text-align: center">
<el-dropdown-item style="font-size: 14px; padding: 5px 0">
<router-link to="/password">修改密码</router-link>
</el-dropdown-item>
<el-dropdown-item style="font-size: 14px; padding: 5px 0">
<router-link to="/person">个人信息</router-link>
</el-dropdown-item>
<el-dropdown-item style="font-size: 14px; padding: 5px 0">
<span style="text-decoration: none" @click="logout">退出</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</template>
介绍router下的文件
import Vue from 'vue'
import VueRouter from 'vue-router'
import store from "@/store";
Vue.use(VueRouter)
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('../views/Login.vue')
},
{
path: '/register',
name: 'Register',
component: () => import('../views/Register.vue')
},
{
path: '/404',
name: '404',
component: () => import('../views/404.vue')
},
{
path: '/front',
name: 'Front',
component: () => import('../views/front/Front'),
children: [
{
path: 'home',
name: 'FrontHome',
component: () => import('../views/front/Home.vue')
},
{
path: 'item1',
name: 'Item1',
component: () => import('../views/front/Item1.vue')
},
{
path: 'person',
name: 'FrontPerson',
component: () => import('../views/front/Person')
},
{
path: 'password',
name: 'FrontPassword',
component: () => import('../views/front/Password')
},
{
path: 'video',
name: 'Video',
component: () => import('../views/front/Video')
},
{
path: 'videoDetail',
name: 'VideoDetail',
component: () => import('../views/front/VideoDetail')
},
{
path: 'article',
name: 'FrontArticle',
component: () => import('../views/front/Article')
},
{
path: 'articleDetail',
name: 'ArticleDetail',
component: () => import('../views/front/ArticleDetail')
},
]
},
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
// 提供一个重置路由的方法
export const resetRouter = () => {
router.matcher = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
}
// 具体代码见下面的功能点介绍
export default router
这段代码是一个Vue应用中的路由配置文件。通过导入Vue
、VueRouter
、以及应用中的store
对象来进行路由的配置。
首先定义了一个路由数组routes
,其中包含了一些路由对象,包括登录、注册、404页面以及应用中的其他页面路由。
然后使用VueRouter
的构造函数创建了一个router
实例,其中通过routes
属性指定了路由数组。
在代码的后面,提供了一个resetRouter
方法,用于重置路由。同时还提供了一个setRoutes
方法,用于根据从localStorage
中读取的菜单数据动态生成路由。当菜单发生变化时,可以调用setRoutes
方法重新生成路由,从而实现动态路由的更新。
在路由实例上使用了beforeEach
钩子函数,用于在导航切换前进行一些操作。在这里,我们根据用户的菜单数据动态生成路由,并通过addRoute
方法将其添加到路由中。同时,还在localStorage
中存储了当前路由的名称,并通过store
对象提交了setPath
方法来更新store
中的路径信息。
最后,通过export default router
将路由实例导出,以便在应用的其他组件中使用路由。
有一些值得注意的点:
1、这里使用了动态导入组件的方式,通过import
方法动态地导入../views/Login.vue
组件,并将其作为Login
路由的组件。其他路由的配置中也使用了类似的方式引用组件。这种方式可以减小打包后的文件大小,只有访问到该路由时才会去请求加载对应的组件,从而提高页面的加载速度。
{
path: 'article',
name: 'FrontArticle',
component: () => import('../views/front/Article')
}
替换了传统的
import IndexGoods from "@/components/IndexGoods.vue";
{
path: 'goods',
name: '商品',
component: IndexGoods
},
2、在代码的后面,提供了一个resetRouter
方法,用于重置路由。同时还提供了一个setRoutes
方法,用于根据从localStorage
中读取的菜单数据动态生成路由。当菜单发生变化时,可以调用setRoutes
方法重新生成路由,从而实现动态路由的更新。
export const setRoutes = () => {
const storeMenus = localStorage.getItem("menus");
if (storeMenus) {
// 获取当前的路由对象名称数组
const currentRouteNames = router.getRoutes().map(v => v.name)
if (!currentRouteNames.includes('Manage')) {
// 拼装动态路由
const manageRoute = { path: '/', name: 'Manage', component: () => import('../views/Manage.vue'), redirect: "/home", children: [
{ path: 'person', name: '个人信息', component: () => import('../views/Person.vue')},
{ path: 'password', name: '修改密码', component: () => import('../views/Password.vue')}
] }
const menus = JSON.parse(storeMenus)
menus.forEach(item => {
if (item.path) { // 当且仅当path不为空的时候才去设置路由
let itemMenu = { path: item.path.replace("/", ""), name: item.name, component: () => import('../views/' + item.pagePath + '.vue')}
manageRoute.children.push(itemMenu)
} else if(item.children.length) {
item.children.forEach(item => {
if (item.path) {
let itemMenu = { path: item.path.replace("/", ""), name: item.name, component: () => import('../views/' + item.pagePath + '.vue')}
manageRoute.children.push(itemMenu)
}
})
}
})
// 动态添加到现在的路由对象中去
router.addRoute(manageRoute)
}
}
}
替换了传统的静态路由配置,可以根据用户的不同动态生成,提高了系统的可扩展性和灵活性。
3、使用路由守卫进行两个判断逻辑:
-
未找到路由的情况:如果用户访问了一个不存在的路由,代码会从本地存储中获取用户的菜单权限(menus),如果存在,则跳转到 “/404” 页面;如果不存在,则跳转到登录页面 (“/login”)。
-
其他情况:如果用户访问的路由存在于路由表中,则直接放行,允许用户访问;否则,根据第一种情况的判断逻辑进行处理。
// 重置我就再set一次路由
setRoutes()
router.beforeEach((to, from, next) => {
localStorage.setItem("currentPathName", to.name) // 设置当前的路由名称
store.commit("setPath")
// 未找到路由的情况
if (!to.matched.length) {
const storeMenus = localStorage.getItem("menus")
if (storeMenus) {
next("/404")
} else {
// 跳回登录页面
next("/login")
}
}
// 其他的情况都放行
next()
})
介绍store下的文件
import Vue from 'vue'
import Vuex from 'vuex'
import router, {resetRouter} from "@/router";
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
currentPathName: ''
},
mutations: {
setPath (state) {
state.currentPathName = localStorage.getItem("currentPathName")
},
logout() {
// 清空缓存
localStorage.removeItem("user")
localStorage.removeItem("menus")
router.push("/login")
// 重置路由
resetRouter()
}
}
})
export default store
这段代码是定义了一个 Vuex store,主要用于管理全局的状态,包括当前访问的路由名称以及用户的登出操作。
具体来说,它定义了一个状态 state,包含一个 currentPathName 属性,用于保存当前路由名称。同时,它还定义了两个 mutations,setPath 和 logout,分别用于更新当前路由名称和执行用户登出操作。
在 logout 中,除了清空用户的缓存信息外,还会调用 router.push() 方法,将路由跳转到登录页面。同时,也会调用 resetRouter() 方法,该方法会重置 Vue Router 中的路由表,清空原有的路由配置并重新添加初始的静态路由。这么做是为了防止用户登出后仍然可以通过浏览器的前进或后退按钮访问到敏感页面,确保用户登出后不会访问到需要权限的页面。
介绍utils下的文件
这是一个封装了 axios 的模块,用于发送 HTTP 请求。其中,创建了一个名为 request
的 axios 实例,并通过拦截器来对请求和响应做一些处理。
具体来说,这个模块做了以下几个功能:
- 创建一个名为
request
的 axios 实例 - 设置
request
的请求拦截器,对请求进行统一处理。 - 设置
request
的响应拦截器,对响应进行统一处理。
这个模块的主要作用是简化 HTTP 请求的发送过程,使得应用中的代码更加简洁和易于维护。同时,通过拦截器的使用,可以对请求和响应进行统一处理,提高了代码的复用性和可维护性。
- 创建一个名为
request
的 axios 实例,并设置了基本的配置项,比如请求超时时间、接口地址等。
const request = axios.create({
baseURL: `http://${serverIp}:8081`,
timeout: 30000
})
配置了baseURL就简化了发请求,可以删除烦人的http://localhost:8081
created() {
this.request.get("/echarts/file/front/all").then(res => {
console.log(res.data)
this.files = res.data.filter(v => v.type === 'png' || v.type === 'jpg' || v.type === 'webp')
})
},
- 设置
request
的请求拦截器,对请求进行统一处理。具体来说,拦截器中设置了请求头的Content-Type
,并从本地存储中读取用户信息,如果有用户信息,则将其 token 值设置到请求头中,以便服务端做身份验证。
具体来说,request.interceptors.request.use()
函数用于向axios的请求拦截器中添加一个新的拦截器,这个拦截器在每次请求发送前都会被调用,并且可以对请求的配置信息进行修改。
在这个函数中,首先通过config.headers['Content-Type']
语句设置请求头中的Content-Type字段为"application/json;charset=utf-8",表明请求的数据格式为JSON格式。
接下来,通过localStorage.getItem("user")
获取本地存储中的"user"对象,如果"user"存在,则通过config.headers['token']
将其存储的token添加到请求头中,这样后端服务可以根据这个token来判断请求的合法性。
这个代码片段中的config.headers['token']
就是用来设置JWT令牌的请求头,后端根据这个token
来验证用户的身份。
request.interceptors.request.use(config => {
config.headers['Content-Type'] = 'application/json;charset=utf-8';
let user = localStorage.getItem("user") ? JSON.parse(localStorage.getItem("user")) : null
if (user) {
config.headers['token'] = user.token; // 设置请求头
}
return config
}, error => {
return Promise.reject(error)
});
- 设置
request
的响应拦截器,对响应进行统一处理。具体来说,拦截器中首先判断是否返回了文件,如果是,则直接返回文件数据;否则,将响应数据解析为 JSON 对象,并检查响应状态码,如果状态码为 401,则说明身份验证不通过,跳转到登录页面。
这样设计有几个好处:
- 兼容服务端返回的数据类型:有些服务端返回的数据是字符串格式,需要先将其解析成 JSON 格式的数据,这样前端才能更方便地使用它们。
- 统一处理权限问题:如果请求返回的状态码是 401(未授权)的话,就会执行
router.push("/login")
将用户跳转到登录页面,这样用户就可以重新登录并获取授权。这样的做法可以提升用户体验,同时也保证了系统的安全性。 - 处理文件下载:如果返回的是文件类型,直接返回数据,对于文件下载来说一般,一般不需要进行JWT校验。因为文件下载通常是匿名的,无需身份验证即可下载。而对于其他需要身份验证的请求,如获取用户信息等,需要校验用户的身份信息,因此需要在请求头中携带JWT token进行身份验证。。
request.interceptors.response.use(
response => {
let res = response.data;
// 如果是返回的文件
if (response.config.responseType === 'blob') {
return res
}
// 兼容服务端返回的字符串数据
if (typeof res === 'string') {
res = res ? JSON.parse(res) : res
}
// 当权限验证不通过的时候给出提示
if (res.code === '401') {
// ElementUI.Message({
// message: res.msg,
// type: 'error'
// });
router.push("/login")
}
return res;
},
error => {
console.log('err' + error) // for debug
return Promise.reject(error)
}
)
介绍App.vue
<template>
<div id="app">
<router-view/>
</div>
</template>
介绍main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import './assets/gloable.css'
import request from "@/utils/request";
// main.js全局注册
import mavonEditor from 'mavon-editor'
import 'mavon-editor/dist/css/index.css'
// use
Vue.use(mavonEditor)
Vue.config.productionTip = false
Vue.use(ElementUI, { size: "mini" });
Vue.prototype.request=request
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
具体介绍一下
Vue.prototype.request=request
在Vue.js应用程序中,经常需要使用到网络请求,为了方便在各个组件中使用相同的请求库,可以将请求库注册在Vue原型中,这样每个组件都可以通过this.$request
来访问到请求库。在这个例子中,request
是一个自定义的请求库,使用Vue.prototype.request=request
可以将其注册在Vue原型中。这样在组件中就可以直接使用this.$request
来发起网络请求,而无需在每个组件中都引入和创建请求库。