前言
这个系列的文章已经拖了好久。我一直想着我应该写点什么比较好。想着想着就觉得算了,明天吧,可能明天就有新的思路。我应该写上手一些框架的步骤?这可能比较简单,刚入门上手框架也确实容易对这门语言产生自己的印象。但是现在网站上这种教程没有吗?我想,只要浏览一下cnode,你能很快找到各种各样的教程。
我想分享的是一种体会,一种从无快速上手的体会,一种先入为主导致种种问题而产生的体会,这才是我写这些东西的初心。可能他不会很容易懂,需要你稍微做过一点你才能知道我走过的这些坑是真实存在的。
稍微总结一下之前写的文章内容。第一篇,我写的是什么是异步,以及在代码层面上怎么实现异步。我也说了,是回调函数实现的异步。第二篇,我简单说了一下node的异步处理逻辑,使用的是事件循环机制。此外因为回调函数会产生多层回调,所以为了去除他,说了如何用promise将回调函数包装成async。第三篇,我介绍了node中的包管理工具npm的用法以及简单的使用了一下koa来生成一个网页服务应用。
第四篇说什么呢,咱们继续来说说网页服务应用,因为这是node后端程序员接触的最多的一部分。
如果你写的是一个javaweb应用,最原始的就是使用servlet。首先你会写一个servlet继承httpServlet,然后在类的下面编写doGet方法doPost方法等。写完了这些你需要在web.xml中编写urlPartern来将url对应到你的servlet类中。这些弄完了你会将他打包,放在apache目录下,开启apache服务。这里面,servlet就是mvc中的controller,web.xml实现的就是一个路由转发的功能。
我们再来看看node原生怎么实现一个网页服务。
1、在一个目录下新建一个文件app.js,输入以下代码
const http = require('http');
http.createServer((req,res) => {
res.end('hello world');
}).listen(8888);
console.log('server is running on 127.0.0.1:8888');
2、命令行中node app.js 启动应用。
这样浏览器访问127.0.0.1:8888就可以看到hello world。就这样简单的4行代码(不算最后一行),就可以实现很高的qps了(每秒8000次左右),node默认是单线程工作,如果开启多线程,那么就可达到一万多的每秒请求。作为参考,apache的qps大概在5000次,go和node差不多,Nginx可达几万。
要知道,网络io是io,硬盘查询也是io。对于网络io,大家都是采用轮询的方式扫描端口,在这一处的io影响是不大的。我个人认为,系统内部的硬盘io才是node对于io处理的优势之处。举个例子,同样发送8000个请求,在没有涉及硬盘存储,直接从内存获取数据返回的时候,大家比较的就只是网络的io。但如果这个时候请求涉及到数据的存储,这时候apache这种传统同步服务器在单个请求中会阻塞到其他的请求,而如果是node的话就能进行异步访问从而达到并行处理的效果。
关于这一点我在第一篇中说过
使用node的话是非阻塞IO,调用了IO操作之后不要求数据直接就能返回,cpu直接就开始处理下一个操作,等到了IO操作结束之后,IO操作会去通知cpu执行接下来的操作。这就使计算机的IO处理速度大大提升。
也就是说,如果增加了数据的存储操作,可能node就是会变慢一点(7000)次左右,而apache会迅速降到(1000)次左右。
我们来看一眼这几行代码,其中最主要的就是这一句。
http.createServer((req,res) => {res.end('hello world'); })
其中(req,res) => {res.end('hello world'); }就是以下的缩写
function func1(request,response) {
res.end('hello world');
}
也就是说,将写一个带有两个参数的函数放入http.createServer()中就能生成一个服务器对象。
http
为了不让大家太迷糊,我尽量简单讲一下http模块都做了什么。
你将函数放入http.createServer()中之后,http会给你生成一个服务器对象,一直监听着8888这个端口,当他发现端口有连接事件(connect)的时候,他按兵不动(不会触发你的那个函数)。只有当端口收到了一个有效请求的时候,这时候http会生成一个request对象和一个response对象,将这两个对象放入你的函数之中。你的函数处理完之后就会返回给原请求的地址。
有了这个,我们就可以在request对象中获取我们需要的信息,如get请求中地址栏携带的信息、post中body存放的信息、请求地址等等。有了这些你就可以实现一个网络应用了。
但是,此时你的网络应用写起来会零零散散,看起来像这样。
const http = require('http');
const url = require('url');
const qs = require('querystring');
http.createServer((req,res) => {
// console.log(req.url, req.method);
const method = req.method;
let { pathname: path, query } = url.parse(req.url);
// GET /index1 请求
if(path === '/index1' && method === 'GET') {
query = qs.parse(query);
res.end(`处理来自${method} ${path},数据为 ${JSON.stringify(query)} 的请求`);
return ;
}
// POST /index2
if(path === '/index2' && method === 'POST') {
let data = '';
req.on('data', (chunk) => {
data += chunk;
});
req.on('end', () => {
res.end(`处理来自${method} ${path},数据为 ${JSON.stringify(qs.parse(data))}`)
})
return;
}
res.statusCode = 404;
res.end('404 NOT FOUND')
}).listen(8888);
console.log('server is running on 127.0.0.1:8888');
理论上,所有的请求都会经过咱们编写的“这一层函数”。但,难道我们要在这里依次写无数个ifelse来判断request的各个参数、路径,来决定response的各个响应消息吗?
不可能吧,一个应用中,你会有日志功能,配置功能,定时功能,路由管理,mvc这些需求。你全写在一个文件中,那真的就是面向过程编程了,还不如直接用c语言去写。
之前我们说过,node中异步函数的调用虽然使用了async和await,但他内部还是使用回调函数,每当有一个事件出现,消息一定是随着函数作为参数层层往下,再层层往上。这是node中一个特性,我们能不能根据这个做点什么呢?
答案已经呼之欲出了,利用回调函数会层层往下又层层往上的特点,我们何不让将逻辑布置成一层一层的,让请求每走一层就处理一部分逻辑?
koa中就是这样,我们称之为洋葱模型。每一层就是一个中间件。
中间件
洋葱模型是一个很不错的组织方式,他天然就实现了面向切片编程。你可以写一个中间件,让某一部分请求通过,这样就不用在每一个请求中都调用一次。
当然,我不是说洋葱模型就是完美的,有很多地方依旧用起来会比较别扭,在某些特定的地方你依然会像以前一样封装成工具类这样调用。但由于回调函数对中间件的天然支持,你能感觉到这种形式的编程还是能给你带来很多不错的体验。
只是说的话会有点抽象,让我们运行一段这样的代码。
const Koa = require('koa');
const app = new Koa();
async function middleWare1(ctx,next){
console.log('----------middleWare1 start------------');
await next();
console.log('----------middleWare1 end------------');
}
async function middleWare2(ctx,next){
console.log('----------middleWare2 start------------');
await next();
console.log('----------middleWare2 end------------');
}
async function middleWare3(ctx,next){
console.log('----------middleWare3 start------------');
ctx.body = 'hello world';
console.log('----------middleWare3 end------------');
}
app.use(middleWare1);
app.use(middleWare2);
app.use(middleWare3);
app.listen(8888);
console.log('Server running at http://127.0.0.1:8888/');
访问浏览器会看到输出这样的一段日志。这说明一个请求进来会进入层层的中间件,再层层的离开。
----------middleWare1 start------------
----------middleWare2 start------------
----------middleWare3 start------------
----------middleWare3 end------------
----------middleWare2 end------------
----------middleWare1 end------------
解释一下,每个中间件都是一个函数,规定传入的参数是contxt(上下文)和next(回调函数,调用他就可以执行下一个中间件);
应用中要添加中间件,就要通过app.use(中间件方法)传入,应用会自动按照其传入的顺序执行。
可以尝试注释掉其中的某一个await next(),看看结果是怎么样的。
在koa中,中间件大多都是单独的模块。我们只需要添加到入口文件app.js中,让app.use(middleware)添加到应用中即可运用。
比如最简单的koa-router管理路由的,我们可以看看他是如何管理我们上面原始的粗糙的http请求分发。 这个例子主要用到3个文件
// app.js 主要用于将中间件添加到应用中
const Koa = require('koa');
const app = new Koa();
const router = require('./router');
var bodyParser = require('koa-bodyparser');
app.use(bodyParser());//加入这个 才可以解析post请求中参数
app
.use(router.routes()) //将路由添加到应用
.use(router.allowedMethods());
app.listen(8888);
console.log('server is running on 127.0.0.1:8888');
// router.js 管理路由
const Router = require('koa-router');
const index = require('./controller/index')
const router = new Router();
router.get('/index1',index.index1); //路由一般与controller对应
router.post('/index2',index.index2);
module.exports =router;
//index.js 具体处理逻辑的地方 controller层
async function index1(ctx, next) {
const {method,path,query} = ctx.request;
ctx.body = `处理来自${method} ${path},数据为 ${JSON.stringify(query)} 的请求`;
}
async function index2(ctx, next) {
const {method,path,body} = ctx.request;
ctx.body = `处理来自${method} ${path},数据为 ${JSON.stringify(body)} 的请求`;
}
module.exports = {
index1,
index2,
}
有了这样的一个框架,我们就可以方便的对代码进行模块化管理、分层管理。
代码已上传到Zeeephr/koa-demo ,如果有需要可以看一看。
后记
这个系列后面可能就是一起看源码了,但是不要慌,node中看源码的体验非常好。node编程中有的时候甚至不用去查api或者百度哪里报错,直接在node_modules文件夹中点开就能看,最夸张的就是他还可以在引入的包中打断点,这样你就能清晰地知道你的数据是如何走向的。
好了这一part就先讲到这吧,觉得有用的话可以点点赞,留下你的评论!