一符喝、基本用法
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í)行漆诽。
- 最外層的中間件首先執(zhí)行厢拭。
- 調(diào)用
next
函數(shù)供鸠,把執(zhí)行權(quán)交給下一個中間件。- ...
- 最內(nèi)層的中間件最后執(zhí)行。
- 執(zhí)行結(jié)束后胶坠,把執(zhí)行權(quán)交回上一層的中間件沈善。
- ...
- 最外層的中間件收回執(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