Node框架學(xué)習(2)--Koa

一符喝、基本用法

1.1 架設(shè) HTTP 服務(wù)


// demos/01.js
const Koa = require('koa');
const app = new Koa();

app.listen(3000);

$ node demos/01.js

打開瀏覽器,訪問 http://127.0.0.1:3000 缴川。你會看到頁面顯示"Not Found"二跋,表示沒有發(fā)現(xiàn)任何內(nèi)容扎即。這是因為我們并沒有告訴 Koa 應(yīng)該顯示什么內(nèi)容谚鄙。

1.2 Context 對象

Koa 提供一個 Context 對象闷营,表示一次對話的上下文(包括 HTTP 請求和 HTTP 回復(fù))。通過加工這個對象嫂丙,就可以控制返回給用戶的內(nèi)容跟啤。

Context.response.body屬性就是發(fā)送給用戶的內(nèi)容。


// demos/02.js
const Koa = require('koa');
const app = new Koa();

const main = ctx => {
  ctx.response.body = 'Hello World';
};

app.use(main);
app.listen(3000);

上面代碼中袄简,main函數(shù)用來設(shè)置ctx.response.body绿语。然后汞舱,使用app.use方法加載main函數(shù)莹规。

ctx.response代表 HTTP Response泌神。同樣地欢际,ctx.request代表 HTTP Request损趋。


$ node demos/02.js

訪問 http://127.0.0.1:3000 浑槽,現(xiàn)在就可以看到"Hello World"了桐玻。

1.3 HTTP Response 的類型

Koa 默認的返回類型是text/plain镊靴,如果想返回其他類型的內(nèi)容,可以先用ctx.request.accepts判斷一下敞峭,客戶端希望接受什么數(shù)據(jù)(根據(jù) HTTP Request 的Accept字段)褪子,然后使用ctx.response.type指定返回類型嫌褪。


// demos/03.js
const main = ctx => {
  if (ctx.request.accepts('xml')) {
    ctx.response.type = 'xml';
    ctx.response.body = '<data>Hello World</data>';
  } else if (ctx.request.accepts('json')) {
    ctx.response.type = 'json';
    ctx.response.body = { data: 'Hello World' };
  } else if (ctx.request.accepts('html')) {
    ctx.response.type = 'html';
    ctx.response.body = '<p>Hello World</p>';
  } else {
    ctx.response.type = 'text';
    ctx.response.body = 'Hello World';
  }
};

$ node demos/03.js

訪問 http://127.0.0.1:3000 笼痛,現(xiàn)在看到的就是一個 XML 文檔了缨伊。

[圖片上傳失敗...(image-14a658-1512521964504)]

1.4 網(wǎng)頁模板

實際開發(fā)中刻坊,返回給用戶的網(wǎng)頁往往都寫成模板文件谭胚。我們可以讓 Koa 先讀取模板文件灾而,然后將這個模板返回給用戶旁趟。


// demos/04.js
const fs = require('fs');

const main = ctx => {
  ctx.response.type = 'html';
  ctx.response.body = fs.createReadStream('./demos/template.html');
};

$ node demos/04.js

訪問 http://127.0.0.1:3000 锡搜,看到的就是模板文件的內(nèi)容了余爆。

二蛾方、路由

2.1 原生路由

網(wǎng)站一般都有多個頁面。通過ctx.request.path可以獲取用戶請求的路徑拓春,由此實現(xiàn)簡單的路由硼莽。


// demos/05.js
const main = ctx => {
  if (ctx.request.path !== '/') {
    ctx.response.type = 'html';
    ctx.response.body = '<a href="/">Index Page</a>';
  } else {
    ctx.response.body = 'Hello World';
  }
};

$ node demos/05.js

訪問 http://127.0.0.1:3000/about 懂鸵,可以看到一個鏈接匆光,點擊后就跳到首頁终息。

2.2 koa-route 模塊

原生路由用起來不太方便周崭,我們可以使用封裝好的koa-route模塊。


// demos/06.js
const route = require('koa-route');

const about = ctx => {
  ctx.response.type = 'html';
  ctx.response.body = '<a href="/">Index Page</a>';
};

const main = ctx => {
  ctx.response.body = 'Hello World';
};

app.use(route.get('/', main));
app.use(route.get('/about', about));

上面代碼中征绎,根路徑/的處理函數(shù)是main人柿,/about路徑的處理函數(shù)是about


$ node demos/06.js

訪問 http://127.0.0.1:3000/about

2.3 靜態(tài)資源

如果網(wǎng)站提供靜態(tài)資源(圖片逢净、字體爹土、樣式表社露、腳本......)琼娘,為它們一個個寫路由就很麻煩,也沒必要坷备。koa-static模塊封裝了這部分的請求。


// demos/12.js
const path = require('path');
const serve = require('koa-static');

const main = serve(path.join(__dirname));
app.use(main);

$ node demos/12.js

訪問 http://127.0.0.1:3000/12.js竟秫,在瀏覽器里就可以看到這個腳本的內(nèi)容朝巫。

2.4 重定向

有些場合劈猿,服務(wù)器需要重定向(redirect)訪問請求揪荣。比如往史,用戶登陸以后挨决,將他重定向到登陸前的頁面订歪。ctx.response.redirect()方法可以發(fā)出一個302跳轉(zhuǎn)盖高,將用戶導(dǎo)向另一個路由眼虱。


// demos/13.js
const redirect = ctx => {
  ctx.response.redirect('/');
  ctx.response.body = '<a href="/">Index Page</a>';
};

app.use(route.get('/redirect', redirect));

$ node demos/13.js

訪問 http://127.0.0.1:3000/redirect ,瀏覽器會將用戶導(dǎo)向根路由过牙。

三、中間件

3.1 Logger 功能

Koa 的最大特色矫渔,也是最重要的一個設(shè)計庙洼,就是中間件(middleware)蚁袭。為了理解中間件石咬,我們先看一下 Logger (打印日志)功能的實現(xiàn)删性。

最簡單的寫法就是在main函數(shù)里面增加一行焕窝。


// demos/07.js
const main = ctx => {
  console.log(`${Date.now()} ${ctx.request.method} ${ctx.request.url}`);
  ctx.response.body = 'Hello World';
};

$ node demos/07.js

訪問 http://127.0.0.1:3000 巴帮,命令行就會輸出日志虐秋。


1502144902843 GET /

3.2 中間件的概念

上一個例子里面的 Logger 功能,可以拆分成一個獨立函數(shù)起愈。


// demos/08.js
const logger = (ctx, next) => {
  console.log(`${Date.now()} ${ctx.request.method} ${ctx.request.url}`);
  next();
}
app.use(logger);

像上面代碼中的logger函數(shù)就叫做"中間件"(middleware)抬虽,因為它處在 HTTP Request 和 HTTP Response 中間休涤,用來實現(xiàn)某種中間功能。app.use()用來加載中間件。

基本上捷凄,Koa 所有的功能都是通過中間件實現(xiàn)的跺涤,前面例子里面的main也是中間件桶错。每個中間件默認接受兩個參數(shù)院刁,第一個參數(shù)是 Context 對象退腥,第二個參數(shù)是next函數(shù)演闭。只要調(diào)用next函數(shù),就可以把執(zhí)行權(quán)轉(zhuǎn)交給下一個中間件购城。


$ node demos/08.js

3.3 中間件棧

多個中間件會形成一個棧結(jié)構(gòu)(middle stack)瘪板,以"先進后出"(first-in-last-out)的順序執(zhí)行漆诽。

  1. 最外層的中間件首先執(zhí)行厢拭。
  2. 調(diào)用next函數(shù)供鸠,把執(zhí)行權(quán)交給下一個中間件。
  3. ...
  4. 最內(nèi)層的中間件最后執(zhí)行。
  5. 執(zhí)行結(jié)束后胶坠,把執(zhí)行權(quán)交回上一層的中間件沈善。
  6. ...
  7. 最外層的中間件收回執(zhí)行權(quán)之后矮瘟,執(zhí)行next函數(shù)后面的代碼澈侠。

// demos/09.js
const one = (ctx, next) => {
  console.log('>> one');
  next();
  console.log('<< one');
}

const two = (ctx, next) => {
  console.log('>> two');
  next(); 
  console.log('<< two');
}

const three = (ctx, next) => {
  console.log('>> three');
  next();
  console.log('<< three');
}

app.use(one);
app.use(two);
app.use(three);


$ node demos/09.js

訪問 http://127.0.0.1:3000 哨啃,命令行窗口會有如下輸出拳球。


>> one
>> two
>> three
<< three
<< two
<< one

如果中間件內(nèi)部沒有調(diào)用next函數(shù)祝峻,那么執(zhí)行權(quán)就不會傳遞下去莱找。作為練習奥溺,你可以將two函數(shù)里面next()這一行注釋掉再執(zhí)行浮定,看看會有什么結(jié)果桦卒。

3.4 異步中間件

迄今為止闸盔,所有例子的中間件都是同步的,不包含異步操作针贬。如果有異步操作(比如讀取數(shù)據(jù)庫),中間件就必須寫成 async 函數(shù)谆棱。


// demos/10.js
const fs = require('fs.promised');
const Koa = require('koa');
const app = new Koa();

const main = async function (ctx, next) {
  ctx.response.type = 'html';
  ctx.response.body = await fs.readFile('./demos/template.html', 'utf8');
};

app.use(main);
app.listen(3000);

上面代碼中垃瞧,fs.readFile是一個異步操作个从,必須寫成await fs.readFile()嗦锐,然后中間件必須寫成 async 函數(shù)萎羔。


$ node demos/10.js

訪問 http://127.0.0.1:3000 碳默,就可以看到模板文件的內(nèi)容磅崭。

3.5 中間件的合成

koa-compose模塊可以將多個中間件合成為一個。


// demos/11.js
const compose = require('koa-compose');

const logger = (ctx, next) => {
  console.log(`${Date.now()} ${ctx.request.method} ${ctx.request.url}`);
  next();
}

const main = ctx => {
  ctx.response.body = 'Hello World';
};

const middlewares = compose([logger, main]);
app.use(middlewares);

$ node demos/11.js

訪問 http://127.0.0.1:3000 柔逼,就可以在命令行窗口看到日志信息愉适。

四癣漆、錯誤處理

4.1 500 錯誤

如果代碼運行過程中發(fā)生錯誤,我們需要把錯誤信息返回給用戶。HTTP 協(xié)定約定這時要返回500狀態(tài)碼租副。Koa 提供了ctx.throw()方法结胀,用來拋出錯誤,ctx.throw(500)就是拋出500錯誤院仿。


// demos/14.js
const main = ctx => {
  ctx.throw(500);
};

$ node demos/14.js

訪問 http://127.0.0.1:3000意蛀,你會看到一個500錯誤頁"Internal Server Error"县钥。

4.2 404錯誤

如果將ctx.response.status設(shè)置成404若贮,就相當于ctx.throw(404)谴麦,返回404錯誤匾效。


// demos/15.js
const main = ctx => {
  ctx.response.status = 404;
  ctx.response.body = 'Page Not Found';
};

$ node demos/15.js

訪問 http://127.0.0.1:3000 面哼,你就看到一個404頁面"Page Not Found"匈子。

4.3 處理錯誤的中間件

為了方便處理錯誤闯袒,最好使用try...catch將其捕獲。但是其徙,為每個中間件都寫try...catch太麻煩擂橘,我們可以讓最外層的中間件通贞,負責所有中間件的錯誤處理昌罩。


// demos/16.js
const handler = async (ctx, next) => {
  try {
    await next();
  } catch (err) {
    ctx.response.status = err.statusCode || err.status || 500;
    ctx.response.body = {
      message: err.message
    };
  }
};

const main = ctx => {
  ctx.throw(500);
};

app.use(handler);
app.use(main);

$ node demos/16.js

訪問 http://127.0.0.1:3000 茎用,你會看到一個500頁旭斥,里面有報錯提示 {"message":"Internal Server Error"}垂券。

4.4 error 事件的監(jiān)聽

運行過程中一旦出錯,Koa 會觸發(fā)一個error事件柒昏。監(jiān)聽這個事件职祷,也可以處理錯誤堪旧。


// demos/17.js
const main = ctx => {
  ctx.throw(500);
};

app.on('error', (err, ctx) =>
  console.error('server error', err);
);

$ node demos/17.js

訪問 http://127.0.0.1:3000 淳梦,你會在命令行窗口看到"server error xxx"爆袍。

4.5 釋放 error 事件

需要注意的是陨囊,如果錯誤被try...catch捕獲蜘醋,就不會觸發(fā)error事件压语。這時胎食,必須調(diào)用ctx.app.emit()衩匣,手動釋放error事件琅捏,才能讓監(jiān)聽函數(shù)生效递雀。


// demos/18.js`
const handler = async (ctx, next) => {
  try {
    await next();
  } catch (err) {
    ctx.response.status = err.statusCode || err.status || 500;
    ctx.response.type = 'html';
    ctx.response.body = '<p>Something wrong, please contact administrator.</p>';
    ctx.app.emit('error', err, ctx);
  }
};

const main = ctx => {
  ctx.throw(500);
};

app.on('error', function(err) {
  console.log('logging error ', err.message);
  console.log(err);
});

上面代碼中映之,main函數(shù)拋出錯誤杠输,被handler函數(shù)捕獲蠢甲。catch代碼塊里面使用ctx.app.emit()手動釋放error事件鹦牛,才能讓監(jiān)聽函數(shù)監(jiān)聽到曼追。


$ node demos/18.js

訪問 http://127.0.0.1:3000 礼殊,你會在命令行窗口看到logging error

五啄枕、Web App 的功能

5.1 Cookies

ctx.cookies用來讀寫 Cookie频祝。


// demos/19.js
const main = function(ctx) {
  const n = Number(ctx.cookies.get('view') || 0) + 1;
  ctx.cookies.set('view', n);
  ctx.response.body = n + ' views';
}

$ node demos/19.js

訪問 http://127.0.0.1:3000 ,你會看到1 views未辆。刷新一次頁面咐柜,就變成了2 views拙友。再刷新遗契,每次都會計數(shù)增加1。

5.2 表單

Web 應(yīng)用離不開處理表單鲫竞。本質(zhì)上从绘,表單就是 POST 方法發(fā)送到服務(wù)器的鍵值對僵井。koa-body模塊可以用來從 POST 請求的數(shù)據(jù)體里面提取鍵值對批什。


// demos/20.js
const koaBody = require('koa-body');

const main = async function(ctx) {
  const body = ctx.request.body;
  if (!body.name) ctx.throw(400, '.name required');
  ctx.body = { name: body.name };
};

app.use(koaBody());

$ node demos/20.js

打開另一個命令行窗口渊季,運行下面的命令却汉。


$ curl -X POST --data "name=Jack" 127.0.0.1:3000
{"name":"Jack"}

$ curl -X POST --data "name" 127.0.0.1:3000
name required

上面代碼使用 POST 方法向服務(wù)器發(fā)送一個鍵值對青扔,會被正確解析微猖。如果發(fā)送的數(shù)據(jù)不正確,就會收到錯誤提示轻姿。

5.3 文件上傳

koa-body模塊還可以用來處理文件上傳互亮。


// demos/21.js
const os = require('os');
const path = require('path');
const koaBody = require('koa-body');

const main = async function(ctx) {
  const tmpdir = os.tmpdir();
  const filePaths = [];
  const files = ctx.request.body.files || {};

  for (let key in files) {
    const file = files[key];
    const filePath = path.join(tmpdir, file.name);
    const reader = fs.createReadStream(file.path);
    const writer = fs.createWriteStream(filePath);
    reader.pipe(writer);
    filePaths.push(filePath);
  }

  ctx.body = filePaths;
};

app.use(koaBody({ multipart: true }));

$ node demos/21.js

打開另一個命令行窗口炊昆,運行下面的命令,上傳一個文件洛搀。注意姥卢,/path/to/file要更換為真實的文件路徑独榴。


$ curl --form upload=@/path/to/file http://127.0.0.1:3000
["/tmp/file"]

參考自 http://www.ruanyifeng.com/blog/2017/08/koa.html
示例庫
zip 文件
demos

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末瓶堕,一起剝皮案震驚了整個濱河市谭梗,隨后出現(xiàn)的幾起案子宛蚓,更是在濱河造成了極大的恐慌凄吏,老刑警劉巖痕钢,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件任连,死亡現(xiàn)場離奇詭異随抠,居然都是意外死亡暮刃,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來背犯,“玉大人漠魏,你說我怎么就攤上這事妄均》岚” “怎么了邑彪?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長宙彪。 經(jīng)常有香客問我悲没,道長柑潦,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任览露,我火速辦了婚禮差牛,結(jié)果婚禮上堰乔,老公的妹妹穿的比我還像新娘镐侯。我一直安慰自己苟翻,他們只是感情好崇猫,可當我...
    茶點故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布蜡歹。 她就那樣靜靜地躺著涕烧,像睡著了一般澈魄。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上铛漓,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天,我揣著相機與錄音包晰,去河邊找鬼伐憾。 笑死树肃,一個胖子當著我的面吹牛胸嘴,可吹牛的內(nèi)容都是我干的劣像。 我是一名探鬼主播耳奕,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼时迫,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了掠拳?” 一聲冷哼從身側(cè)響起溺欧,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎聂使,沒想到半個月后柏靶,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體屎蜓,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡炬转,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年驻啤,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖森逮,靈堂內(nèi)的尸體忽然破棺而出褒侧,到底是詐尸還是另有隱情闷供,我是刑警寧澤,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站钞艇,受9級特大地震影響哩照,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一蹋岩、第九天 我趴在偏房一處隱蔽的房頂上張望剪个。 院中可真熱鬧版确,春花似錦、人聲如沸侵歇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽琳猫。三九已至脐嫂,卻和暖如春账千,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背鞭衩。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留会放,地道東北人咧最。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓滥搭,卻偏偏與公主長得像,于是被迫代替她去往敵國和親闽坡。 傳聞我的和親對象是個殘疾皇子疾嗅,可洞房花燭夜當晚...
    茶點故事閱讀 44,781評論 2 354

推薦閱讀更多精彩內(nèi)容