前言
经过几个周末的学习和编码,终于把「微型项目:机器管理系统」给搭建起来了。本文是对本次开发过程中遇到的问题以及一些知识点的总结。
简介
首先,开发这个系统的目的主要是记录部门机器的使用情况,并提供公示(查询)的能力,方便机器管理,提高机器有效使用率。所以,项目一开始就本着快速搭建的初衷进行,这也使得在一些功能的实现上选择了简单轻便的方式进行处理,而这也会导致一些非功能性需求的缺失(如:性能、安全)。
系统功能
- 普通用户(非登录用户)可以对机器进行查询,包括按一定条件搜索机器
- 管理员(登录用户,系统有且只有一个账号)可以对机器(宿主机和虚拟机)进行管理,包括:增加、编辑、删除
- 支持中英文两种语言切换
系统截图
- 普通用户中文界面
- 普通用户英文界面
- 登录界面
- 管理员界面
- 增加机器界面
- 增加虚拟机界面
知识点及问题
vue 组件
这个主要是用在登录模块和条件查询模块。刚开始以为这是一个很小的系统,代码量应该很少,可以直接放在一个文件中。后面随着功能的增加和完善,代码越来越多,如果全部放入一个文件,将显得臃肿难看。所以就学习应用 vue 组件的思想和功能,将比较独立的「登录」和「条件查询」这两个功能抽成「组件」进行使用。
vue 组件有三个主要点:定义组件、注册组件、使用组件、组件通信
定义组件
组件定义跟 vue 实例的定义类似,不同的是相关的 html 代码写在
template
元素中,data 元素是一个函数,这样才能保证各个组件之间不会相互影响。以下代码是条件查询的组件定义相关代码。1
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
58var queryBlock = {
props: ["machineKinds"],
data: function () {
return {
queryCondition: {}
}
},
methods: {
query() {
this.queryCondition.startPageNum = 1;
this.queryCondition.pagePerCount = 10;
this.$emit("get-data", this.queryCondition);
}
},
template: `
<el-collapse accordion>
<el-collapse-item :title="$t('message.query')">
<el-form :model="queryCondition" label-position="right" label-width="120px">
<el-row :gutter="20">
<el-col :span="12" >
<el-form-item :label="$t('message.serialNumber')">
<el-input clearable v-model="queryCondition.serialNumber" :placeholder="$t('message.tips.input.serialNumber')"></el-input>
</el-form-item>
<el-form-item :label="$t('message.type')" required>
<el-select v-model="queryCondition.kind" clearable :placeholder="$t('message.tips.input.type')">
<el-option v-for="item in machineKinds" :key="item" :label="item" :value="item"></el-option>
</el-select>
</el-form-item>
<el-form-item label="BMCIP">
<el-input clearable v-model="queryCondition.bmcIP" :placeholder="$t('message.tips.input.BMCIP')"></el-input>
</el-form-item>
<el-form-item :label="$t('message.machineIP')">
<el-input clearable v-model="queryCondition.businessIP" :placeholder="$t('message.tips.input.machineIP')"></el-input>
</el-form-item>
</el-col>
<el-col :span="12" >
<el-form-item :label="$t('message.owner')">
<el-input clearable v-model="queryCondition.owner" :placeholder="$t('message.tips.input.owner')"></el-input>
</el-form-item>
<el-form-item :label="$t('message.machineUser')">
<el-input clearable v-model="queryCondition.user" :placeholder="$t('message.tips.input.machineUser')"></el-input>
</el-form-item>
<el-form-item :label="$t('message.address')">
<el-input clearable v-model="queryCondition.virtualMachineIP" :placeholder="$t('message.tips.input.address')"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-form-item align="right">
<el-button type="primary" @click="query()">{{$t('message.query')}}</el-button>
<el-button @click="queryCondition={}">{{$t('message.reset')}}</el-button>
</el-form-item>
</el-form>
</el-collapse-item>
</el-collapse>
`
};注册组件
组件注册有两种,一种是全局注册,一种是局部注册。由于全局注册的组件影响范围较广,所以一般采用局部注册。本项目中的条件查询模块就是采用局部注册,其代码如下
1
2
3
4
5
6new Vue({
el: '#app',
components: {
"query-block": queryBlock
}
})使用组件
使用组件很简单,只需要像正常的 html 元素调用即可。如下是对条件查询组件的调用。
1
<query-block :machine-kinds="machineKinds" v-on:get-data="getData"></query-block>
组件通信
通过 prop 向子组件传值
在子组件中定义 prop 元素,这样父组件就可以通过定义的 prop 元素将父组件的值传递到子组件中,子组件使用这个值就像使用 data 元素里面的变量一样。需要注意的是,prop 元素在父组件的 html 相关代码中需要转成小写字母加横线的写法。如下代码是条件查询组件的 prop 传值
1
<query-block :machine-kinds="machineKinds"></query-block>
1
2
3var queryBlock = {
props: ["machineKinds"] // 也可以写成 props: ["machine-kinds"]
}通过 $emit 函数向父组件传值
在子组件中,调用 emit 函数,传入一个标志符和相应的值,然后在父组件中通过传递的标识符获取相应的值。如下所示,子组件传递
get-data
标识符,其值为子组件的queryCondition
,在父组件中通过v-on:get-data
调用相应的函数获取子组件传递的值。1
this.$emit("get-data", this.queryCondition);
1
2
3
4
5
6
7
8
9
10
11
12
13
14<query-block v-on:get-data="getData"></query-block>
<script>
new Vue({
el: '#app',
method: {
getData(queryCondition) {
this.queryCondition = this.copyObject(queryCondition);
this.currentPage = 1;
this.getMachines(this.queryCondition);
}
}
})
</script>
关于组件更多的内容,请参考 vue 官网
用户 token 认证
这个主要是用在登录模块中的,主要用于判断用户是否登录。同时,由于系统只有一个用户,所以,也用这个 token 进行了权限的控制(对机器的增、删、改的权限控制)。
token 的产生
在用户登录成功是,在后台产生一个 token 返回给前台,后续前台发起相关请求时携带此 token 进行身份验证。出于简便的目的,直接用 UUID 的方式产生随机 token
1
String token = UUID.randomUUID().toString();
token 的维护
用户退出登录时,清除 token
1
2
3
4
5
6
public void logout(HttpServletRequest request, HttpServletRequest reponse)
{
String token = request.getHeader("token");
TokenUtil.removeToken(token);
}用户在登录后每发起一个请求,就更新 token 的时间,否则可能会出现用户在操作(非退出登录)过程中突然 token 失效导致无法使用系统
1
2
3
4public static void updateTime(String token)
{
tokenMap.replace(token, new Date());
}前台在获取到后台返回的 token 时,将 token 保存到 cookie 中
1
localStorage.setItem("token", response.data);
前台发起请求携带 token
本项目使用的 axios 向后台发送请求,axios 允许在所有请求的头部中增加相关信息。如下代码是在请求头中加入 token 信息,这样设置之后,所有的 axios 请求都会在
Request Headers
加入 token 信息。1
2
3
4
5
6
7// 设置请求头中增加token,用于认证是否登录
axios.interceptors.request.use(function (config) {
config.headers.token = localStorage.getItem("token");
return config;
}, function (error) {
return Promise.reject(error);
});关于 axios 的详细使用可以参考在线文档
后台对 token 的校验
由于对登录过后的用户发起的每个请求都需要更新 token 时间,所有最简单的做法是增加一个过滤器,拦截所有的请求,对每个请求中的 token 进行判断,存在系统中则进行时间的更新。同时,也可以在过滤器中对 token 的有效性进行校验。判断 token 的有效性主要是该 token 是否存在系统中以及是否过期。如果 token 不合法,则抛异常,这样就不会执行真正的业务代码。
1
2
3
4
5
6
7private void validateToken(String token)
{
if (TokenUtil.isTokenExpired(token))
{
throw new RuntimeException("The token is invalid");
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public static boolean isTokenExpired(String token)
{
if (StringUtils.isEmpty(token))
{
throw new RuntimeException("The token is invalid");
}
Date date = tokenMap.get(token);
if (date == null)
{
throw new RuntimeException("The token is invalid");
}
Date currentDate = new Date();
if (date.getTime() + DURATION_TIME < currentDate.getTime())
{
return true;
}
return false;
}由于并不是所有的请求都要求登录,所以也并不是所有的请求都需要进行 token 的校验,因此,在进行 token 校验前,需要对请求进行判断,只有对机器的增删改的请求才进行校验,查询操作没有权限控制,不用进行 token 校验。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23private boolean needAuth(HttpServletRequest request)
{
String requestURI = request.getRequestURI();
String requestMethod = request.getMethod();
if (StringUtils.equals("/machine", requestURI))
{
if (StringUtils.equals("POST", requestMethod)
|| StringUtils.equals("PUT", requestMethod)
|| StringUtils.equals("DELETE", requestMethod))
{
return true;
}
return false;
}
if (StringUtils.equals("/virtualMachine", requestURI))
{
return true;
}
return false;
}
国际化
国际化主要涉及两方面,一个是使用 Element 框架组件自身的国际化,另一个是业务产生所需的国际化。
业务代码国际化
国际化主要使用的是 vue 的插件
vue-i18n
, 需要引入<script src="https://unpkg.com/vue-i18n/dist/vue-i18n.js"></script>
文件。其定义如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25const messages = {
zh: {
message: {
delete: "删除",
}
},
en: {
message: {
delete: "delete",
}
}
};
const i18n = new VueI18n({
locale: "zh", // set locale
messages, // set locale messages
});
new Vue({
el: '#app',
i18n,
data: {
}
})使用时,分三种情况,有不同的用法
html 元素
格式:
1
{{$t('message.item')}}
例子:
1
<el-button type="primary" @click="login()">{{$t('message.login')}}</el-button>
Element元素传值
格式:
1
$t('message.item')
例子:
1
<el-input v-model="user.name" :placeholder="$t('message.tips.input.username')" ></el-input>
函数
格式:
1
this.$t('message.item')
例子:
1
2
3
4this.$alert(errorMessage, this.$t('message.error'), {
confirmButtonText: this.$t('message.confirm'),
type: "error"
});
关于 vue-i18n 的更多内容,可以参考 vue-i18n官网
Element 组件国际化
Element 组件默认使用中文,当将语言切换成英文时,需要将 Element 组件的语言也切换成英文。需要引入两个 Element 国际化相关的文件
1
2<script src="https://unpkg.com/element-ui/lib/umd/locale/zh-CN.js"></script>
<script src="https://unpkg.com/element-ui/lib/umd/locale/en.js"></script>在定义业务代码国际化时,将 Element 组件的国际化包含进去。如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24const messages = {
zh: {
message: {
delete: "删除",
},
...ELEMENT.lang.zhCN
},
en: {
message: {
delete: "delete",
},
...ELEMENT.lang.en
}
};
const i18n = new VueI18n({
locale: "zh", // set locale
messages, // set locale messages
});
// very important for element i18n
Vue.use(ELEMENT, {
i18n: (key, value) => i18n.t(key, value)
});关于 Element 框架国际化的内容,请浏览 Element 官网
语言间切换
vue-i18n 提供了
$i18n.locale
变量来设置国际化语言。因此可以通过改变这个变量的值来达到切换语言的目的。代码如下1
2<el-button type="text" @click="changeLanguaue('zh')" :disabled="$i18n.locale=='zh'">中文</el-button>
<el-button type="text" @click="changeLanguaue('en')" :disabled="$i18n.locale=='en'">English</el-button>1
2
3changeLanguaue(lang) {
this.$i18n.locale = lang;
}
其他问题
请见:一个微型项目中期总结 中 「遇到的问题及解决方案」章节
参考资料
- vue 官网:https://cn.vuejs.org/v2/guide/
- axios 在线文档:http://www.axios-js.com/zh-cn/docs/
- vue-i18n 官网:http://kazupon.github.io/vue-i18n/zh/introduction.html
- Element 官网:https://element.eleme.cn/#/zh-CN/component/i18n