前言
這個系列的文章已經拖了好久。我一直想着我應該寫點什麼比較好。想着想着就覺得算了,明天吧,可能明天就有新的思路。我應該寫上手一些框架的步驟?這可能比較簡單,剛入門上手框架也確實容易對這門語言產生自己的印象。但是現在網站上這種教程沒有嗎?我想,只要瀏覽一下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就先講到這吧,覺得有用的話可以點點贊,留下你的評論!