Vue + nodejs 登录注册页面
放暑假了想着做一些还算成体系的东西提升一下自己的能力,恰巧有个有意思的想法。小时候家里有个台式电脑,每次待机的时候windows的logo就在屏幕里面碰来碰去,就想着这个能不能拿前端来实现,当成登录页面的背景,还可以和用户交互。

图片移动背景保护
vue遵循组件式开发,我就单独将屏幕保护背景作为一个组件拎出来。代码也能看的方便一些,第一想要实现这个效果,最重大的应该是先得让图片动起来,设置变量x、y、xSpeed、ySpeed,通过帧渲染器控制在每次渲染时将xSpeed、ySpeed和x、y的和赋值给x、y,这样就实现了图片的移动逻辑。移动的逻辑解决了,接下来就是实现图片碰撞到屏幕边缘反弹的逻辑,要想实现这个效果得知道以下几个信息。
-
屏幕有多大(宽和高)
-
图片到达屏幕边缘之后应该怎么办?(逻辑)
第一解决第一个问题,js原生提供了可以获取到浏览器实际渲染的屏幕宽度和高度的方法,Window.innerWidth和Window.innerHeight可以将这两个值定义在 data 内方便随时调用。
第二个问题,要实现的效果是碰到屏幕边缘后来,反弹回屏幕内部,这里有个细节,要想图片到边缘后反弹,第一得知道图片的位置是否到了边界,然后才能执行反弹操作。而这两个问题可以在一个函数内同时解决,一开始定义了x、y来记录图片在屏幕中的位移距离,起始位置为0,0,也就相当于知道x,y就知道图片在屏幕的具体位置,由此我们可以在帧渲染器中写一个判断,如果x的大小大于了屏幕的screenWidth那么就对xSpeed取反,同理y的大小大于了屏幕的screenHeight那么就对ySpeed取反。这样子就了解了图片碰撞后反弹的逻辑。
接下来是细节方面的处理
图片的移动本质上是style样式的改变,我们将x,y赋值给图片style的transform的x,y,就相当于是每一帧改变了图片的样式,这里有个很有意思的用法。this.imageStyle是一个对象,包含了一些CSS样式的属性和值。使用扩展运算符...可以将这个对象展开,将其中的属性和值合并到当前的属性列表中。这样做的目的是保留之前的样式,并且添加或覆盖一些新的样式。
例如,在imageStyle对象中有一些属性(图片的大小,位置等),然后在现有样式的基础上添加额外动态绑定的样式,可以这样写:
methods: {
updataImg(){
// 根据设置的初始速度值xSpeed、ySpeed,更新‘x’、‘y’的值
this.x += this.xSpeed;
this.y += this.ySpeed;
/* 中间图片属性的基本设置未给出*/
this.imageStyle = {
...this.imageStyle,
transform: `translate(${this.x}px, ${this.y}px)`,
};
}
}
将这些代码写在帧渲染器内就可以丝滑的实现碰撞后反弹的效果。
在客户端运行代码时需要思考到屏幕大小有可能被调整的可能性,这种调整不太可能是时刻发生的,可以设置一个事件监听器来使每次浏览器渲染屏幕的尺寸发生变化的时候,能让图片在合理的位置碰撞反弹。
// 钩子函数: 模版已挂载 且 组件事例已经被创建和挂载到Vue实例上
mounted() {
window.addEventListener( resize , this.handleResize);// 添加事件监听器 名称 ‘resize’ 作用:调用handleResize
},
method:{
handleResize() {// 该函数被监听,变化时会更新浏览器的实际渲染宽高
this.screenWidth = window.innerWidth;//
this.screenHeight = window.innerHeight;
},
},
下面是该组件的全部代码:
<template>
<div>
<img ref="screensaverImage" src="../assets/xc.png" :style="imageStyle" @load="startScreensaver" @click="redirectToLogin"/>
<router-view v-on:closeImg="handleLogin"></router-view>
</div>
</template>
<script>
export default {
data() {
return {
lock:true,
showLogin:false,
imageStyle: {
height: 320px ,
width: 280px ,
position: absolute ,
top: 0 ,
left: 0 ,
},
x: 0,// 图片在屏幕中横坐标的位置
y: 0,
xSpeed: 5, // 在x轴的初始速度
ySpeed: 5, // 在y轴的初始速度
screenWidth: window.innerWidth, // 获取到浏览器的实际渲染宽度赋给 screenWidth
screenHeight: window.innerHeight, // 获取到浏览器的实际渲染高度赋给 screenHeight
screensaverInterval: null,
};
},
// 钩子函数: 模版已挂载 且 组件事例已经被创建和挂载到Vue实例上
mounted() {
window.addEventListener( resize , this.handleResize);// 添加事件监听器 名称 ‘resize’ 作用:调用handleResize
},
// 钩子函数: 当页面中的 Vue 实例被销毁之前,会先执行 beforeDestroy 生命周期函数。
beforeDestroy() {// 使用beforeDestroy()生命周期钩子来移除事件监听器,以避免`内存泄漏。`
window.removeEventListener( resize , this.handleResize);// 移除时间监听器 名称 ‘resize’ 作用:调用handleResize
this.stopScreensaver();// 同时调用 ‘stopScreensaver’ 停止动画
},
methods: {
handleResize() {// 该函数时刻被监听,相当于时刻监听更新浏览器的实际渲染宽高
this.screenWidth = window.innerWidth;//
this.screenHeight = window.innerHeight;
},
updateScreensaver() {
// 根据设置的初始速度值xSpeed、ySpeed,更新‘x’、‘y’的值
this.x += this.xSpeed;
this.y += this.ySpeed;
const imageWidth = this.$refs.screensaverImage.offsetWidth;// 通过ref属性 `$refs` 访问对应元素,
const imageHeight = this.$refs.screensaverImage.offsetHeight;//并指定引用ID为`screensaverImage`,通过offsetWidth属性获取img的宽
// 检测碰撞边界 x 表明图片左上角的点此时在屏幕中的距离屏幕左上角的距离,y同理 ; (screenWidth - imagewidth)就相当于将img看作一点
if (this.x >= this.screenWidth - imageWidth || this.x <= 0) {// 当 x 大于屏幕像素最大值 或 x 小于屏幕像素时 将 xSpeed 的速度取反(实现碰撞效果)
this.xSpeed *= -1;// 乘 -1 相当于给速度取反向
}
if (this.y >= this.screenHeight - imageHeight || this.y <= 0) {// 同 x
this.ySpeed *= -1;
}
this.imageStyle = {
...this.imageStyle,// 展开imageStyle这个对象,将其中的属性和值合并到当前的属性列表中。这样做的目的是保留之前的样式,并且添加或覆盖一些新的样式。
transform: `translate(${this.x}px, ${this.y}px)`,// 该位置为新添加的值(动态实现了移动效果)
};
// 通过`screensaverInterval`存储动画帧标识符,请求动画帧调用需要的`updateScreensaver`函数
this.screensaverInterval = requestAnimationFrame(this.updateScreensaver);
},
startScreensaver() {
this.$nextTick(() => {
const imageWidth = this.$refs.screensaverImage.offsetWidth;
const imageHeight = this.$refs.screensaverImage.offsetHeight;
this.x = Math.random() * (this.screenWidth - imageWidth);
this.y = Math.random() * (this.screenHeight - imageHeight);
this.updateScreensaver();
});
},
stopScreensaver() {
cancelAnimationFrame(this.screensaverInterval);
},
redirectToLogin() {
if(this.lock){
this.$router.push( /loginTmp );
this.lock = !this.lock
}
},
handleLogin(){
console.log("img元素隐藏");
this.$refs.screensaverImage.remove();
}
},
};
</script>
<style>
img{
z-index: 2;
}
</style>
PS.该组件实际上只有一个img元素,而且是当作背景来使用,所以把图片的index值设置的偏大一些
注册组件
在实现注册操作时,应该注意的有以下几点:
-
用户点击注册时或者发送邮件时,是否已经填写邮箱及其他信息
-
填写的邮箱是否已经被注册过
-
填写的邮箱验证码和数据库内的验证码是否匹配
-
发送验证码时,应该先将验证码和其相对应的邮箱存储到临时数据表内,便于在用户进行二次验证时进行比对
-
点击注册时,同时比对邮箱和验证码是否和数据库内的一致,若一致则将临时表内的数据删除
在后端写注册代码时,使用express框架。引入包<u>express</u>、<u>mysql</u>、<u>body-parser</u>。作用分别为mysql连接mysql数据库、bodyParser解析前端上传的json数据。
写发送邮件代码时,使用express框架。引入包<u>express</u>、<u>mysql</u>、<u>body-parser</u>、<u>nodemailer</u>、<u>randomstring</u>。作用分别为mysql连接mysql数据库,bodyParser解析前端上传的json数据,nodemailer创建邮件传输器,randomstring用来生成随机的6位验证码
需要注意的是:
邮件传输器中的pass值并不是密码,而是在邮箱客户端或者网页供应商提供给用户的密钥。并且在请求连接服务器时,必定要注意端口是否匹配,路由是否匹配。请求头设置为res.setHeader( Access-Control-Allow-Headers , Content-Type );用来匹配前端的json格式
在前端注册的逻辑中,使用的是Axios的发送方式,最需要注意的就是当发送的文件或者信息确认时,注意发送格式的书写。这里我使用了json格式发送信息,请求头设置为 application/json 。同理,发送邮件的逻辑也是一样的。
下面是关于注册的前端Vue逻辑:
<script>
import axios from axios ;
export default {
data() {
return {
email1: ,
password1: ,
verificationCode: ,
isSending: false
};
},
methods: {
sendVerificationCode(){
if(!this.email1){
this.errorMessage = 请先填写邮箱
return;
}
this.isSending = !this.isSending
// 调用后端接口发送邮箱验证码
axios.post( http://localhost:8081/sendVerification , {
email: this.email1 // 上传email的的值,用来将邮箱和验证码添加进数据库
}, {
headers: {
Content-Type : application/json
}
})
.then(response => {
const data = response.data;
console.log(data);
if (data.success) {
alert(data.message);
// 发送验证码成功后的逻辑
} else {
this.errorMessage = data.message;
}
})
.catch(error => {
console.error( Error: , error);
this.errorMessage = 发送验证码失败,请稍后再试 ;
});
},
register(){
axios.post( http://localhost:8082/register , {
email: this.email1,
password:this.password1,
verificationCode:this.verificationCode
}, {
headers: {
Content-Type : application/json
}
})
.then(response => {
const data = response.data;
if (data.success) {
alert(data.message);
} else {
this.errorMessage = data.message;
}
})
.catch(error => {
console.error( Error: , error);
this.errorMessage = 注册失败 ;
});
},
},
};
</script>
发送邮件的nodejs文件:
// action:根据用户填写的邮箱发送邮箱验证码
// 1.点击发送验证码,在对应数据表中存储用户邮箱和邮箱对应的验证码
// 2.点击注册时,获取输入的邮箱和验证码与verification_code数据表中的进邮箱和验证码行比对验证,
// 验证通过后上传邮箱和密码至user_email数据表进行存储,
// author:wy
const express = require( express );
const mysql = require( mysql );
const nodemailer = require( nodemailer );
const randomstring = require( randomstring );
const bodyParser = require( body-parser );
const app = express();
const interface = 8081;
// 设置请求头信息
app.use((req, res, next) => {
res.setHeader( Access-Control-Allow-Origin , * );
res.setHeader( Access-Control-Allow-Methods , GET, POST, PUT, DELETE );
res.setHeader( Access-Control-Allow-Headers , Content-Type );
next();
});
// 解析数据
app.use(bodyParser.json());
// 生成验证码
const code = randomstring.generate(6); // 生成一个6位的随机验证码
// 创建邮件传输器
const transporter = nodemailer.createTransport({
service: 对应邮箱的名字,如"qq邮箱":"QQ" ,
auth: {
user: 邮箱账号 ,
pass: 运营商提供的密钥
}
});
// 处理发送邮箱验证码请求
app.post( /sendVerification , (req, result) => {
const { email } = req.body;
// 创建数据库连接
const db = mysql.createConnection({
host: localhost ,
user: 数据库用户名 ,
port: 3306 ,
password: 数据库密码 ,
database: 数据库名
});
db.connect();
// 判断邮箱和验证码是否有问题
if(!email){
result.end("邮箱上传失败");
return
}else{
const reEmailsql = SELECT * FROM user_email WHERE email = ? ;
db.query(reEmailsql,[email],(err,res) => {
if(err){
console.error(err,"查询出错");
}else if (res.length > 0) {
console.log("该邮箱已被注册!");
return result.json({ success: true, message: 该邮箱已经被注册 });
}else{
// 发送邮件
const mailOptions = {
from: 发件邮箱 ,
to: email,
subject: 邮件主题 ,
text: `这是你的验证码 -- ${code}
内容
`
};
// 通过transporter对象的方法sendMail发送邮件,mailOptions为邮件的具体内容
transporter.sendMail(mailOptions, (err, info) => {
if (err)
console.log("发送失败",err);
});
console.log(email,code);
const sql = INSERT INTO verification_code (id, email, vfc_code) VALUES (?, ?, ?) ;
db.query(sql, [0, email, code], (err,res) => {
if(err){
console.error( Error sendVerificationCode user: , err);
// res.send({ success: false, message: 发送失败 });
return;
}else{
console.log("已将邮箱以及验证信息添加至暂时存储的数据库")
// res.send({ success: true, message: 发送成功 });
}
});
}
});
}
});
// 启动服务器
app.listen(interface, () => {
console.log(`服务器正在运行于 http://localhost:${interface}`);
});
注册账号的nodejs文件
// 使用了 express 框架构建服务器应用,注意需要控制允许访问的请求头 res.setHeader
// 在前端通过通过设置`Content-Type`为`application/json`,
// 将请求中发送的数据设置JSON格式,从而正确地解析和处理请求的数据。解析失败会不能获取到想要的值
// author:WY
const express = require( express );
const mysql = require( mysql );
const bodyParser = require( body-parser );
const Connection = require( mysql/lib/Connection );
const app = express();
const port = 8082;
// 设置请求头
app.use((req, res, next) => {
res.setHeader( Access-Control-Allow-Origin , * );
res.setHeader( Access-Control-Allow-Methods , GET, POST, PUT, DELETE );
res.setHeader( Access-Control-Allow-Headers , Content-Type );
next();
});
const connection = mysql.createConnection({
host: "localhost",
user: "数据库用户名",
port: 3306 ,
password: "数据库密码",
database: "数据库名",
});
connection.connect();
app.use(bodyParser.json());
// 对应的路由
app.post( /register ,(req, res) => {
const { email, password, verificationCode} = req.body;
if(!email || !password || !verificationCode){
res.end("未能正常获取到某值");
return
}else{
console.log(email,password,verificationCode);
const reEmailsql = SELECT * FROM user_email WHERE email = ? ;
connection.query(reEmailsql,[email],(err,res) => {
if(err){
console.error(err,"查询出错");
}else if (res.length > 0) {
res.json({ success: false, message: 该邮箱已经被注册 });
return
}
});
}
const vfcSql = SELECT * FROM verification_code WHERE email = ? AND vfc_code = ? ;
connection.query(vfcSql, [email, verificationCode], (err, result) => {
if(err){
console.log(err);
// res.status(500).send( 验证码验证失败 );
}else if (result.length > 0) {
const deleteSql = DELETE FROM verification_code WHERE vfc_code = ? ;
connection.query(deleteSql, [verificationCode], (err) => {
if(err)
console.log(err);
});
// res.send("验证成功");
// 验证成功之后执行插入操作,将正确的email和password添加进用户表内
const sql = INSERT INTO user_email (id, email, password) VALUES (?, ?, ?) ;
connection.query(sql, [0, email, password], (err, result) => {
if (err) {
console.error( Error registering user: , err);
res.json({ success: false, message: 注册失败 });
return;
}
console.log( User registered );
res.json({ success: true, message: 注册成功,请点击返回登录 });
});
}
// res.status(401).send( 验证码验证失败 );
});
});
app.listen(port,() => {
console.log(`${port}`);
})
登录组件
在制作登录功能的时候,依旧是第一判断当点击登录按钮时,用户有没有输入邮箱信息,用没有输入密码,然后就该分析这个邮箱有没有注册过,密码是否输入正确。
在验证通过后来,使用jwt(jsonwebtoken)返回一个token值并且通过cookie存储该token,在路由导航守卫中通过token来限制没有token的用户进入内部页面。
需要注意的一点是,按照我写的这个页面来说,要想实现初始页面点击img进入登录页面的话,路由守卫的没有token值的情况就应该是返回到路由为”/”的页面下。这样子页面的逻辑才走的通。
<script>
import axios from axios ;
export default {
data(){
return{
email: ,
password: ,
};
},
methods: {
login(){
if(!this.email || !this.password){
this.errorMessage = 请先填写邮箱
return;
}
axios.post( http://localhost:443/login , {
email:this.email,
password:this.password,
},{
headers:{
Content-Type : application/json
}
}).then(response => {
const data = response.data;
if(data.success){
console.log(data.message);
console.log(data.token);
const token = data.token
this.$cookie.set( token , token , { expires: 1h }); //在cookie中存储token,限时1h
this.$emit( closeImg ) // 通过 $emit 触发父组件自定义事件 closeImg
this.$router.push( /gameHome ); // 将用户路由到首页
}else{
this.errorMessage = data.message;
}
}).catch(error => {
console.error( Error ,error);
this.errorMessage = "登录失败"
});
},
}
};
</script>
router部分代码
import Vue from vue ;
import VueRouter from vue-router ;
import loginTmpVue from @/components/loginTmp.vue ;
import registerTemVue from @/components/registerTem.vue ;
import GameScoreLayer from @/components/GameScoreLayer.vue
import VueCookie from vue-cookie ;
Vue.use(VueCookie);
Vue.use(VueRouter);
const routes = [
// 添加路由配置
{ path: /loginTmp , component: loginTmpVue},
{ path: /registerTem , component: registerTemVue},
{ path: /gameHome , component: GameScoreLayer}
];
const router = new VueRouter({
mode: history , // 使用history模式,去掉URL中的#
routes
});
router.beforeEach((to,from,next) => {
const cookieToken = VueCookie.get( token );// 通过cookie获取存储的token
if (to.path == "/loginTmp") {
next();
}else if (!cookieToken) {
next("/");
}else{
next();
}
})
export default router;
后端node
const express = require( express );
const bodyParser = require( body-parser );
const mysql = require( mysql );
const jwt = require( jsonwebtoken );
const app = express();
const port = 443;
// 定义一个私有的密钥进行签名验证
const secretKey = mySecretKey ;
app.use((req, res, next) => {
res.setHeader( Access-Control-Allow-Origin , * );
res.setHeader( Access-Control-Allow-Methods , GET, POST, PUT, DELETE );
res.setHeader( Access-Control-Allow-Headers , Content-Type );
next();
});
const connection = mysql.createConnection({
host: "localhost",
user: "数据库用户名",
port: 3306 ,
password: "数据库密码",
database: "数据库名称",
});
connection.connect();
// 通过bodyParser解析请求体
app.use(bodyParser.json());
app.post( /login ,(req,result) => {
// 接收请求体的值
const {email, password} = req.body;
if (!email || !password) {
return result.json({ success: false, message: 请输入完整的登录信息 });
}else{
const verifySql = SELECT * FROM user_email WHERE email = ? AND password = ? ;
connection.query(verifySql,[email, password],(err,res) => {
if (err) {
console.error(err,"查询出错");
}else{
if(res.length > 0){
// 验证通过,账号密码存在。开始生成 token
const token = jwt.sign({ email: email }, secretKey);
// 将生成的 token 返回给前端
result.json({ token: token, success: true, message: 验证成功 });
console.log(token);
return
}
}
})
}
});
app.listen(port,() => {
console.log(`${port}`);
})
还有一些细节方面的问题:
点击登录后来清除img-bg组件和登录组件:
在组件的设计中,imgbg组件被当作背景设在app.vue中,而登录和注册组件通过在imgbg中设置router - view来显示。这就造成了一个状况,当点击登录后来,新出现的组件变为了imgbg的子组件,且imgbg组件并未被覆盖,就需要我们在点击登录操作的同时,先对组件imgbg进行清除,再跳转路由到home界面。
点击登录对登录组件的父组件触发自定义事件”closeImg”:
this.emit( closeImg ) // 通过 emit 触发父组件自定义事件 closeImg
在父组件中接受事件closeImg,并设置函数handleLogin来控制组件的显示:
<router-view v-on:closeImg="handleLogin"></router-view>
handleLogin(){
console.log("img元素隐藏");
this.$refs.screensaverImage.remove();
}
关于前端上传数据到后端,后端添加数据到数据库
写这个注册登录的实现,最大的阻力出目前前端上传数据到后端,后端解析前端数据的部分,在写这部分时。
第一是由于并未熟悉Axios上传的特性,将上传体包裹为javascript的形式直接上传至后端使用querystring.parse进行解析,且并没有将该javascript对象转化URL查询字符串,导致后端打印邮箱和账号的情况一直为null。
然后就是在改正了上传形式为formdata数据之后,依旧使用querystring.parse来解析数据,导致数据解析为字符串,提取不到有用信息,依旧出现打印结果为null的情况。
目前分析实则就可以发现,出问题的主要结果就是上传数据和解析数据的方式不匹配,解决方式也还算简单,设置请求头为json格式,上传时将javascript对象转化为json格式,在后端通过express的中间件函数bodyParser.json()把json格式转化为javascript对象,再逐级得到需要的值,具体代码已经在上述文本中给出。
在后端添加数据到数据库时,需要注意的则是,数据库允许存储的字段和添加的字段是否匹配,以及添加数据的逻辑问题。列如,注册时,这个邮箱是否在数据库中已存在,提交的数据是否完全等等,都需要去判断和思考



















暂无评论内容