Vuetify看了几天,发现先把组件过一遍,心里有数,就直接上吧。
- 前端设计
- 后端的继续改进
- 自动登录的逻辑
- Login组件
- 投票页面
前端设计
简单的View和组件设计
简单想了一下流程,分开两个视图,一个是登录视图(路径为/login
),组件为登录表单;一个是展示投票页面的视图(路径为是根路径),组件是投票表单+登录展示。
主要业务逻辑如下:
- 项目初始化
- vuex从尝试从本地加载TOKEN和用户名
- 用户访问首页
- before导航守卫判断vuex中是否有TOKEN,没有则转至登录视图
- 如果有TOKEN,向后端发送TOKEN获取投票信息
- 正常获得响应,使用vote中的数据进行渲染--正常进入投票视图
- 出现401错误,表示TOKEN认证失败--转至登录视图
- 出现404错误,表示用户没有投过票。此时只渲染投票组件中投票部分,不展示投票数量,让用户进行投票之后再展示投票结果。
- 用户访问登录视图
- 导航守卫不做任何验证-->所有用户都可以访问登录视图
- 用户发送登录请求-->成功取得200响应-->将TOKEN和用户名,是否投票,上次投票日期等信息加载至Vuex并存储到本地存储中,然后转至首页。
- 用户发送登录请求-->返回任何错误代码-->依然返回登录视图,但是需要通过SLOT插入错误信息。
- 用户选择了组件之后点击发送按钮
- 如果获取201响应,则表示成功进行投票。
- 如果获取406页面,想办法在页面上提示每24小时只能投一次票,什么也不做,还是留在当前页面。
- 如果获取401响应,表示TOKEN出现问题,将用户转至登录页面。
这样设计之后,Vue开发的过程在我脑子里基本形成了URL--》视图逻辑--》载入数据--》分发给组件。仔细想了一下,其实和后端有异曲同工之妙,因为前端切换视图在传统后端里等于访问不同的URL,自然就是加载数据的过程,然后把数据放到页面上渲染出来的过程,就是载入数据然后分发给组件进行渲染的过程。
一对比,前端的逻辑就清晰好多了。
不过实际编写的时候,发现细节问题还是很多,所以决定重构一下后端,将所有的请求放行到控制器中来进行处理,这样可以比较方便的处理跨域问题,同时使用Spring Security来简单允许跨域。
后端的继续改进
主要是在返回投票结果的地方做了一些改进,不再返回错误码,而是如果用户未投过票,就返回一个-1的投票总数;此外还返回一个expireTime,表示整个投票的过期时间。
现在显示的是:
{
"votes": [
{
"name": "VoteItemA",
"score": 9
},
{
"name": "VoteItemB",
"score": 11
},
{
"name": "VoteItemC",
"score": 27
},
{
"name": "VoteItemD",
"score": 5
},
{
"name": "VoteItemE",
"score": 19
}
],
"expireTime": 1561824000000,
"totalVotes": 71
}
自动登录
自动登录使用了Vuex,由于要自动登录,很显然要在应用启动的时候从localStorage
中载入相关信息,在login
的时候写入相关信息。
这两个动作都放到Vuex的actions中去,这样无论在任何时候都可以通过这两个函数来设置vuex的内容。
store.js的主要内容:
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
token: null,
username: null,
lastVote: null,
expire: null,
vote:null
},
//更新token,username和上次投票的mutations
mutations: {
setToken: function (state, token) {
state.token = token;
},
setUsername: function (state, username) {
state.username = username;
},
setLastVote: function (state, lastVote) {
state.lastVote = lastVote;
},
setExpire: function (state, expire) {
state.expire = expire;
},
setVote: function (state, vote) {
state.vote = vote;
}
},
//异步更新,用于登录请求
actions: {
tryAutoLogin: function (context) {
const token = localStorage.getItem('token')
const username = localStorage.getItem('username')
const lastVote = localStorage.getItem('lastVote')
const expire = localStorage.getItem('expire')
const vote = localStorage.getItem('vote')
const now = (new Date()).getTime()
if(now<=expire){
context.commit('setToken', token)
context.commit('setUsername', username)
context.commit('setLastVote', lastVote)
context.commit('setExpire', expire)
context.commit('setVote',vote)
} else {
context.commit('setExpire', -1);
}
},
login:function({commit}, authData){
return new Promise((resolve,reject)=>{
axios.post('http://localhost:8080/api/token', {
username: authData.username,
password: authData.password
}).then(res => {
commit('setToken', res.headers.authorization)
commit('setUsername', authData.username)
commit('setLastVote', res.data.lastVotedAt)
commit('setExpire', res.data.expire)
localStorage.setItem('token', res.headers.authorization)
localStorage.setItem('username', authData.username)
localStorage.setItem('expire', res.data.expire)
localStorage.setItem('lastVote', res.data.lastVotedAt);
resolve()
}).catch(err => {
err.toString()
reject()
})
})
}
}
})
这里的数据设置有些冗余。tryAutoLogin
用于读入信息,然后马上就要被首页使用。login
的方法则比较特殊,返回一个Promise
对象,这样视图在调用这个方法的时候,能够根据结果来访问路由。
Login视图
这里其实写的不是太好,就直接渲染了一个表单作为login视图的唯一组件。应该是将向后端提交数据的方法写在视图里,然后视图将传回的数据通知给组件。
不过由于比较简单,也就直接实现了,而是是通过Vuex中的login
返回Promise来实现的。
登录的组件如下:
<template>
<v-container fill-height>
<v-layout justify-center align-center>
<v-flex xs10 sm9 md7 lg6 xl4 class="mb-5">
<h2 class="display-3 text-xs-center">Please Login</h2>
<v-layout wrap row justify-center>
<v-flex text-xs-center xs8>
<v-alert type="error" :value="loginFailed" outline>用户名或密码错误</v-alert>
</v-flex>
<v-flex xs12>
<v-text-field
v-model="username"
label="Username"
type="text"
required
@blur="$v.username.$touch()"
></v-text-field>
</v-flex>
<v-flex xs12>
<v-text-field
v-model="password"
label="Password"
type="password"
@blur="$v.password.$touch()"
required
></v-text-field>
</v-flex>
<v-flex xs12 class="text-xs-center">
<v-btn :disabled="$v.$invalid" @click="handleLogin">Login</v-btn>
</v-flex>
</v-layout>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import {required} from 'vuelidate/lib/validators'
export default {
name: "LoginForm",
data: function () {
return {
username: "",
password: ""
}
},
computed: {
loginFailed: function () {
return window.location.href.endsWith('error')
}
},
validations: {
username: {
required
},
password: {
required
}
},
methods: {
handleLogin: function () {
var _this=this
this.$store.dispatch('login',{
username:this.username,
password:this.password
}).then(()=>_this.$router.push('/')).catch(()=>_this.$router.push('/login?error'))
}
}
}
</script>
<style scoped>
</style>
这个组件比较简单,就是使用了一个表单,然后将用户名和密码发送过去。如果不成功,就返回一个错误页面,这里实际上应该用个snack-bar
来做一下提示,比较懒也没做。
投票页面
投票页面编写的时候,终于找到了Vue的套路,想想看不然为什么Vue CLI生成的项目,要先分出来视图,再分出来组件。
访问视图的时候,涉及到去请求数据的内容,都写在视图中,然后视图将获得数据传递给组件,组件进行渲染和操作。如果请求出错,视图可以直接控制跳转,无需再渲染视图。
组件获得数据后进行渲染和自己的处理,在需要提交投票的时候,抛出一个自定义事件给父组件=视图,然后视图去进行更新页面的操作。
这里把发送投票写在了组件里,发现更新页面就非常麻烦,后来才发现其实根本无需组件自己做这个,应该发送一个事件给视图,视图去更新,再把数据取回来。
依据这个思路,其实子组件发送请求的事件也不一定写在组件里,完全可以传给视图。不过不管怎么样,全局更新视图这个肯定要写在视图中比较好。
这是Home.vue的视图:
<template>
<vote :vote="vote" :expire="vote.expireTime" @new-voted="handleVote"></vote>
</template>
<script>
import vote from '../components/Vote.vue'
import axios from 'axios'
export default {
components: {
vote
},
data: function () {
return {
vote: {},
}
},
created:function () {
let token = this.$store.state.token
axios.get('http://localhost:8080/api/vote', {
headers: {
Authorization: token
}
}).then(res => {
this.$store.commit('setVote', res.data)
this.vote = res.data
}).catch(() => {
this.$router.push('/login')
});
},
methods:{
handleVote() {
let token = this.$store.state.token
axios.get('http://localhost:8080/api/vote', {
headers: {
Authorization: token
}
}).then(res => {
/* eslint-disable */
// console.log(res)
this.$store.commit('setVote')
this.vote = res.data
}).catch(() => {
this.$router.push('/login')
});
}
}
}
</script>
创建和每次事件发生的时候都去调一次,其实重复了。
vote组件的内容:
<template>
<v-container>
<v-layout v-if="voted" justify-center>
<v-flex xs12 class="text-xs-center">
<h2 class="display-3">投票结果</h2>
</v-flex>
</v-layout>
<v-layout>
<v-flex xs12 class="text-xs-center">
<h3 class="display-2" v-if="this.expire-(new Date()).getTime()>=0">还有{{day}}天{{hr}}小时{{min}}分钟{{sec}}秒投票截止</h3>
</v-flex>
</v-layout>
<v-layout v-if="!voted">
<v-flex xs12 class="text-xs-center">
<h2 class="display-3">请先投票然后查看投票结果</h2>
</v-flex>
</v-layout>
<v-layout v-for="(voteItem, index) in vote.votes" :key="index" justify-center align-center wrap>
<v-flex xs12 sm3 md2 class="pa-0 ma-0">
<v-layout justify-end>
<v-flex offset-xs-1>
<label v-if="voted">{{voteItem.name}} {{voteItem.score}}票
<input type="checkbox" v-if="voted" v-model="votenames" :value="voteItem.name" >
</label>
<label v-if="!voted">{{voteItem.name}}
<input type="checkbox" v-if="!voted" v-model="votenames" :value="voteItem.name" >
</label>
</v-flex>
</v-layout>
</v-flex>
<v-flex xs11 sm6 md7>
<v-progress-linear
v-if="voted"
color="red"
height="10"
:value="voteItem.score"
background-color="rgba(250,250,250,0)"
></v-progress-linear>
</v-flex>
</v-layout>
<v-layout justify-center>
<v-btn color="primary" @click="comeToVote">投票</v-btn>
</v-layout>
<v-snackbar
v-model="errorOccured"
:timeout="3000"
top
auto-height
>您已经投过票</v-snackbar>
</v-container>
</template>
<script>
import axios from 'axios'
function VoteItem(name,score=0) {
this.name = name
this.score = score;
}
// 代码重写,抛自定义事件,还是接受prop
export default {
name: "Vote",
props :{
vote:{
type: Object
},
expire: {
type: Number,
default: 0
}
},
data: function () {
return {
votenames:[],
errormessage: "",
errorOccured: false,
day: 0, hr: 0, min: 0, sec: 0
}
},
computed:{
voted:function () {
return this.vote.totalVotes !== -1;
},
voteSentToBackend: function () {
var temp = [];
this.votenames.map(votename => temp.push(
new VoteItem(votename, 0)
));
var o = {};
o.votes = temp;
return o;
},
},
methods: {
comeToVote: function () {
// eslint-disable-next-line
console.log("当前的votes对象是" + this.vote)
// eslint-disable-next-line
console.log('发送的投票对象是' + JSON.stringify(this.voteSentToBackend))
axios.post('http://localhost:8080/api/vote', this.voteSentToBackend, {
headers: {
Authorization: this.$store.state.token
}
}).then(res => {
// eslint-disable-next-line
console.log(res)
this.$emit('new-voted', res.data);
}).catch((err) => {
// eslint-disable-next-line
if (err.response.status === 406) {
this.errorOccured = true;
// eslint-disable-next-line
console.log(this.errorOccured)
} else {
this.$router.push('/login')
}
});
},
countdown: function() {
let mesc = this.expire-(new Date()).getTime()
let day = parseInt(mesc / 1000 / 60 / 60 / 24);
let hr = parseInt(mesc / 1000 / 60 / 60 % 24)
let min = parseInt(mesc / 1000 / 60 % 60)
let sec = parseInt(mesc / 1000 % 60)
this.day = day
this.hr = hr > 9 ? hr : '0' + hr
this.min = min > 9 ? min : '0' + min
this.sec = sec > 9 ? sec : '0' + sec
const that = this
setTimeout(function () {
that.countdown()
}, 1000)
}
},
mounted() {
this.countdown();
}
}
</script>
<style scoped>
.v-input__control {
width: 100%!important;
}
</style>
这样就基本写完了最简单的投票核心逻辑。
果然书看得再多,不实际写也是不行的。写了这一个东西,终于把Vue的开发套路也摸清楚了。