Vuetify看了几天,发现先把组件过一遍,心里有数,就直接上吧。
前端设计
简单的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的开发套路也摸清楚了。