前言
這個(gè)系列的文章已經(jīng)拖了好久离例。我一直想著我應(yīng)該寫點(diǎn)什么比較好咏雌。想著想著就覺得算了凡怎,明天吧,可能明天就有新的思路赊抖。我應(yīng)該寫上手一些框架的步驟统倒?這可能比較簡單,剛?cè)腴T上手框架也確實(shí)容易對(duì)這門語言產(chǎn)生自己的印象氛雪。但是現(xiàn)在網(wǎng)站上這種教程沒有嗎房匆?我想,只要瀏覽一下cnode报亩,你能很快找到各種各樣的教程浴鸿。
我想分享的是一種體會(huì),一種從無快速上手的體會(huì)弦追,一種先入為主導(dǎo)致種種問題而產(chǎn)生的體會(huì)岳链,這才是我寫這些東西的初心【⒓可能他不會(huì)很容易懂掸哑,需要你稍微做過一點(diǎn)你才能知道我走過的這些坑是真實(shí)存在的约急。
稍微總結(jié)一下之前寫的文章內(nèi)容。第一篇苗分,我寫的是什么是異步厌蔽,以及在代碼層面上怎么實(shí)現(xiàn)異步。我也說了摔癣,是回調(diào)函數(shù)實(shí)現(xiàn)的異步奴饮。第二篇,我簡單說了一下node的異步處理邏輯择浊,使用的是事件循環(huán)機(jī)制戴卜。此外因?yàn)榛卣{(diào)函數(shù)會(huì)產(chǎn)生多層回調(diào),所以為了去除他近她,說了如何用promise將回調(diào)函數(shù)包裝成async叉瘩。第三篇,我介紹了node中的包管理工具npm的用法以及簡單的使用了一下koa來生成一個(gè)網(wǎng)頁服務(wù)應(yīng)用粘捎。
第四篇說什么呢薇缅,咱們繼續(xù)來說說網(wǎng)頁服務(wù)應(yīng)用,因?yàn)檫@是node后端程序員接觸的最多的一部分攒磨。
如果你寫的是一個(gè)javaweb應(yīng)用泳桦,最原始的就是使用servlet。首先你會(huì)寫一個(gè)servlet繼承httpServlet娩缰,然后在類的下面編寫doGet方法doPost方法等灸撰。寫完了這些你需要在web.xml中編寫urlPartern來將url對(duì)應(yīng)到你的servlet類中。這些弄完了你會(huì)將他打包拼坎,放在apache目錄下浮毯,開啟apache服務(wù)。這里面泰鸡,servlet就是mvc中的controller债蓝,web.xml實(shí)現(xiàn)的就是一個(gè)路由轉(zhuǎn)發(fā)的功能。
我們?cè)賮砜纯磏ode原生怎么實(shí)現(xiàn)一個(gè)網(wǎng)頁服務(wù)盛龄。
1饰迹、在一個(gè)目錄下新建一個(gè)文件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 啟動(dòng)應(yīng)用余舶。
這樣瀏覽器訪問127.0.0.1:8888就可以看到hello world啊鸭。就這樣簡單的4行代碼(不算最后一行),就可以實(shí)現(xiàn)很高的qps了(每秒8000次左右)匿值,node默認(rèn)是單線程工作赠制,如果開啟多線程,那么就可達(dá)到一萬多的每秒請(qǐng)求千扔。作為參考憎妙,apache的qps大概在5000次库正,go和node差不多曲楚,Nginx可達(dá)幾萬厘唾。
要知道,網(wǎng)絡(luò)io是io龙誊,硬盤查詢也是io抚垃。對(duì)于網(wǎng)絡(luò)io,大家都是采用輪詢的方式掃描端口趟大,在這一處的io影響是不大的鹤树。我個(gè)人認(rèn)為,系統(tǒng)內(nèi)部的硬盤io才是node對(duì)于io處理的優(yōu)勢之處逊朽。舉個(gè)例子罕伯,同樣發(fā)送8000個(gè)請(qǐng)求,在沒有涉及硬盤存儲(chǔ)叽讳,直接從內(nèi)存獲取數(shù)據(jù)返回的時(shí)候追他,大家比較的就只是網(wǎng)絡(luò)的io。但如果這個(gè)時(shí)候請(qǐng)求涉及到數(shù)據(jù)的存儲(chǔ)岛蚤,這時(shí)候apache這種傳統(tǒng)同步服務(wù)器在單個(gè)請(qǐng)求中會(huì)阻塞到其他的請(qǐng)求邑狸,而如果是node的話就能進(jìn)行異步訪問從而達(dá)到并行處理的效果。
關(guān)于這一點(diǎn)我在第一篇中說過
使用node的話是非阻塞IO涤妒,調(diào)用了IO操作之后不要求數(shù)據(jù)直接就能返回单雾,cpu直接就開始處理下一個(gè)操作,等到了IO操作結(jié)束之后她紫,IO操作會(huì)去通知cpu執(zhí)行接下來的操作硅堆。這就使計(jì)算機(jī)的IO處理速度大大提升。
也就是說贿讹,如果增加了數(shù)據(jù)的存儲(chǔ)操作渐逃,可能node就是會(huì)變慢一點(diǎn)(7000)次左右,而apache會(huì)迅速降到(1000)次左右围详。
我們來看一眼這幾行代碼朴乖,其中最主要的就是這一句。
http.createServer((req,res) => {res.end('hello world'); })
其中(req,res) => {res.end('hello world'); }就是以下的縮寫
function func1(request,response) {
res.end('hello world');
}
也就是說助赞,將寫一個(gè)帶有兩個(gè)參數(shù)的函數(shù)放入http.createServer()中就能生成一個(gè)服務(wù)器對(duì)象买羞。
http
為了不讓大家太迷糊,我盡量簡單講一下http模塊都做了什么雹食。
你將函數(shù)放入http.createServer()中之后畜普,http會(huì)給你生成一個(gè)服務(wù)器對(duì)象,一直監(jiān)聽著8888這個(gè)端口群叶,當(dāng)他發(fā)現(xiàn)端口有連接事件(connect)的時(shí)候吃挑,他按兵不動(dòng)(不會(huì)觸發(fā)你的那個(gè)函數(shù))钝荡。只有當(dāng)端口收到了一個(gè)有效請(qǐng)求的時(shí)候,這時(shí)候http會(huì)生成一個(gè)request對(duì)象和一個(gè)response對(duì)象舶衬,將這兩個(gè)對(duì)象放入你的函數(shù)之中埠通。你的函數(shù)處理完之后就會(huì)返回給原請(qǐng)求的地址。
有了這個(gè)逛犹,我們就可以在request對(duì)象中獲取我們需要的信息端辱,如get請(qǐng)求中地址欄攜帶的信息、post中body存放的信息虽画、請(qǐng)求地址等等舞蔽。有了這些你就可以實(shí)現(xiàn)一個(gè)網(wǎng)絡(luò)應(yīng)用了。
但是码撰,此時(shí)你的網(wǎng)絡(luò)應(yīng)用寫起來會(huì)零零散散渗柿,看起來像這樣。
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 請(qǐng)求
if(path === '/index1' && method === 'GET') {
query = qs.parse(query);
res.end(`處理來自${method} ${path},數(shù)據(jù)為 ${JSON.stringify(query)} 的請(qǐng)求`);
return ;
}
// POST /index2
if(path === '/index2' && method === 'POST') {
let data = '';
req.on('data', (chunk) => {
data += chunk;
});
req.on('end', () => {
res.end(`處理來自${method} ${path},數(shù)據(jù)為 ${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');
理論上脖岛,所有的請(qǐng)求都會(huì)經(jīng)過咱們編寫的“這一層函數(shù)”朵栖。但,難道我們要在這里依次寫無數(shù)個(gè)ifelse來判斷request的各個(gè)參數(shù)鸡岗、路徑混槐,來決定response的各個(gè)響應(yīng)消息嗎?
不可能吧轩性,一個(gè)應(yīng)用中声登,你會(huì)有日志功能,配置功能揣苏,定時(shí)功能悯嗓,路由管理,mvc這些需求卸察。你全寫在一個(gè)文件中脯厨,那真的就是面向過程編程了,還不如直接用c語言去寫坑质。
之前我們說過合武,node中異步函數(shù)的調(diào)用雖然使用了async和await,但他內(nèi)部還是使用回調(diào)函數(shù)涡扼,每當(dāng)有一個(gè)事件出現(xiàn)稼跳,消息一定是隨著函數(shù)作為參數(shù)層層往下,再層層往上吃沪。這是node中一個(gè)特性汤善,我們能不能根據(jù)這個(gè)做點(diǎn)什么呢?
答案已經(jīng)呼之欲出了,利用回調(diào)函數(shù)會(huì)層層往下又層層往上的特點(diǎn)红淡,我們何不讓將邏輯布置成一層一層的不狮,讓請(qǐng)求每走一層就處理一部分邏輯?
koa中就是這樣在旱,我們稱之為洋蔥模型摇零。每一層就是一個(gè)中間件。
中間件
洋蔥模型是一個(gè)很不錯(cuò)的組織方式颈渊,他天然就實(shí)現(xiàn)了面向切片編程遂黍。你可以寫一個(gè)中間件终佛,讓某一部分請(qǐng)求通過俊嗽,這樣就不用在每一個(gè)請(qǐng)求中都調(diào)用一次。
當(dāng)然铃彰,我不是說洋蔥模型就是完美的绍豁,有很多地方依舊用起來會(huì)比較別扭,在某些特定的地方你依然會(huì)像以前一樣封裝成工具類這樣調(diào)用牙捉。但由于回調(diào)函數(shù)對(duì)中間件的天然支持竹揍,你能感覺到這種形式的編程還是能給你帶來很多不錯(cuò)的體驗(yàn)。
只是說的話會(huì)有點(diǎn)抽象邪铲,讓我們運(yùn)行一段這樣的代碼芬位。
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/');
訪問瀏覽器會(huì)看到輸出這樣的一段日志。這說明一個(gè)請(qǐng)求進(jìn)來會(huì)進(jìn)入層層的中間件带到,再層層的離開昧碉。
----------middleWare1 start------------
----------middleWare2 start------------
----------middleWare3 start------------
----------middleWare3 end------------
----------middleWare2 end------------
----------middleWare1 end------------
解釋一下,每個(gè)中間件都是一個(gè)函數(shù)揽惹,規(guī)定傳入的參數(shù)是contxt(上下文)和next(回調(diào)函數(shù)被饿,調(diào)用他就可以執(zhí)行下一個(gè)中間件);
應(yīng)用中要添加中間件搪搏,就要通過app.use(中間件方法)傳入狭握,應(yīng)用會(huì)自動(dòng)按照其傳入的順序執(zhí)行。
可以嘗試注釋掉其中的某一個(gè)await next()疯溺,看看結(jié)果是怎么樣的论颅。
在koa中,中間件大多都是單獨(dú)的模塊囱嫩。我們只需要添加到入口文件app.js中恃疯,讓app.use(middleware)添加到應(yīng)用中即可運(yùn)用。
比如最簡單的koa-router管理路由的挠说,我們可以看看他是如何管理我們上面原始的粗糙的http請(qǐng)求分發(fā)澡谭。 這個(gè)例子主要用到3個(gè)文件
// app.js 主要用于將中間件添加到應(yīng)用中
const Koa = require('koa');
const app = new Koa();
const router = require('./router');
var bodyParser = require('koa-bodyparser');
app.use(bodyParser());//加入這個(gè) 才可以解析post請(qǐng)求中參數(shù)
app
.use(router.routes()) //將路由添加到應(yīng)用
.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對(duì)應(yīng)
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},數(shù)據(jù)為 ${JSON.stringify(query)} 的請(qǐng)求`;
}
async function index2(ctx, next) {
const {method,path,body} = ctx.request;
ctx.body = `處理來自${method} ${path},數(shù)據(jù)為 ${JSON.stringify(body)} 的請(qǐng)求`;
}
module.exports = {
index1,
index2,
}
有了這樣的一個(gè)框架,我們就可以方便的對(duì)代碼進(jìn)行模塊化管理、分層管理蛙奖。
代碼已上傳到Zeeephr/koa-demo 潘酗,如果有需要可以看一看。
后記
這個(gè)系列后面可能就是一起看源碼了雁仲,但是不要慌仔夺,node中看源碼的體驗(yàn)非常好。node編程中有的時(shí)候甚至不用去查api或者百度哪里報(bào)錯(cuò)攒砖,直接在node_modules文件夾中點(diǎn)開就能看缸兔,最夸張的就是他還可以在引入的包中打斷點(diǎn),這樣你就能清晰地知道你的數(shù)據(jù)是如何走向的吹艇。
好了這一part就先講到這吧惰蜜,覺得有用的話可以點(diǎn)點(diǎn)贊,留下你的評(píng)論受神!