前言
上回讲述了我搭建好了部分开发环境,从这一章开始,就要动手写代码了。
一个像样的网站要有注册登录功能。所以今天讲一讲,我是如何设计开发毕设的登录注册的,效果如下方所示
那么,整理一下本次的开发目标。
开发目标
- 打开毕设网站就显示登录页面(vue-router)
- 注册和登陆的页面设计(采用element-ui)
- 登录页面和注册页面之间的路由跳转(vue-router)
- 注册页面的动态设计和与服务器的通讯(vue的数据绑定,axios调用api,epress作为服务器,mongodb作为数据库)
1.打开毕设网站就显示登录页面
其实登录页面的显示方式有很多种,我目前能想到以下三种方式:
- 访问根目录,先进入Home主页,然后点击登陆按钮弹出一个dialog的表单,如:腾讯视频
- 访问根目录,先进入Home页面,然后点击登陆按钮跳转到另一个登录页面,如:bilibili
- 访问根目录,直接进入登录页面,如:QQ邮箱
首先,该模板中,express已配置好了以下代码,坐标src/server/index.js
//项目中的静态文件放在根目录的views文件夹下
app.use(express.static(path.join(__dirname, 'views')))
//当有人访问根目录的时候,把views文件夹中的index.html给他看
app.get('/', function (req, res) { res.sendFile('./views/index.html')})
复制代码
接下来,再去看看这个index.html,坐标src/server/views/index.html
"utf-8">
不要关掉我
"app">
复制代码
这里甚至没有看到任何script标签,只有一个body,但有这句话:built files will be auto injected!打包文件将会自动插入!
打包啥?打包src/client
文件夹下的所有文件:webpack会从入口文件开始,也就是从src/client/index.js
开始,顺藤摸瓜,把所有涉及到的文件,打包成一个JavaScript,插入到src/server/views/index.html
中。
继续前进,去看看这个打包的入口文件,坐标src/client/index.js
import ElementUi from 'element-ui'; //引入element-ui组件库
import 'element-ui/lib/theme-chalk/index.css' //引入element-ui样式文件
import Vue from 'vue' //引入Vue
import App from './App' //引入App.vue组件,作为视图入口
import router from './router/index' //引入配置的路由文件
import store from './store/store' //引入状态管理器
import axios from 'axios' //引入axios可请求HTTP服务
Vue.use(ElementUi) //全局注册element-ui组件库,在所有组件都能用
Vue.config.debug = true
Vue.config.productionTip = false
Vue.prototype.$axios = axios //将axios设置为全局属性,所有组件可this.$axios
//定义一个vue实例
new Vue({
el: '#app',
router: router,
store: store,
template: ' ',
components: { App }
})
复制代码
在vue实例中,el
对应的值'#app'
,就是本实例要控制的元素
即src/server/views/index.html
中的
template就是本组件要渲染的模板,components
属性注册这个实例中所需要的组件,{App}
其实就是{App:App}
的简写,{App:App}
这个对象中,属性就是组件名,值就是组件对象,这个组件对象在上方我们已经通过import App from './App'
导入。所以如果愿意,也可以写成{'my-app':App}
,那么template
中也要对应改成
不过只有在实例中注册了components
属性,template
模板中的
才可以生效。
也就是说,这个打包的入口文件,除了引入必要的文件,只显示App.vue组件
接下来就去App.vue看看。坐标src/client/App.vue
复制代码
如果忽略样式,忽略script中的各种空对象,其实该文件就可以看成是
复制代码
即路由视图!也就是说,路由到哪儿,App就显示啥!
现在想要的效果是刚刚的方式3:访问根目录,直接进入登录页面。
也就是说只要用户访问了'/'根目录,就显示包含登录页面的视图。比如说,只显示一个登录组件Login.vue,那么只需要设置,'/'对应的路由视图为Login.vue即可。
来,咱去vue-router路由看看,坐标src/client/router/index.js
import Vue from 'vue' //导入vue
import Router from 'vue-router' //导入vue-router
// 导入相应的子组件
import Login from './../components/Login'
Vue.use(Router) //全局注册路由,所有组件可访问this.$router
var router = new Router({
mode: 'history',
routes: [
{
name: 'login',
path: '/',
component: Login
}
]
})
export default router复制代码
大雄可以自由选择进哪个门,想去哪儿就走进哪个门。如果这个时候,大雄想去中国,那走这三个门是没有用的,必须要哆啦A梦再拿出一个可以去中国的任意门。只有哆啦A梦拿出了这个门,大雄才有机会去中国。
上述例子中,哆啦A梦就是写代码的我,任意门就是路由,大雄就是用户,大雄走进哪个门就是使用了哪个路由。
显然,现在要做的,是给大雄拿出一个可以进入登录页面的任意门。也就是在vue-router中注册一个可以访问登录组件的路由。
src/client/router/index.js
中的
Router
实例中的
routers
数组中即可。
{
name:'login',
path:'/',
component: Login
}复制代码
name
表示给该路由起个名字,path
表示请求路径,比如在本地访问的话,就是localhost:3000/home
,component
表示该路由对应显示的组件,这里对应的是我们src/client/components/Login.vue
组件,所以我们需要将Login.vue组件引入src/client/router/index.js
路由文件中。
至此,当我们访问项目根目录,便会直接显示Login.vue组件!
2.注册和登录的页面设计
然而,在src/client/components/
下并没有Login.vue文件。所以需要该目录下新建一个Login.vue,然后设计制作它。
其实页面设计并不难,尤其是使用了element-ui组件库,样式都是别人封装好的。
难的是动态显示,比如在注册页面,当学号格式符合要求的时候,文本框后面动态地显示一个提示正确的icon,同时,下方的文本框解除禁用状态。我将会在这一个部分的后半段讲解我是如何实现动态显示的。
那么首先,先搭出登录页面的架子,坐标src/client/components/Login.vue
'50px'>
'学号' >
'密码'>
type="password">
type='primary'>登录
注册
复制代码
效果如下图
那么接下来,希望点击注册按钮之后,跳转到显示注册组件的页面中去!可还没有注册组件呢,总要先做出来吧?
那么在src/client/components/
下,新建一个Register.vue组件,
接下来,搭出注册页面的架子,坐标src/client/components/Register.vue
'70px' id='form'>
"20">
'学号'>
"12">
"20">
'密码'>
"12">
"20">
'确认密码'>
"12">
"20">
"12">
'姓名'>
"20">
'邮箱'>
"15">
"我同意" border>
type='primary'>确认注册
type='danger'>取消
复制代码
效果如下图
至此,已经成功通过element-ui搭建好登录和注册的两个页面架子!
3.登录页面和注册页面之间的路由跳转
现在两个页面架子已经有了,那么接着上面的问题,希望从登录界面,点击注册按钮之后,跳转注册页面去,这么做呢?
可以这么理解,当访问'/register'
路径时,App.vue就显示Register.vue组件,于是就是要注册一个path
为'/register'
,component
为Register
的路由,
拿出这个json任意门:
{
name:'register',
path:'/register',
component: Register
}
复制代码
如何让“大雄”走进这个门呢?
当我们点击注册的时候,绑定一个点击事件,触发一个函数,在这个函数内,跳转路由。
坐标src/client/components/Login.vue
...
'register'>注册
...
复制代码
this.$router.push('/register')
中,this.$router
就是访问了全局路由,push
方法可以实现路由跳转,所带的一个参数,可以使注册路由中的name
,也可以是path
等等,具体可以去看官方文档,非常详细。
至此,点击注册后,页面将会由根目录下的登录页面,跳转至注册页面。
4.注册页面的动态设计和与服务端的通讯
本小节涵盖内容较多,首先是注册页面的动态效果。
单以学号和密码这两个输入框为例,其他输入框的动态效果以此类推。
- 初始状态,仅学号(即注册账号)的输入框为可输入状态,密码输入框为禁用状态。
- 一旦输入学号满8位,或者焦点离开了学号输入框,就做以下两个条件判断:
- 输入的学号是否符合'JAPXXXXX'的格式。
- 输入的学号是否被注册过。
- 若输入学号不符合条件,则在学号输入框后方显示错误信息。
- 若输入学号符合条件,则在学号输入框后方显示正确图标。解除密码输入框的禁用状态。
整理好以上逻辑,下面一步一步去用vue实现它们。
1.初始状态,仅学号的输入框为可输入状态,密码输入框为禁用状态。
这一节的坐标都在这儿:src/client/components/Register.vue
...
"20">
'学号'>
"12">
'registerForm.id'>
"20">
'密码'>
"12">
'true' v-model='registerForm.password'>
...
复制代码
在密码输入框中的:disabled='true'
即指定该文本框为禁用状态,用boolean
值控制禁用状态的开启和解除。因此可以在后面通过控制boolean
值来控制禁用状态。
同时,将两个输入框双向绑定到data
的数据中了,即v-model
,当文本框中输入数据时,data
中的数据会实时更新。
2.一旦输入学号满8位,或者焦点离开了学号输入框,做条件判断。
这里有两个判断:
- 输入的学号是否符合'
JAPXXXXX
'的格式。 - 输入的学号是否被注册过。
此时就出现几个问题,
- 如何判断焦点(也就是光标)是否在学号输入框内。
- 如何判断学号是否被注册过。
首先解决问题1,我的方法是在学号输入框绑定焦点移入移出事件,并在data
中定义foucsIn
来记录焦点所在位置。如下:
"registerForm.id"
@blur="foucsIn=''"
@focus="foucsIn='id'">
复制代码
当焦点移入学号输入框时,foucsIn
的值变为id
。让移出时,foucsIn
为空。
这样,我们就可以通过访问foucsIn
判断焦点在不在学号输入框内了,问题1可以暂时告一段落。
然后是问题二,如何判断学号是否被注册过。
整理一下思路,当学号符合格式,就可以把学号发送至express
,express
拿到我们的学号去mongoDB
里面查询是否已有记录。
若无记录,则响应前端可以注册,若有记录,则响应前端该账号已被注册。
道理是这么个道理,如何实现呢?只要在express
中,封装一个接口,验证学号是否被注册过,再在前端中使用这个接口,接收响应值。
来吧,去造造这个接口,坐标src/server/index.js
//导入User的模型
import User from './../models/User' ...
app.post('/register-id',async (req,res)=>{
try {
//查询数据库
let user = await User.findOne({uid:req.body.id}).exec();
if(user){
return res.json({err:'该学号已被注册'});
}
return res.json({msg:'ok'});
} catch (error) {
throw error
}
})
...复制代码
我以我的理解来解释一下以上代码:
当服务器实例app
,监听到有人访问'/register-id'
接口时,触发一个回调函数。
这个回调函数通过async/await
异步处理,需要一个try catch
来捕获错误。
回调函数中有两个参数,
第一个参数req
是客户端请求'/register-id'
接口时,生成的对象。
第二个参数res
是服务端在在响应客户端时需要返回的对象。
而客户端发送来的数据,便会存在req
的body
中,为了获取req
中的body
,需要有中间件body-parser
解析所有请求,不过本项目模板以为我们配置好:
app.use(bodyParser.json())app.use(bodyParser.urlencoded({ extended: false }))复制代码
继续,我们在try的作用域中,我们先通过客户端传来的学号req.body.id
查询一下数据库,若数据库中无记录,则user
为undifine
,若数据库中已有该学号,则user
为一个用户数据对象。
因此,判断user
,若user
不为undifine
,即已存在该用户,则让res
返回一个带err
的json
文件通知客户端,该学号已被注册。
否则,随便响应结束本次访问,不带err
即可。这样客户端就知道,该学号没被注册过了。
那么,导入的User
模型是什么?
我再以我的理解解释一下它,它就类似一个容器,就像一个空表格,已经给你定好了表头,但是内容都是空的,而这个容器模型是通过mongoose
模块定义的。
我们去看看这个模型,坐标src/server/models/User.js
import mongoose from 'mongoose';
const userSchema = new mongoose.Schema({
id: {type: String, unique: true, required: true, trim: true },
realname: { type: String, required: true, trim: true },
password: { type: String, required: true },
post_box: { type: String },
create_time: { type: Date, default: Date.now() },
friends: { type: Array },
friend_req: { type: Array },
admin: { type: Boolean, default: false },
token: { type: String }
});
export default mongoose.model('User', userSchema);
复制代码
一个用户模型中,定义了可以储存学号,真实姓名,密码(通过sha1
加密),邮箱,创建时间,好友,要有请求,是否为管理员,和token
的空间。
并且规定了他们的变量类型和要求。
而模型(column
)会对应的去找对应的去找mongoDB
中的集合,然后操作集合(table
)中的文档(row
)。
等到注册的时候,我们就要通过客户端发来的表单,结合这个模型,往mongoDB
中加一个文档,就注册完成啦。
mongoose
的科普到次,现在接口已经做好了,现在来看看如何使用:
我在computed
中定义一个计算属性ifIdCorrect
监听文本框中的数据变化:
computed:{
if(this.registerForm.id.length>=8||this.foucsIn!='id'){//当输入满8位,或焦点离开id
if(!/^JAP\d{5}$/.test(this.registerForm.id)){//判断学号是否符合'JAPXXXXX'
return false;
}
this.$axios.post('/api/register-id',{id:this.registerForm.id})//调用接口判断是否被注册过
.then(res=>{
if(res.data.err){
return false;
}
})
return true;
}}复制代码
可通过this.$axios
访问全局属性axios
,通过post
的请求方式访问服务端接口。并把参数id
,也就是学号,以json形式传给服务端。
请求后,axios
会返回一个promis对象,在.then()
中,获得服务端返回的res
对象,在res
的data
中,就是服务端返回的json对象。
根据刚刚造的API接口,如果这个对象中有err
,那么就被注册过了。
至此,可以通过计算属性idIdCorrect
的值来判断学号是否符合条件。
3.若输入学号不符合条件,则在学号输入框后方显示错误信息。
这一环节需要考虑以下几点:
- 什么时候显示错误信息。(when)
- 用什么来作为错误信息的载体。(how)
- 错误信息的内容是什么?(what)
先整理这三个问题,再着手写代码。
首先,当判断学号是否符合要求后,若学号格式不对,或者已经被注册过,则显示错误信息。否则不显示。
其次,在data
中定义一个errMsg
的对象,专门储存对应输入框的错误信息。此处应该在对象中加入id
属性,内容为空。
最后,在学号输入框后方加入一个红色的标签,标签的内容为errMsg
中的id
的值。
将以上思路整理成代码:
在data
中定义errMsg
data(){
return{
registerForm:{
id:'',
....
},
errMsg:{
id:''
},
...
}
}复制代码
在computed中控制errMsg.id的内容
ifIdCorrect(){
if(this.registerForm.id.length<8&&this.foucsIn==='id'){//当焦点在id,且输入值未满8位的时候
this.errMsg.id=''//清空错误数据
return false
}
if(this.registerForm.id.length>=8||this.foucsIn!='id'){//当需要执行判断的时候
if(!/^JAP\d{5}$/.test(this.registerForm.id)){//若学号不符合格式
this.errMsg.id='学号格式错误'; //让错误信息为,学号格式错误
return false
}
this.$axios.post('/api/register-id',{id:this.registerForm.id})
.then(res=>{
if(res.data.err){//若请求接口后返回了错误信息
this.errMsg.id = res.data.err;//让错误信息为,返回的错误信息(已被注册)
return false;
}
this.errMsg.id=''//否则,学号符合要求,清空错误数据。
})
}
return this.errMsg.id==''?true:false//运行至此,若错误信息为空,则ifIdCorrect为true
}
复制代码
在template中,往学号输入框后方加入显示错误信息的红色标签。
'学号'>
"12">
"registerForm.id"
@blur="foucsIn=''"
@focus="foucsIn='id'">
"12">
"errMsg.id!=''"
type="danger"
v-text="errMsg.id">
复制代码
如果errMsg.id
不为空,则显示错误信息,否则不显示。
至此,错误信息提示解决完毕。
4.若输入学号符合条件,则在学号输入框后方显示正确图标。解除密码输入框的禁用状态。
此处需要考虑:
- 显示正确图标(解除密码输入框禁用状态)的条件是什么。(when)
- 如何显示正确图标。(how)
还是像刚刚一样,先整理思路:
首先,计算属性ifIdCorrect
的值为true
的话,说明学号符合条件。
其次,用element-ui
自带的icon
可以作为提示。
最后,密码框禁用状态有boolean
来控制,也就是说可以直接用ifIdCorrect
来控制。
将上述思路整理成代码:
"20">
'学号'>
"12">
"registerForm.id"
@blur="foucsIn=''"
@focus="foucsIn='id'">
"12">
"errMsg.id!=''"
type="danger"
v-text="errMsg.id">
"ifIdCorrect==true"
class="el-icon-success color-green">
'密码'>
'registerForm.password'
:disabled="!(ifIdCorrect===true"
type='password'
@blur="foucsIn=''"
@focus="foucsIn='password';errMsg.password=''">
复制代码
后续输入框的动态设计如法炮制,便显示为本章开头gif效果(因为代码做了局部改善,所以和原来的有点不一样,请见谅!)
至此,本小节内容搞定。すごいですね!