前言:前段时间学习了如何基于Flask框架部署服务器,感觉非常容易上手,而这段时间又开始接触和学习Node.js与Koa,相比较而言它们有着很强的特性(比如异步),摸爬滚打中总算是成功学会了部署简单的服务,在此记录一下坎坷的过程。
Node.js是一个事件驱动I/O服务端JavaScript环境,它执行Javascript的速度非常快,使得 JavaScript 成为与PHP、Python等服务端语言平起平坐的脚本语言。Koa则是一个新的Web框架,是Express的下一代基于Node.js的Web框架。Koa丢弃了回调函数,并有力地增强错误处理能力。同时Koa提供了一套优雅的方法,可以快速而愉快地编写服务端应用程序。Koa2作为新版本,完全使用Promise并配合async来实现了异步。
本文基于Node.js与Koa2框架,讲解如何搭建服务器,开发实现后端API。在此之前,我们要具备一些HTML和JavaScript的基础知识和异步编程的思想,如果你已经具备了,那就开始吧!
下载与安装
Node.js 安装包及源码下载地址为:https://nodejs.org/en/download/,在其中选择对应的系统即可。安装完成后命令行输入:
node --version
显示版本号,即代表安装成功。
Koa的安装参可考官方文档,在命令行输入:
npm install koa
等待安装完成即可。
至于Node.js的开发工具,笔者习惯使用功能强大的VSCode。
最简单的应用
我们在常用的工作目录下,创建一个文件,命名为server.js,输入代码如下:
const Koa = require('koa');
const app = new Koa();
app.use(async ctx => // 处理请求的异步函数
{
ctx.body = 'Hello World';
});
app.listen(8080); // 监听端口号
console.log("Server running at http://127.0.0.1:8080");
接着在命令行输入(首先要cd进入工作目录,如果是在VSCode中,在运行界面中选择终端即可):
node server.js
结果如下图,点击链接即可访问,如下图所示。
至此就搭建好了一个最简单的服务器,对客户端发送的请求,服务端回复一个“Hello World”。
PS:在终端退出程序按Ctrl+C即可,服务相应就停止。
路由
通常情况下,我们服务器需要不同的路由来处理不同的请求,而Koa拥有一个集中处理URL的中间件:koa-router。
PS:中间件(middleware)是Koa的一个重要组成部分,Koa拥有许多实用的中间件,我们安装的中间件都在工作目录下的node_modules文件夹中。中间件的执行顺序十分重要,理解了中间件,也就学会了Koa。
首先安装中间件,在命令行输入:
npm install koa-router --save
安装完成后,我们把刚才的server.js文件修改为如下:
const Koa = require('koa');
const router = require('koa-router')(); // 注意这里最后是函数调用
const app = new Koa();
// 添加路由
router.get('/', async (ctx, next) => // 根路由
{
ctx.body = '我是服务器,我可以处理很多请求,对应不同的路由';
});
router.get('/hello', async (ctx, next) =>
{
ctx.body = 'Hello!';
});
app.use(router.routes());
app.listen(8080);
console.log("Server running at http://127.0.0.1:8080");
再次运行,对应在浏览器中可以访问到不同的路由。
跨域
当域名、端口、协议有任意一个不一样的时候就会出现跨域。通常我们都希望可以跨域访问服务器,但在默认情况下,我们用Koa部署的服务器却不允许我们这么做,因此要设置可以跨域。设置跨域的方法很多,比如请求与响应的数据使用jsonp格式,而Koa里又封装了一个实现跨域的中间件:koa2-cors。
首先安装中间件,在命令行输入:
npm install koa2-cors --save
安装完成后,我们继续修改刚才的server.js文件。
在一开始引入语句后加入如下语句:
const cors = require('koa2-cors');
在最后为app增加中间件的语句中加入如下语句:
app.use(cors());
至此就实现了跨域了,在这里运行看不出有什么区别,但接下来我们设计客户端后,就可以在前端访问到我们的服务器了。
处理Get/Post请求
我们搭建服务器,最重要的功能就是能够接收请求、发送响应,在客户端、服务器之间顺畅地收发数据。接下来就分别讲解Get和Post两种方式如何请求与响应。
我们首先建立一个客户端页面,命名为client.html,其中代码如下:
<!doctype html>
<head>
<meta charset="utf-8"/>
<title>
简单客户端页面
</title>
</head>
<body>
ID:<textarea id="ID" style="height:15px;width:150px;"></textarea>
<br>
姓名:<textarea id="Name" style="height:15px;width:150px;"></textarea>
<br>
<button onclick="getSubmit()">以Get方式提交</button>
<button onclick="postSubmit()">以POST方式提交</button>
<script>
// 此处存放处理事件的代码
</script>
<script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js">
</script>
</body>
我们在客户端向服务器发送HTTP请求的方式有很多,而在这里采用的是jQuery中的$.ajax方法,其一般的调用方式如下:
$.ajax(
{
type: "str类型", // 请求方法,get或者post
url: "str类型", // 请求的URL路径,即服务器路径
data: // 发送的数据
{
attribute: value
},
dataType: "str类型", // 收发数据的格式,通常为json或jsonp
success: function(data) // 请求成功的回调函数,data即服务器返回的数据
{
console.log(data);
},
error: function(XMLHttpRequest, textStatus, errorThrown)
// 请求错误的回调函数,参数分别为XMLHttpRequest对象、错误信息、捕获的错误对象
{
console.log(textStatus);
}
});
因为要用到这一方法,所以在刚才的客户端页面中,千万不要漏掉最后的一段引用jQuery的JavaScript代码。
Get方式
在Koa中,获取Get请求接收的数据的来源是request对象中的query方法或querystring方法。其中query方法返回的是格式化的参数对象,querystring方法返回的是请求字符串。而Koa又可以直接从上下文中获取这两个方法,因此在服务器端,我们在刚才的server.js文件中增加一条路由:
router.get('/getData', async (ctx, next) =>
{
// 获取路由
let url = ctx.url;
// 从上下文的request对象中获取
let request = ctx.request;
let req_query = request.query;
let req_queryString = request.querystring;
// 从上下文中直接获取
let ctx_query = ctx.query;
let ctx_queryString = ctx.querystring;
// 显示
console.log(url);
console.log(req_query);
console.log(req_queryString);
console.log(ctx_query);
console.log(ctx_queryString);
// 发送数据
ctx.response.body = JSON.stringify("我已收到消息");
});
在刚才的client.html文件中,我们在script代码部分加入Get请求的事件处理函数:
function getSubmit()
{
$.ajax(
{
type: "get",
url: "http://127.0.0.1:8080/getData",
data:
{
ID: document.getElementById("ID").value,
Name: document.getElementById("Name").value
},
dataType: "json",
success: function(data)
{
console.log(data);
},
error: function(XMLHttpRequest, textStatus, errorThrown)
{
console.log(textStatus);
}
});
}
启动服务器,再在客户端页面中输入ID和姓名,点击“以Get方式”提交,可以看到URL中加上了客户端发送的数据的键值对,query和querystring方法各自可以对此解析,获取到前端以Get方式发送的数据,客户端也收到了服务器响应的信息。
Post方式
Koa又封装了一个处理Post请求的中间件:koa-bodyparser。
首先安装中间件,在命令行输入:
npm install koa-bodyparser --save
安装完成后,同样在server.js开始的引用部分添加:
const bodyParser = require('koa-bodyparser');
在最后为app添加中间件的部分添加:
app.use(cors());
app.use(bodyParser()); // 注意最后是函数调用
app.use(router.routes());
实际添加第2条即可,这里展示所有中间件是提醒大家注意中间件的顺序!
接着就给server.js增加一条路由:
router.post('/postData', async (ctx, next) =>
{
// 获取数据
let ID = ctx.request.body.ID;
let Name = ctx.request.body.Name;
// 显示
console.log(ID);
console.log(Name);
// 发送数据
ctx.response.body = JSON.stringify("我已收到消息");
});
在client.html文件中,我们在script代码部分加入Post请求的事件处理函数:
function postSubmit()
{
$.ajax(
{
type: "post",
url: "http://127.0.0.1:8080/postData",
data:
{
ID: document.getElementById("ID").value,
Name: document.getElementById("Name").value
},
dataType: "json",
success: function(data)
{
console.log(data);
},
error: function(XMLHttpRequest, textStatus, errorThrown)
{
console.log(textStatus);
}
});
}
启动服务器,再在客户端页面中输入ID和姓名,点击“以Post方式”提交,可以看到服务器获取到前端以Post方式发送的数据,客户端也收到了服务器响应的信息。
笔者在实际的部署的过程中,经常会遇到以下两个问题:
1.客户端提交后出现 404 not found error。
解决:这类问题大多是由服务器没启动或者客户端请求的URL路径不正确或不存在引起的,但笔者遇到更多的情况,是服务器端处理请求(无论是Get还是Post)后没有返回数据,所以记得服务器在处理请求结束之前要返回消息。
2.客户端提交后报错:parsererror 或 Unexpected token xxx。
解决:这是由于服务器发送的格式与客户端请求中规定的数据格式不一致,导致了转换错误。通常情况下服务器与客户端间收发的数据都是json格式,所以在服务器返回的消息中,一定要使用ctx.response.body = JSON.stringify(发送数据),以转为json格式。而笔者遇到Unexpected token xxx 更多的原因则是客户端规定了数据格式为jsonp,而服务器返回json格式,之所以使用jsonp也是因为要实现跨域,而使用了本文中的跨域方法,就不再需要使用jsonp格式了。
至此,以上的讲解和代码你都能理解和运行,那么就已经成功地掌握了基于Node.js和Koa框架搭建服务器。
连接MySQL与异步编程
我们搭建好了服务器,能自由自在地与客户端之间收发数据,但这离一个基础的服务器还差一点。服务器的一个基本功能就是能够读取数据库,所以在这里以MySQL为例,讲解一下Koa2如何操作MySQL数据库。
首先是安装需要的模块,在命令行输入:
npm install mysql --save
安装成功后,在server.js的引入模块部分加入:
const mysql = require('mysql');
const connection = mysql.createConnection
({
host : 'localhost',
user : 'root',
password : '*******', // 你的MySQL数据库root密码
database : '*******' // 要连接的数据库名
});
connection.connect(); // 打开连接
我们先对client文件进行修改,在其表单部分加入:
<br>
<br>
<button onclick="search()">查询</button>
<br>
结果:<textarea id="selectData" style="height:150px;width:300px;"></textarea>
在其script代码部分加入:
function search()
{
$.ajax(
{
type: "post",
url: "http://127.0.0.1:8080/selectData",
dataType: "json",
success: function(data)
{
document.getElementById("selectData").innerHTML = JSON.stringify(data);
},
error: function(XMLHttpRequest, textStatus, errorThrown)
{
console.log(textStatus);
}
});
}
再在server.js中增加一条路由:
router.post('/selectData', async (ctx, next) =>
{
// sql语句
let sqlStr = `select * from user;`
// 查询
connection.query(sqlStr, function (error, results, fields)
{
if (error)
{
console.log(error);
}
// 发送结果
console.log(results);
ctx.response.body = JSON.stringify(results);
});
});
注意,在这里sql语句使用了撇引号``,而非单引号或双引号,是因为它可以进行字符串格式化操作,比如:
let ID = ctx.request.body.ID;
let sqlStr = `select * from bodydata where ID = ${ID}`;
回归正题,我们整个业务逻辑很清晰,客户端点击“查询”按钮,触发search()事件,请求获取user表中的数据,服务端查询后打印一下结果并将结果返回,客户端收到后显示在文本框selectedData中,看起来没有一点问题,但实际执行的过程中,客户端报错:
在之前讨论过,在路径正确的情况下,这一错误产生的原因是服务端没有返回数据,于是我们查看服务器在终端上的运行情况:
看到这里不禁会露出问号脸,服务器已经成功地查询到了结果,并且输出在了控制台,而紧接着的代码就是发送回这一数据,为何客户端偏偏就没收到呢?
笔者在此困扰了许久。查明原因不难,在学习Node.js时我们就了解到它异步的特性,例如我们定义3个异步函数func1()、func2()、func3(),然后执行以下代码:
func1();
func2();
func3();
Node.js执行的过程并不是顺序式的func1()、func2()、func3(),而是异步执行,也就是说执行func1()时,程序并不会就此等待它返回,而是立马接着执行它之后的代码,func2()、func3()也是如此。异步的特性使得Web进行请求和响应时有很高的并发度,响应时间也因此大大缩短。而我们服务端用于处理请求的正是异步函数,因此服务器并不会等待数据库查询函数执行完成,而是直接执行后面的代码,而后面则是处理函数体结束,服务端自然结束处理请求,什么也没返回给客户端,客户端报错,而在这之后query()执行完成,打印信息,返回结果,但此时连接已经断开。
解决这一问题就需要用到Promise对象, Promise对象使得异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。
我们在server.js的引入模块部分的打开连接后加入以下语句:
query= function(sql, parmas=null)
{
return new Promise(function(reject,resolve)
{
//执行sql语句
connection.query(sql,parmas, function (error, results, fields)
{
if (error)
{
throw error;
}
reject(results); // 注意是reject
});
// 提交
connection.commit();
})
}
在此定义了query()函数,它返回一个Promise对象。这里没有在query()函数中前后各打开、关闭连接,而是一直保持连接处于打开状态,因为每次开关的代价比较大,会减慢响应速度。
然后修改刚才的路由为如下:
router.post('/selectData', async (ctx, next) =>
{
// sql语句
let sqlStr = `select * from user;`
// 查询
let res = await query(sqlStr);
ctx.response.body = JSON.stringify(res);
});
一定要注意await的使用,await使得一个异步函数停下来等待另一个异步函数的执行结束并返回,这样就能等待query()的执行,取到数据后返回,客户端成功收到数据,运行结果:
至此,我们成功地使用Koa2对Mysql数据库进行操作,我们基于Node.js和Koa框架搭建一个基础的服务器,也可以说是大功告成了。但笔者对JavaScript中的Promise对象和async/await的使用还只是一个浅显的理解,异步编程的思想更需要深入学习。
参考
- 菜鸟教程,[Node.js 安装配置]:https://www.runoob.com/nodejs/nodejs-install-setup.html;
- Bootstrap前端框架,[koa]:https://koa.bootcss.com/#introduction;
- 廖雪峰的官方网站,[JavaScript教程/Node.js/Web开发/koa]:https://www.liaoxuefeng.com/wiki/1022910821149312/1023025933764960;
- CSDN软件开发网,[koa2如何允许跨域]:https://blog.csdn.net/wyw223/article/details/86745599;
- 博客园,[$.ajax()方法详解]:https://www.cnblogs.com/tylerdonet/p/3520862.html;
- 简书,[Koa2框架学习笔记]:https://www.jianshu.com/p/892d2098a75e;
- 简书,[nodejs查询数据库后将值返回前端]:https://www.jianshu.com/p/6e77d6fdaf13;
- 菜鸟教程,[JavaScript Promise 对象]:https://www.runoob.com/w3cnote/javascript-promise-object.html;
- CSDN软件开发网,[KOA2操作mysql数据库]:https://blog.csdn.net/weixin_34378969/article/details/91436028。