1. 起因
不想看废话的 hxd,可以直接跳到目录第四点知识储备,呜呜~
想着弄出一个用户登录功能,结果在用 Node.js 写后端服务的时候,遇到了困难,待我细细说来~
登录的思路,就是前端发起请求,携带用户填写好的用户名以及密码,后端收到请求,连接数据库,查询表里面是否有该用户,用则登录成功,返回给前端该用户的其他信息。
2. 问题代码
2.1 前端代码
JS 代码如下:
const btn = document.getElementById('btn')
btn.addEventListener('click',function() {//给按钮d绑定
const user = document.getElementsByClassName('input')[0].value
const password = document.getElementsByClassName('input')[1].value
axios.get(`http://localhost:3001/home?user=${user}&password=${password}`)
//测试用的 GET 请求,应该要用 Post 的
.then(res =>console.log(res.data))
.catch(err => console.log('我错了'+err))
})
上边的代码不难理解,就获取用户输入的数据,作为请求的参数发送出去。
2.2 数据库截图
user
表如下:
2.3 后端代码
2.3.1 代码
index.js 文件如下:
const mysql = require("mysql");
const cors = require("cors");
const express = require("express");
const app = express();
app.use(cors()); //express利用cors解决跨域
let result = "2111"; //定义全局变量,给初始值
function connectMysql(user, password) {//自定义函数,连接数据库,获取数据
let connection = mysql.createConnection({
host: "localhost",
user: "root",
password: "******",//mysql数据库的密码
database: "test",
});
connection.connect();
connection.query(
"SELECT user_name,user_password FROM user",
function (error, results, fields) {
if (error) throw error;
for (let i = 0; i < results.length; i++) {
if (results[i].user_name === user &&results[i].user_password === password) {
result = results[i].id;//匹配上了,就将用户名字赋值给全局变量
break //不再查找,结束循环
}else{
console.log('匹配失败')
}
}
}
);
connection.end();
}
app.get("/home", (req, res) => {
const {user,password} = req.query;
connectMysql(user, password)
console.log('我可是有响应她的')
res.send(result)
});
app.listen(3001);
console.log("服务启动成功了");
上边的代码有一些值得注意的地方:
- 引入
mysql
模块用来连接数据库 - 使用
express
框架来创建服务 - 引入
express
的cors
中间件来解决跨域问题,因为服务端是在3001
端口运行着的,前端则默认在5000
端口
2.3.2 跨域拓展
说到跨域问题,想起来之前看到过的一个有意思的事(代理的原理):
如图:由 3000 端口直接向 5000 端口发送请求,会导致跨域问题,但是注意,这里的 3000 端口的请求是可以发送出去的,但是5000端口响应回来的数据,客户端的 ajax 引擎会阻挡(安全第一),设置一个 3000 端口的代理,这个中间人没有引擎,所以可以接收5000端口的数据,再由相同的3000端口把数据给客户端的3000端口,这样就能解决同源跨域问题。
去掉 cors
中间件,在终端 node index.js
前端,前端输入正确的用户名和密码(1,1),我们可以看到:
- 终端:
- 浏览器控制台:
结果一目了然,上述结论正确!
2.4 问题浮现
回归正题:我们把 cors
中间件加上去,重复上述操作,预计的结果如果用户信息正确,后端返回用户名给前端,这里返回的结果我用的是一个全局变量 result
,初始值给了个 0
。但是幸福总是来得那么突然,输入正确的消息后,浏览器控制台打印出的是初始值,第二次点击按钮,才返回用户名,如图:
聪明的小伙伴一眼就能发现问题,这不就是异步了嘛!也就是说连接数据库是异步操作?我也在群里问起了起来,老哥说:
暂时就理解为 connection.connect();
是多线程,同步的,connection.query()
是异步的
3.无脑改动
但是现在的问题不就是让异步操作变同步嘛!心想着这操作我懂, async
和 await
给我用起来。于是我改动了 index.js
的代码如下:
3.1 无脑行为1
主要做了如下改动:
- 在声明
connectMysql
函数前加上async
,因为await
要在async
函数里面才能用 - 在
connection.connect
,connection.query
,connection.end()
加上await
关键词
但是也不行,因为 connectMysql
还是异步的,并没有解决问题,为了解决问题我也是煞费苦心,各种变换 async
,await
的位置,甚至搞出了如下的sao代码:
3.2 无脑行为2
app.get("/home", (req, res) => {
const {user,password} = req.query;
const myFun2 = async function aaa(){//await要放在函数里面
await connectMysql(user, password);
};
myFun2();//这个函数还是异步的呀!这个函数没执行完,就已经send了(自己的脑回路就很离谱)
res.send(result)
});
结果可想而知,浏览器控制台打印依旧是异步的。
3.3 无脑行为3
app.get("/home", (req, res) => {
const {user,password} = req.query;
const myFun2 = async function aaa(){//await要放在函数里面
await connectMysql(user, password);
res.send(result)
};
myFun2();
});
其实到了这一步,我觉得处理异步的逻辑,应该是没有问题了,但是还是没能解决问题,我意识到是我自己知识点出了问题,光靠 async
和 await
不能解决问题,于是我想到了 Promise
,不是说 Promise
是异步编程的一种很好的解决方案吗,但是我不知道,Promise
和 async
和 await
如何结合使用,于是我踏上了重新学习的道路,这一学才发现自己的基础知识点确实没掌握好。
4 知识储备
4.1 单线程与异步
JavaScript 是单线程(一个主线程)的,所以为了充分利用资源就用了异步的思想,对于一些比较耗费时间的,比如加载文件,发起网络请求,还有定时器等操作,遇到这些操作,就新开一个子线程,主线程依旧干着那些比较主要的事情,让这个子线程去干耗费时间资源的事,这就是异步。
4.2 回调
但是子线程有一个局限:一旦发射了以后就会与主线程失去同步,我们无法确定它的结束,如果结束之后需要处理一些事情,比如处理来自服务器的信息,我们是无法将它合并到主线程中去的。
所以就会有了回调函数的概念,子线程执行完毕,就执行我指定的函数。同步的回调函数很好理解,
function f2(name) {
alert('Hello ' + name);
}
function f1(callback) {
var name = prompt('输入用户名');
callback(name);
}
f1(f2);
log('我不是恁爹')
异步回调我见过的大多是封装好的,像setTimeOut(callback,time)
,fs.readFile(path,callback)
,对 Node.js 使用不多,但是作为写服务端的语言,肯定是可以创建子线程,并对此进行事件监听的。
但是回调函数有很多的问题,也就是常说的回调地狱,层级太多,不利于阅读维护
setTime(()=>{
setTime(()=>{
setTime(()=>{
setTime(()=>{
});
});
});
console.log("小林别闹");
});
4.3 Promise
Promise
应运而生,它最早由社区提出并实现,ES6 将其写进了语言标准。像上述的回调地狱,使用 Promise
的链式能更加方便,直观的写
4.3.1 基础用法
function sleep(ms) {
return new Promise(function(resolve, reject) {
setTimeout(() => {
console.log('小林别闹')
resolve('1')
}, ms);
})
}
sleep(500).then( ()=> console.log("finished"));
观察上边的代码,我们一点一点的来说到说到,首先 new
了一个新的 Promise
,传递的是一个函数参数,但是这个函数会在 new
的时候就马上执行,我们需要控制它的执行时机,所以我们在外面包装一个函数,当函数调用的时候再执行。
那 Promise
到底是如何方便的实现异步编程的呢,我们在参数函数里面放如入应该执行的异步操作,我们根据该操作执行的情况,执行来改变 Promise
的状态,Promise
有三种状态 Pending(默认),Fulfilled(成功),Rejected(失败)。通过执行 resolve()
和 reject()
来实现 Pending -> Fulfilled 和 Pending -> Rejected 的状态变化,注意 Promise
的状态只会变化一次。
使用 resolve
,reject
传递的参数,可以在 then
里面接收到:
sleep()
.then((result)=>{
console.log(result);
}, (_err)=> {
console.log('我出错啦,进到这里捕获错误,但是不经过catch了');
})
如果上一个 Promise
中的操作成功,使用resolve()
,则then的第一个参数接收结果并执行,reject()
则是第二个。
4.3.2 链式
注意:链式是靠往后面传递一个 Promise
来维护的,因为只有在 Promise
里面才存在,这样通过链式,我们就可以控制异步操作的执行顺序
sleep()
.then((result)=>{
return new Promise(function(resolve, reject) {
resolve(result+1)
})
})
.then((res) => {
consolr.log(res)
})
当然,你不在 then 里面返回一个 Promise
后面的 then,也是会执行的,但是遇见异步的操作,顺序就完全不受控制,看哪个异步操作先完成了,失去了使用 Promise
解决异步编程的意义。
4.3.3 错误捕获
上边已经提到过,我们可以使用 then
的第二个参数来捕获错误,同时我们也可以使用 catch
来捕获
sleep()
.then((result)=>{
console.log(result);
})
.catch(err => {
console.log('我错了'+err)
})
有一些需要注意的地方:
then
的第二个参数以及catch来捕获错误都不会break
链路,也就是说,捕获到错误后,后面的then
语句是可以正常执行的,所以catch
语句的位置也是有讲究的,重要且必须执行的语句一般放在catch
语句后面,记得还出了个Promise.prototype.finally()
。
finally()
方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
catch
能够捕获到整条链路上的错误,一旦捕获到,错误发生地,到catch
语句之间的then
都不会执行了,而then
的第二个参数是捕获上一个promise
错误,两种捕获方式,谁捕获到就看谁在链路上的位置考前了。
promise
的内容远不止如此,蛋解决这次问题,这些就差不多了,等我过段时间看了存了老久了的廖雪峰老师的《ES6标准入门》后或许能够更加系统的学习到 Promise
4.4 aysnc and await
Promise
的使用也是存在一些问题的,大量复杂的 Promise
不易读,而且promise链式中断很不方便,就有了 async
和await
.
需要记住:
async
,await
缺少一个就没有意义了,await
必须在async
声明的函数内部使用async
声明的函数的返回本质上是一个Promise
,获取返回值可以通过then
await
后面必须是一个Promise
才起作用,等待到有返回值了,代码往下继续执行 (我的无脑改动三就是这点没有意识到)
const demo = async ()=>{
let result = await new Promise((resolve, reject) => {
setTimeout(()=>{
resolve('res')
}, 1000)
});
console.log('我由于上面的程序还没执行完,先不执行“等待一会”');
return result;
}
// demo的返回当做Promise
demo().then(result=>{
console.log(result); // res
})
链式的 promise
:promise1().then(() => {return promise2}).then(() => {return promise3})
使用 async
和 await
后可以这样写了:
(async ()=>{
const result1 = await promise1();
const result2 = await promise2(result1);
const result3 = await promise3(result2);
console.log(result3);
})()
感觉直观了很多。
至此,我已经找到解决问题的方法了,服务端代码具体如下:
const mysql = require("mysql");
const cors = require("cors");
const express = require("express");
const app = express();
app.use(cors()); //express利用cors解决跨域
let result = "2111"; //定义全局变量,给初始值
async function connectMysql(user, password) {//自定义函数,连接数据库,获取数据
let connection = mysql.createConnection({
host: "localhost",
user: "root",
password: "*******",
database: "test",
});
connection.connect();
return new Promise((resolve,reject) => {
connection.query(
"SELECT user_name,user_password FROM user",
function (error, results, fields) {
if (error) throw error;
for (let i = 0; i < results.length; i++) {
if (
results[i].user_name === user &&
results[i].user_password === password
) {
result = results[i].user_name;//匹配上了,就将用户名字赋值给全局变量
// res = results
console.log('匹配成功'+result)
resolve('promise'+result)
}else{
console.log('匹配失败')
}
}
}
);
connection.end();
console.log(result+'connectMySql')
})
}
app.get("/home", (req, res) => {
const {user,password} = req.query;
const myFun2 = async function aaa(){//await要放在函数里面
await connectMysql(user, password)//await 后面必须是一个 promise
res.send(result)
};
myFun2();
});
app.listen(3001);
console.log("服务启动成功了");
参考: