Node主要用在開發 Web 應用。這決定了使用 Node,每每離不開 Web 應用框架。javascript
Koa 就是一種簡單好用的 Web 框架。它的特色是優雅、簡潔、表達力強、自由度高。自己代碼只有1000多行,全部功能都經過插件實現,很符合 Unix 哲學。css
本文從零開始,按部就班,教會你如何使用 Koa 寫出本身的 Web 應用。每一步都有簡潔易懂的示例,但願讓你們一看就懂。html
首先,檢查 Node 版本。java
$ node -v v8.0.0
Koa 必須使用 7.6 以上的版本。若是你的版本低於這個要求,就要先升級 Node。這裏的全部示例我都運行過,可到http://bijian1013.iteye.com/blog/2425085下載。node
只要三行代碼,就能夠用 Koa 架設一個 HTTP 服務。git
// 01.js const Koa = require('koa'); const app = new Koa(); app.listen(3000);
運行這個腳本。es6
$ node 01.js
打開瀏覽器,訪問 http://127.0.0.1:3000 。你會看到頁面顯示"Not Found",表示沒有發現任何內容。這是由於咱們並無告訴 Koa 應該顯示什麼內容。github
Koa 提供一個 Context 對象,表示一次對話的上下文(包括 HTTP 請求和 HTTP 回覆)。經過加工這個對象,就能夠控制返回給用戶的內容。數據庫
Context.response.body
屬性就是發送給用戶的內容。請看下面的例子。npm
// 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
函數用來設置ctx.response.body
。而後,使用app.use
方法加載main
函數。
你可能已經猜到了,ctx.response
表明 HTTP Response。一樣地,ctx.request
表明 HTTP Request。
運行這個腳本。
$ node 02.js
訪問 http://127.0.0.1:3000 ,如今就能夠看到"Hello World"了。
Koa 默認的返回類型是text/plain
,若是想返回其餘類型的內容,能夠先用ctx.request.accepts
判斷一下,客戶端但願接受什麼數據(根據 HTTP Request 的Accept
字段),而後使用ctx.response.type
指定返回類型。請看下面的例子。
// 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 03.js
訪問 http://127.0.0.1:3000 ,如今看到的就是一個 XML 文檔了。
實際開發中,返回給用戶的網頁每每都寫成模板文件。咱們可讓 Koa 先讀取模板文件,而後將這個模板返回給用戶。請看下面的例子。
// 04.js const fs = require('fs'); const main = ctx => { ctx.response.type = 'html'; ctx.response.body = fs.createReadStream('./template.html'); };
運行這個腳本。
$ node 04.js
訪問 http://127.0.0.1:3000 ,看到的就是模板文件的內容了。
網站通常都有多個頁面。經過ctx.request.path
能夠獲取用戶請求的路徑,由此實現簡單的路由。請看下面的例子。
// 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 05.js
訪問 http://127.0.0.1:3000/about ,能夠看到一個連接,點擊後就跳到首頁。
原生路由用起來不太方便,咱們可使用封裝好的koa-route
模塊。請看下面的例子。
// 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));
上面代碼中,根路徑/
的處理函數是main
,/about
路徑的處理函數是about
。
運行這個腳本。
$ node 06.js
訪問 http://127.0.0.1:3000/about ,效果與上一個例子徹底相同。
若是網站提供靜態資源(圖片、字體、樣式表、腳本......),爲它們一個個寫路由就很麻煩,也不必。koa-static
模塊封裝了這部分的請求。請看下面的例子。
// 12.js const path = require('path'); const serve = require('koa-static'); const main = serve(path.join(__dirname)); app.use(main);
運行這個腳本。
$ node 12.js
訪問 http://127.0.0.1:3000/12.js,在瀏覽器裏就能夠看到這個腳本的內容。
有些場合,服務器須要重定向(redirect)訪問請求。好比,用戶登錄之後,將他重定向到登錄前的頁面。ctx.response.redirect()
方法能夠發出一個302跳轉,將用戶導向另外一個路由。請看下面的例子。
// 13.js const redirect = ctx => { ctx.response.redirect('/'); ctx.response.body = '<a href="/">Index Page</a>'; }; app.use(route.get('/redirect', redirect));
運行這個腳本。
$ node 13.js
訪問 http://127.0.0.1:3000/redirect ,瀏覽器會將用戶導向根路由。
Koa 的最大特點,也是最重要的一個設計,就是中間件(middleware)。爲了理解中間件,咱們先看一下 Logger (打印日誌)功能的實現。
最簡單的寫法就是在main
函數裏面增長一行。
// 07.js const main = ctx => { console.log(`${Date.now()} ${ctx.request.method} ${ctx.request.url}`); ctx.response.body = 'Hello World'; };
運行這個腳本。
$ node 07.js
訪問 http://127.0.0.1:3000 ,命令行就會輸出日誌。
1502144902843 GET /
上一個例子裏面的 Logger 功能,能夠拆分紅一個獨立函數。
// 08.js const logger = (ctx, next) => { console.log(`${Date.now()} ${ctx.request.method} ${ctx.request.url}`); next(); } app.use(logger);
像上面代碼中的logger
函數就叫作"中間件"(middleware),由於它處在 HTTP Request 和 HTTP Response 中間,用來實現某種中間功能。app.use()
用來加載中間件。
基本上,Koa 全部的功能都是經過中間件實現的,前面例子裏面的main
也是中間件。每一箇中間件默認接受兩個參數,第一個參數是 Context 對象,第二個參數是next
函數。只要調用next
函數,就能夠把執行權轉交給下一個中間件。
運行這個腳本。
$ node 08.js
訪問 http://127.0.0.1:3000 ,命令行窗口會顯示與上一個例子相同的日誌輸出。
多箇中間件會造成一個棧結構(middle stack),以"先進後出"(first-in-last-out)的順序執行。
- 最外層的中間件首先執行。
- 調用
next
函數,把執行權交給下一個中間件。- ...
- 最內層的中間件最後執行。
- 執行結束後,把執行權交回上一層的中間件。
- ...
- 最外層的中間件收回執行權以後,執行
next
函數後面的代碼。
請看下面的例子。
// 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 09.js
訪問 http://127.0.0.1:3000 ,命令行窗口會有以下輸出。
>> one >> two >> three << three << two << one
若是中間件內部沒有調用next
函數,那麼執行權就不會傳遞下去。做爲練習,你能夠將two
函數裏面next()
這一行註釋掉再執行,看看會有什麼結果。
迄今爲止,全部例子的中間件都是同步的,不包含異步操做。若是有異步操做(好比讀取數據庫),中間件就必須寫成 async 函數。請看下面的例子。
// 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('./template.html', 'utf8'); }; app.use(main); app.listen(3000);
上面代碼中,fs.readFile
是一個異步操做,必須寫成await fs.readFile()
,而後中間件必須寫成 async 函數。
運行這個腳本。
$ node 10.js
訪問 http://127.0.0.1:3000 ,就能夠看到模板文件的內容。
koa-compose
模塊能夠將多箇中間件合成爲一個。請看下面的例子。
// 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 11.js
訪問 http://127.0.0.1:3000 ,就能夠在命令行窗口看到日誌信息。
若是代碼運行過程當中發生錯誤,咱們須要把錯誤信息返回給用戶。HTTP 協定約定這時要返回500狀態碼。Koa 提供了ctx.throw()
方法,用來拋出錯誤,ctx.throw(500)
就是拋出500錯誤。請看下面的例子。
// 14.js const main = ctx => { ctx.throw(500); };
運行這個腳本。
$ node 14.js
訪問 http://127.0.0.1:3000,你會看到一個500錯誤頁"Internal Server Error"。
若是將ctx.response.status
設置成404,就至關於ctx.throw(404)
,返回404錯誤。請看下面的例子。
// 15.js const main = ctx => { ctx.response.status = 404; ctx.response.body = 'Page Not Found'; };
運行這個腳本。
$ node 15.js
訪問 http://127.0.0.1:3000 ,你就看到一個404頁面"Page Not Found"。
爲了方便處理錯誤,最好使用try...catch
將其捕獲。可是,爲每一箇中間件都寫try...catch
太麻煩,咱們可讓最外層的中間件,負責全部中間件的錯誤處理。請看下面的例子。
// 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 16.js
訪問 http://127.0.0.1:3000 ,你會看到一個500頁,裏面有報錯提示 {"message":"Internal Server Error"}
。
運行過程當中一旦出錯,Koa 會觸發一個error
事件。監聽這個事件,也能夠處理錯誤。請看下面的例子。
// 17.js const main = ctx => { ctx.throw(500); }; app.on('error', (err, ctx) => console.error('server error', err); );
運行這個腳本。
$ node 17.js
訪問 http://127.0.0.1:3000 ,你會在命令行窗口看到"server error xxx"。
須要注意的是,若是錯誤被try...catch
捕獲,就不會觸發error
事件。這時,必須調用ctx.app.emit()
,手動釋放error
事件,才能讓監聽函數生效。請看下面的例子。
// 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
函數拋出錯誤,被handler
函數捕獲。catch
代碼塊裏面使用ctx.app.emit()
手動釋放error
事件,才能讓監聽函數監聽到。
運行這個腳本。
$ node 18.js
訪問 http://127.0.0.1:3000 ,你會在命令行窗口看到logging error
。
ctx.cookies
用來讀寫 Cookie。請看下面的例子。
// 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 19.js
訪問 http://127.0.0.1:3000 ,你會看到1 views
。刷新一次頁面,就變成了2 views
。再刷新,每次都會計數增長1。
Web 應用離不開處理表單。本質上,表單就是 POST 方法發送到服務器的鍵值對。koa-body
模塊能夠用來從 POST 請求的數據體裏面提取鍵值對。請看下面的例子。
// 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 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 方法向服務器發送一個鍵值對,會被正確解析。若是發送的數據不正確,就會收到錯誤提示。
koa-body
模塊還能夠用來處理文件上傳。請看下面的例子。
// 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 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,原做者的本文的配套示例庫。(若是不方便使用 Git,也能夠下載 zip 文件解壓。)
$ git clone https://github.com/ruanyf/koa-demos.git
接着,進入示例庫,安裝依賴。
$ cd koa-demos $ npm install
全部示例源碼,都在 demos 目錄下面。
koa2的官方文檔資料詳見http://www.koacn.com/#context,https://koa.bootcss.com/#。