使用koa+mysql写一个简易论坛(三)

一、本期目标及知识点概况

1)目标

1、完善注册与登录
2、导航栏改造
3、退出登录

2)知识点概况

  • onchange && oninput
  • 光标定位
  • mysql 的使用(为了方便,我们使用了mysql2中间件)
  • session 的使用
  • fetch的使用

二、知识点讲解

1、onchange

1.1 为什么要使用onchange

我们必须有一个事件用来上传用户输入的数据,所以光有Fetch传送数据还不行,我们还需要监听input输入框,在其达到某一种状态的时候将数据获取到然后上传到后端,很明显,点击事件并不靠谱,这就需要用到onchange了,它可以帮助我们达成目的。前面我还说了一个oninput,那么它们的区别是什么,为什么要使用onchange而不是oninput呢?

1.2 onchange 与oninput的区别
  • onchange事件在内容改变(两次内容有可能还是相等的)且失去焦点时触发。=> 例如你在输入框中输入了一些字符,当你离开这个输入框时或者按下enter或者tab键时,该事件都会触发。
  • oninput 在value改变时实时触发,即每增加或删除一个字符就会触发,然而通过js改变value时,却不会触发。

其实还有个跟它俩差不多:那就是 onpropertychange,只不过这是IE的,就不说了。毕竟IE用的人也不多,应该是吧,反正我是不用的。

  • 结论
    很明显如果使用oninput是不太好的,因为它请求的次数会很多,浪费资源。当然,如果你想实时监听也可以使用oninput。

  • 2、 光标定位

想把光标定位到哪个input上,就获取到该input =>
例如: 定位到

<input type="text" id="userInput"  placeholder="..."></input>

可以这么写 =>

document.getElementById("userInput").focus();
document.getElementById("userInput").select();

3、mysql2

https://github.com/sidorares/node-mysql2

3.1下载包

npm install --save mysql2

3.2配置与使用
image.png

4、session的配置与使用

https://github.com/koajs/session

image.png

具体各个配置选项的含义我就不说了,要想了解可自行查看官方文档,如果你觉得上面的文档不够详细,也可以参考这个(这是express框架的,其实使用方式差不多,koa本来就是开发express的人开发的)

https://github.com/expressjs/session

5、Fetch

5.1. 为什么要用Fetch

在日常生活中,我们在部分网站注册账号的时候明明没有提交数据,页面就提示我们输入的信息是否可用,这个大家应该都有见过吧。这个功能其实就是靠Ajax或者类似技术实现的,我们要做的网站虽然简单,但麻雀虽小,五脏俱全,这个东西还是得用上的:
Ajax 即“Asynchronous JavaScript and XML” : 异步的JavaScriptXML技术

但是我们现在有更好的选择,那就是Fetch,Ajax能做的它也可以,而且更加简单。所以,在这个项目上我使用的就是Fetch啦。

5.2 Fetch文档与教程

MDN

https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API/Using_Fetch

Github

https://github.com/github/fetch

Node.js >> node-fetch

https://github.com/bitinn/node-fetch

5.3 Fetch起步

一个简单的fetch请求很简单

fetch('http://example.com/movies.json')
 .then(function(response) {
   return response.json();
 })
 .then(function(myJson) {
   console.log(myJson);
 }); 
5.4 此项目中的Fetch实例
fetch.png
5.5 fetch参数说明

1、url : 你请求的地址 => 例如请求 http://localhost:3000/signUp
=>那么 url = "/signUp"
2、method : 请求方式 => 一般也就POST || GET
3、headers: 请求头 => 这里可以设置数据格式 => 如图,我这里就是json格式
4、body: 发送到后端的数据 => 使用JSON.stringify可以数据转换成JSON格式 =>
如: 图中发送的数据 {email: email, labelValue: labelValue2} 由后台接受就是一个对象,他的key分别是email跟labelValue, value就是email 跟labelValue2的值
5、第一个then: 如果后端返回数据成功就会执行它,一般直接写:return ```

response.json();
// 其中的response是第一个then的形参,可以自己定义的。

6、第二个then: 执行完第一个then后执行它,在这里你可以对后端返回的数据进行操作,同理,上图中的myJson也是形参,可自定义。
注意:如果使用了Fetch,那么发送请求后后端一定要返回数据,要不然浏览器就会报错

7、后段返回直接用 ctx.body = data 即可(data为json数据)

三、注册功能的实现

1、 准备

  • 为了是我们的代码可读性更高,从这一期开始我们将各模块分开
  • 首先创建lib文件夹,用来保存数据库操作的相关文件
  • 这个项目总共使用了4个表,这一期我们要使用其中的user,所以在lib文件夹下创建user.js文件用来存放对表user进行操作的函数
  • 在routes文件夹下新建user_do.js文件,存放用户操作相关的方法,将index.js中的用户请求登录与注册页面的模块移到user_do.js中
  • 因为在user_do.js中要对表user进行操作,所以我们除了要引koa-router,还要将lin/user.js模块引入其中 =>

const editUser = require('../lib/user');

  • 为了让user_do.js路由模块生效我们还得在app.js中引用它并将其配置好

const userRouter = require('./routes/user_do');
app.use(userRouter.routes()).use(userRouter.allowedMethods());

2、实现

  • 2.1 需求

2.1.1 写作目的

经过我几个月的学习经验来看,在写一个项目或者完成某一功能模块的时候,梳理清除用户和自己的需求(用户需求的话可以换位思考或者参考其他网站,不嫌麻烦调研也不是不行),搞清楚自己想要达到的目标是什么是很重要的,当然,现在网站那么多,我们完全可以参考其中一些优秀网站的做法。

2.1.2 需求说明(按input输入框划分)

注册界面 =>


image.png
  • Username
    1、 将注册用户输入的用户名与user表中的所有用户名进行对比
    2、 如果该用户名不存在,则提示用户该用户名可以注册,并将提示文字变为绿色。


    image.png

3、 如果该用户名已存在,则提示用户该用户名被占用,请重新输入,并将提示文字字体设为红色,加大加粗,光标定位到当前输入框


image.png
  • Email
    1、 检测用户输入的邮箱中是否包含“@”、“.com”
    2、如果缺少任意一个,则提示用户输入的邮箱不是一个有效的邮箱地址,字体变大变红,光标定位到当前输入框


    image.png

3、然后检测邮箱是否被占用
4、未被占用提示该邮箱可以使用,字体颜色变绿


image.png

5、被占用提示邮箱已被占用


image.png

注:其实这个规则并不完善,例如:“@.com”,这很显然不是一个有效的邮箱,还需要改进

  • Password
    1、密码的检测并不需要提交数据到后端
    2、检测用户输入的密码是否为6到16位(包括6和16)
    3、否 => 提示用户: 输入的密码长度应为6到16位,字体为红色


    image.png

3、是 => 无需提示

  • rePassword
    1、重复密码,不需要提交到后端
    2、比较用户两次输入的密码是否相等
    3、否 => 提示用户两次输入的密码不一样,要求重新输入,字体为红色


    image.png

    4、是 => 无提示

  • Sign up 按钮
    1、因为这里我们使用Fetch,所以这个按钮的类型为button,而非submit
    2、当用户在之前输入的数据都可以使用的时候,我们才将这些信息提交到后台,将其存入数据表user中,此时,用户注册成功,我们可以将页面跳转到主页,并用session将用户的登录状 态保存来
    3、如何确保前面用户输入的信息准确无误呢 => 我的做法是创建一个数组signArray,每当用户输入的信息准确无误就向该数组中添加一项,那么当该数signArray.length为4时,也就说明用户输入的信息是没有错误的了。只有signArray.length = 4时,用户点击此按钮才会提交数据到后端,否则点击此按钮提示:请填写所有信息并确认无误
  • 2.2 具体实现(前端)

经过上面的一些说明,实现这一步其实已经变得很容易了。但是我们总共有三个函数需要上传数据,那么我们如何判断上传的数据是哪一个呢?
=> 我的做法是: 将input上面的label标签里的value取到,然后将它与input的value一起上传到后端,在后端通过使用switch判断label标签里的value来判定前端上传的数据到底是哪一个,然后再根据这个返回不同的数据。

2.2.1、首先给每个input标签一个onchange事件,并设置一个函数,然后再实现函数 =>
  • Username => onchange="listenSignUsername()"
  • Email => onchange="listenSignEmail()"
  • Password => onchange="listenSignPassword()"
  • RePassword => onchange="listenSignRePassword()"
2.2.2、在正式写函数前,我们把各个函数的共有属性定义好

=>


const.png

为什么要加图中红色框里的代码呢?
=>
因为不加的话我们获得字符串就不是我们要的,例如:
labelValue1=> 如果不加的话我们获得字符串就是 " Username:",这个相比于"Username",前面多了个空格,后面多了个中文的“ :”,而

 String.substring(firstIndex, lastIndex)

这个方法就是用来去除掉这些的。它的作用是截取原字符串中索引firstIndex到lastIndex的部分,并将其作为一个新的字符串返回,所以我们可以使用这个方法去除掉原字符串中前面的空格和后面冒号,其firstIndex = 1,lastIndex = 原字符串的长度 -1 。

2.3、实现函数

这些函数里面包含的东西我之前就说过了,这里就不一一赘述了,具体解释见源代码,源代码地址我会放在文章结尾

listenSignUsername

=>


listenSignUsername.png

listenSignEmail

=>


listenSignEmail.png

listenSignPassword

=>


listenSignPassword.png

listenSignRePassword

=>


listenSignRePassword.png

Sign up

image.png

这部分需要注意的是
=>
因为我们使用的是Fetch传输数据,而非表单,所以在后端我们无法使用render或者redirect跳转页面,不过我们可以使用JS跳转
即 window.location = url; (url为你要到达的网址)

  • 2.4 具体实现--后端代码及解析

代码有点小长,截图字体太小,还是写吧 =>

// 用户注册 -- 获取用户在form表单中输入的数据,判断是否合乎规则,如果正确则将数据存入数据库
router.post('/signUp', async(ctx) => {
  // 获取前端传过来的Json数据,它是一个对象
    const postData = ctx.request.body;
  // 获取传过来的<label>或注册按钮的值的值
    const labelValue = postData.labelValue
  // switch 语句,判断labelValue的值
    switch(labelValue) {
        //当labelValue = "Username"
        case "Username":
            // 获取用户输入的用户名
            const username = postData.username;
            // 操作数据库,通过该username从数据库中获取user,
            const getUserByUsernamePromise = editUser.getUserByUsername(username);
            // 定义返回的数组为row1
            const row1 = await getUserByUsernamePromise;
            //如果row1的长度不为零,则说明改用户名已被使用
            if(row1.length !== 0) {
                ctx.body = {msg: "用户名已被占用,请重新输入"};
            } else {
                ctx.body = {msg: "此用户名可以使用"};
            }
            break;
      //当labelValue = "Username"
        case "Email address":
            // 获取用户输入的邮箱
            const email = postData.email;
            // 操作数据库,通过该email从数据库中获取user,
            const getUserByEmailPromise = editUser.getUserByEmail(email);
            // 定义返回的数组为row2
            const row2 = await getUserByEmailPromise;
            //如果row2的长度不为零,则说明改用户名已被使用 
            if(row2.length !== 0) {
                ctx.body = {msg: "用邮箱已被占用,请重新输入"};
            } else {
                ctx.body = {msg: "此邮箱可以使用"};
            }
            break;
        //当labelValue = "Username"
        case "Sign up":
             // 获取用户输入的用户名
            const username1 = postData.username;
            // 获取用户输入的邮箱
            const email1 = postData.email;
            // 获取用户输入的密码
            const password1 = postData.password;
            // 将用户名、邮箱、密码组成一个数组data,将data作为参数传到数据库操作函数addUser中
            const data = [username1, email1, password1];
            // 操作数据库,添加用户信息
            const addUserDataPromise = editUser.addUser(data);
            await addUserDataPromise;
            // 通过邮箱获取用户的所有信息
            const usersPromise = editUser.getUsernameByEmail(email1);
            const users = await usersPromise;
            //将获取到的用户信息转为对象
            const user = users[0];
            // 使用session保存登录用户的信息
            ctx.session.user = user;
            // 但会数据到前端
            ctx.body = {msg: "注册成功"}

            break;
    }
})

⚠️ 上面代码中,我们为什么不直接把data存到session里呢?反正用户信息都是一样的。

  • 因为我们后面会往user表中添加其它字段,并为它们设置默认值,这样一来两者包含的信息量就很不一样了。现在这种包含的信息会更多。不过信息量的多少倒是其次,关键的是在后面的项目中我们会经常用到session里保存的用户信息(例如用户头像),如果我们将data存进去,还能取到用户的头像吗?很明显不能,所以我们需要统一以下做法,以确保session里保存了该用户的所有信息,登录界面也一样。

四、登录功能的实现

=> 同样的先给input添加onchange事件,检测用户输入的信息格式是否有误
=> 有误 --> 给出提示
=> 无误 --> 通过Fetch将数据传到后段,再通过该数据操作数据库,看看user表中是否有完全匹配的数据,有则返回"登录成功",并存储session。没有则返回"Incorrect username or password."

=> 前端再根据返回结果的不同来做不同的事
=> "登录成功" --> 跳转到主页
=> "Incorrect username or password." --> 将保存登录失败细心的div的display属性设置为block。

就是这么个逻辑了,我就不再一一赘述了,因为登录比注册简单多了,具体代码如下:
前端JS
=>

<script>

    let buttonLogin = document.getElementById('btn_login');
    let errorBox = document.getElementById("js-flash-container");
    let closeButton = document.getElementById("closeButton");

    document.onkeydown = function(e) {
        let ev = e || window.event;
        if (ev.code === 13) {
            buttonLogin.click();
        }
    }

    buttonLogin.onclick = function () {
        let inputEmail = document.getElementsByTagName("input")[0].value;
        let inputPassword = document.getElementsByTagName("input")[1].value;
        const url = "/signIn";
        fetch(url, {
            method: 'POST',
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json',
                'X-Requested-With': 'XMLHttpRequest'
            },
            body: JSON.stringify({
                inputEmail: inputEmail,
                inputPassword: inputPassword,
            })
        })
            .then(function(response) {
                return response.json();
            })
            .then(function(myJson) {
                console.log(myJson.msg);
                if (myJson.msg === "登录成功") {
                    window.location="http://localhost:3000";
                } else {
                    errorBox.style.display = "block";
                    document.getElementsByTagName("input")[0].value = "";
                    document.getElementsByTagName("input")[1].value = "";
                }
            });
    }

    closeButton.onclick = function() {
        errorBox.style.display = "none";
        document.getElementsByTagName("input")[0].value = "";
        document.getElementsByTagName("input")[1].value = "";
        document.getElementById("labelEmail").removeAttribute("class");
        document.getElementById("labelPassword").removeAttribute("class");
    }

    function listenEmail(){
        let emailLabel = document.getElementById("labelEmail");
        let inputEmail = document.getElementsByTagName("input")[0].value;
        for(let i = 0; i < inputEmail.length; i++) {
            if((inputEmail.indexOf("@") === -1) || (inputEmail.indexOf(".com") === -1)) {
                document.getElementsByTagName("input")[0].focus();
                document.getElementsByTagName("input")[0].select();
                emailLabel.innerHTML = "您输入的邮箱不合法";
                emailLabel.setAttribute('class', 'text-danger');
                buttonLogin.disabled = true;
            } else {
                emailLabel.innerHTML = "Email address";
                emailLabel.removeAttribute('class');
                buttonLogin.disabled = false;
            }
        }
    }

    function listenPassword(){
        const labelPassword = document.getElementById("labelPassword");
        const inputPassword = document.getElementsByTagName("input")[1].value;
        const length = inputPassword.length;
        if (length < 6 || length > 16) {
            document.getElementsByTagName("input")[1].focus();
            document.getElementsByTagName("input")[1].select();
            labelPassword.innerHTML = "请输入6至16位的密码";
            labelPassword.setAttribute('class', 'text-danger');
            buttonLogin.disabled = true;
        } else {
            labelPassword.innerHTML = "Password";
            labelPassword.removeAttribute('class');
            buttonLogin.disabled = false;
        }
    }

</script>

后端 router模块

//获取用户在form表单中输入的数据,并将其与数据库中储存的信息进行对比以判断是否允许该用户登录
router.post('/signIn', async(ctx) => {
    const postData = ctx.request.body;
    const email = postData.inputEmail;  //获取用户输入的邮箱地址
    const password = postData.inputPassword;  //获取用户输入的密码
    const data = [email, password];

    const rowsPromise = editUser.userLogin(data);
    const rows = await rowsPromise;

    const usersPromise = editUser.getUsernameByEmail(email);
    const users = await usersPromise;
    const user = users[0];

    if(rows.length === 0) {
            ctx.body = {msg: "Incorrect username or password."}
    } else {
        ctx.session.user = user;
        ctx.body = {msg: "登录成功"}
    }
});

五、导航栏改造

image.png

目前我们的主页是这样子的,导航栏右边共有6个选项
我们想要的应该是用户未登录只显示登录与注册按钮,登录了之后显示其它四个,去除登录与注册按钮。
我们直接找到导航栏文件head_navbar.html
因为用户登录之前是没有session的,登录之后才有session,所以我们可以根据这个来实现我们想要的结果.
=> 找到请求主页的路由,定义user = ctx.session.user,再将user传入render里
如下图:


image.png

然后进入head_navbar.html文件,使用ejs的模版语法
=>为代码:
if(user不存在) {
显示登录与注册按钮
} else {
显示其它4个按钮
}

代码如下:


image.png

六、退出登录

退出登录功能其实就是将session清空(设为null),很简单对不对
=>


image.png

七、源码地址

文章代码同步仓库
https://github.com/ShyGodB/Forum-Code-Synchronize-

我自己的项目仓库
https://github.com/ShyGodB/BBS-by-Koa-Mysql

至此,本期内容完结!下期再见

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,825评论 4 377
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,887评论 2 308
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,425评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,801评论 0 224
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,252评论 3 299
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,089评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,216评论 2 322
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 31,005评论 0 215
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,747评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,883评论 2 255
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,354评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,694评论 3 265
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,406评论 3 246
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,222评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,996评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,242评论 2 287
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 36,017评论 2 281

推荐阅读更多精彩内容

  • 首先,感谢 夕雅y的“❤️”。 上篇文章因为没有评论,我并不知道反响如何,不知道这种写作方式对于读者来说是否适合,...
    Qibing_Fang阅读 322评论 0 1
  • 项目开发常见流程介绍 需求调研 项目经理------>需求说明书 软件设计书 项目经理------>...
    _1633_阅读 1,332评论 1 6
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,042评论 1 32
  • 有你,就是晴天 阳光撒在你的发梢 仰起脸,45度的美好 洒脱而桀骜 ...
    一生只此一人的唯一阅读 183评论 0 1
  • “要过马路了,牵好弟弟的手。” 不知道从什么时候开始,那个曾经被父母抱着过马路到后来牵着父母的手过马路...
    悠悠穿堂风阅读 1,099评论 0 3