前端项目设计
# 16.3 前端项目设计
vhr 微人事项目的前端使用的技术栈是 vue,下面我们简要介绍前端项目是如何运作并连接到后端服务的。
该项目是通过 Vue-CLI 创建的,具有 vue 脚手架项目的通用结构。
# 16.3.1 Vue CLI 简介
Vue CLI 是一个基于 Vue.js 进行快速开发的完整系统,提供:
- 通过
@vue/cli
搭建交互式的项目脚手架。 - 通过
@vue/cli
+@vue/cli-service-global
快速开始零配置原型开发。 - 一个运行时依赖 (
@vue/cli-service
),该依赖:- 可升级;
- 基于 webpack 构建,并带有合理的默认配置;
- 可以通过项目内的配置文件进行配置;
- 可以通过插件进行扩展。
- 一个丰富的官方插件集合,集成了前端生态中最好的工具。
- 一套完全图形化的创建和管理 Vue.js 项目的用户界面。
Vue CLI 致力于将 Vue 生态中的工具基础标准化。它确保了各种构建工具能够基于智能的默认配置即可平稳衔接,这样你可以专注在撰写应用上,而不必花好几天去纠结配置的问题。与此同时,它也为每个工具提供了调整配置的灵活性,无需 eject。
# 16.3.2 全局配置
vhr 项目使用了“饿了么”开源的 ElementUI 界面组件,在 /src/main.js 文件中引入:
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI,{size:'small'});
2
3
使用 Axios 库做前端和后端的 ajax 交互,并对网络异常等情况处理后封装到 /src/utils/api.js 中,在 /src/main.js 文件中引入配置为 Vue 的 prototype:
import {postRequest} from "./utils/api";
import {postKeyValueRequest} from "./utils/api";
import {putRequest} from "./utils/api";
import {deleteRequest} from "./utils/api";
import {getRequest} from "./utils/api";
Vue.prototype.postRequest = postRequest;
Vue.prototype.postKeyValueRequest = postKeyValueRequest;
Vue.prototype.putRequest = putRequest;
Vue.prototype.deleteRequest = deleteRequest;
Vue.prototype.getRequest = getRequest;
2
3
4
5
6
7
8
9
10
配置完成后,对后续的所有网络请求,就可以通过类似 this.postRequest(url, param) 方式访问后台逻辑。
项目根目录中的 vue.config.js 全局配置文件中配置了前端如何代理到后端的。
let proxyObj = {};
proxyObj['/ws'] = {
ws: true,
target: "ws://localhost:8081"
};
proxyObj['/'] = {
ws: false,
target: 'http://localhost:8081',
changeOrigin: true,
pathRewrite: {
'^/': ''
}
}
module.exports = {
devServer: {
host: 'localhost',
port: 8080,
proxy: proxyObj
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
上述配置了 npm run serve 启动开发服务器时,主机为 localhost,端口为 8080,使用前端代理到后端 http://localhost:8080 上面获取后台逻辑。
vue.config.js
是一个可选的配置文件,如果项目的 (和package.json
同级的) 根目录中存在这个文件,那么它会被@vue/cli-service
自动加载。你也可以使用package.json
中的vue
字段,但是注意这种写法需要你严格遵照 JSON 的格式来写。
在 router.js 文件中配置了路由,引入 Login、Home等组件。
import Vue from 'vue'
import Router from 'vue-router'
import Login from './views/Login.vue'
import Home from './views/Home.vue'
import FriendChat from './views/chat/FriendChat.vue'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'Login',
component: Login,
hidden:true
}, {
path: '/home',
name: 'Home',
component: Home,
hidden:true,
meta:{
roles:['admin','user']
},
children:[
{
path: '/chat',
name: '在线聊天',
component: FriendChat,
hidden:true
}
]
}
]
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 16.3.3 用户登录
路由文件 router.js 中将 web 的根路由到 Login 组件,也就是用户输入 http://localhost:8080/ (opens new window) 就访问的是 ./views/Login.vue 这个组件。
import Login from './views/Login.vue'
export default new Router({
routes: [
{
path: '/',
name: 'Login',
component: Login,
hidden:true
}, {
...
2
3
4
5
6
7
8
9
10
在 views 目录下的 Login.vue 就是用户登录页面。
<template>
<div>
<el-form
:rules="rules"
ref="loginForm"
v-loading="loading"
element-loading-text="正在登录..."
element-loading-spinner="el-icon-loading"
element-loading-background="rgba(0, 0, 0, 0.8)"
:model="loginForm"
class="loginContainer">
<h3 class="loginTitle">系统登录</h3>
<el-form-item prop="username">
<el-input size="normal" type="text" v-model="loginForm.username" auto-complete="off"
placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input size="normal" type="password" v-model="loginForm.password" auto-complete="off"
placeholder="请输入密码" @keydown.enter.native="submitLogin"></el-input>
</el-form-item>
<el-checkbox size="normal" class="loginRemember" v-model="checked"></el-checkbox>
<el-button size="normal" type="primary" style="width: 100%;" @click="submitLogin">登录</el-button>
</el-form>
</div>
</template>
<script>
export default {
name: "Login",
data() {
return {
loading: false,
loginForm: {
username: 'admin',
password: '123'
},
checked: true,
rules: {
username: [{required: true, message: '请输入用户名', trigger: 'blur'}],
password: [{required: true, message: '请输入密码', trigger: 'blur'}]
}
}
},
methods: {
submitLogin() {
this.$refs.loginForm.validate((valid) => {
if (valid) {
this.loading = true;
this.postKeyValueRequest('/doLogin', this.loginForm).then(resp => {
this.loading = false;
if (resp) {
this.$store.commit('INIT_CURRENTHR', resp.obj);
window.sessionStorage.setItem("user", JSON.stringify(resp.obj));
let path = this.$route.query.redirect;
this.$router.replace((path == '/' || path == undefined) ? '/home' : path);
}
})
} else {
this.$message.error('请输入所有字段');
return false;
}
});
}
}
}
</script>
<style>
.loginContainer {
border-radius: 15px;
background-clip: padding-box;
margin: 180px auto;
width: 350px;
padding: 15px 35px 15px 35px;
background: #fff;
border: 1px solid #eaeaea;
box-shadow: 0 0 25px #cac6c6;
}
.loginTitle {
margin: 15px auto 20px auto;
text-align: center;
color: #505458;
}
.loginRemember {
text-align: left;
margin: 0px 0px 15px 0px;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
登录页面输入用户名和密码后,单击登录页面,触发 submitLogin() 方法,使用 this.postKeyValueRequest(/src/utils/api.js 中的 export const postKeyValueRequest = (url, params) => {
)携带 username 和 password 参数以 post 方式访问 /doLogin (根据代理设置,就是去访问 http://localhost:8081/doLogin, 由 vhr-web 后端项目的 SecurityConfig 配置类中的 configure(HttpSecurity http) 方法中配置的 .formLogin().loginProcessingUrl("/doLogin") 提供服务)完成登录。
submitLogin() {
this.$refs.loginForm.validate((valid) => {
if (valid) {
this.loading = true;
this.postKeyValueRequest('/doLogin', this.loginForm).then(resp => {
this.loading = false;
if (resp) {
this.$store.commit('INIT_CURRENTHR', resp.obj);
window.sessionStorage.setItem("user", JSON.stringify(resp.obj));
let path = this.$route.query.redirect;
this.$router.replace((path == '/' || path == undefined) ? '/home' : path);
}
2
3
4
5
6
7
8
9
10
11
12
13
# 16.3.4 加载菜单
首页中根据用户分配的权限加载菜单,最终是由 vhr-mapper 后端项目的 MenuMapper 提供的数据。
public interface MenuMapper {
List<Menu> getMenusByHrId(Integer hrid);
...
2
3
为了在页面导航的过程中,避免多次去后台请求菜单数据,将数据存储在 store 中。
/src/store/index.js 文件中,在 store 中创建 routes 数组。
Vue.use(Vuex)
...
const store = new Vuex.Store({
state: {
routes: [],
...
},
mutations: {
initRoutes(state, data) {
state.routes = data;
},
...
2
3
4
5
6
7
8
9
10
11
12
在 /src/utils/menus.js 文件的 initMenu 方法中,初始化菜单并将数据存储到 store 中。
export const initMenu = (router, store) => {
if (store.state.routes.length > 0) { //已初始化过
return;
}
//没初始化过,则请求后端接口获取数据进行转换并初始化
getRequest("/system/config/menu").then(data => {
if (data) {
let fmtRoutes = formatRoutes(data); //转换
router.addRoutes(fmtRoutes);
store.commit('initRoutes', fmtRoutes); //提交mutation更改state
store.dispatch('connect');
}
})
}
export const formatRoutes = (routes) => {
let fmRoutes = [];
routes.forEach(router => {
let {
path,
component,
name,
meta,
iconCls,
children
} = router;
if (children && children instanceof Array) {
children = formatRoutes(children);
}
let fmRouter = {
path: path,
name: name,
iconCls: iconCls,
meta: meta,
children: children,
component(resolve) {
if (component.startsWith("Home")) {
require(['../views/' + component + '.vue'], resolve);
} else if (component.startsWith("Emp")) {
require(['../views/emp/' + component + '.vue'], resolve);
} else if (component.startsWith("Per")) {
require(['../views/per/' + component + '.vue'], resolve);
} else if (component.startsWith("Sal")) {
require(['../views/sal/' + component + '.vue'], resolve);
} else if (component.startsWith("Sta")) {
require(['../views/sta/' + component + '.vue'], resolve);
} else if (component.startsWith("Sys")) {
require(['../views/sys/' + component + '.vue'], resolve);
}
}
}
fmRoutes.push(fmRouter);
})
return fmRoutes;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
后端 vhr-web 项目的 SystemConfigController 类的 getMenusByHrId 方法(拦截 /system/config/menu 请求)提供菜单数据。
@RestController
@RequestMapping("/system/config")
public class SystemConfigController {
@Autowired
MenuService menuService;
@GetMapping("/menu")
public List<Menu> getMenusByHrId() {
return menuService.getMenusByHrId();
}
}
2
3
4
5
6
7
8
9
10
登录之后刷新的话,数据保存在 vuex 中,在内存里,如果此时用户在其他页面时按了 F5 刷新,页面将重新刷新,而页面重新刷新的话,initMenu 方法将不会调用。那什么时候调用 initMenu 合适呢?
这时,可以使用全局前置守卫(router.beforeEach)来完成 initMenu 的调用。
然后在 /src/main.js 中引入 /src/store/index.js 和 /src/utils/menus.js 在 router.beforeEach 中完成 initMenu 的调用。
import store from './store'
import {initMenu} from "./utils/menus";
...
router.beforeEach((to, from, next) => {
if (to.path == '/') {
next();
}else {
if (window.sessionStorage.getItem("user")) {
initMenu(router, store);
next();
}else{
next('/?redirect='+to.path);
}
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
登录完成后,加载 /home 页面,由 ./views/Home.vue 组件的<el-submenu :index="index+''" v-for="(item,index) in routes"
加载菜单。
<el-aside width="200px">
<el-menu router unique-opened>
<el-submenu :index="index+''" v-for="(item,index) in routes" v-if="!item.hidden" :key="index">
<template slot="title">
<i style="color: #409eff;margin-right: 5px" :class="item.iconCls"></i>
<span>{{item.name}}</span>
</template>
<el-menu-item :index="child.path" v-for="(child,indexj) in item.children" :key="indexj">
{{child.name}}
</el-menu-item>
</el-submenu>
</el-menu>
</el-aside>
2
3
4
5
6
7
8
9
10
11
12
13
菜单数据在 routes 中,是通过 computed 计算属性从 store 中获取菜单数据。
computed: {
routes() {
return this.$store.state.routes;
}
},
2
3
4
5
在 Home 组件中注销登录,需要将 store 中的用户菜单数据清空。
if (cmd == 'logout') {
this.$confirm('此操作将注销登录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.getRequest("/logout"); //后台注销登录
window.sessionStorage.removeItem("user")
this.$store.commit('initRoutes', []); //清空当前登录用户的菜单数据
this.$router.replace("/");
}).catch(() => {
this.$message({
type: 'info',
message: '已取消操作'
});
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17