一、koa-router
為了處理URL,我們需要引入koa-router
這個middleware,讓它負責處理URL映射。
我們修改app.js
,使用koa-router
來處理URL:
const Koa = require('koa'); // 注意require('koa-router')返回的是函數:
const router = require('koa-router')(); const app = new Koa(); // log request URL:
app.use(async (ctx, next) => { console.log(`Process ${ctx.request.method} ${ctx.request.url}...`); await next(); }); // add url-route:
router.get('/hello/:name', async (ctx, next) => { var name = ctx.params.name; ctx.response.body = `<h1>Hello, ${name}!</h1>`; }); router.get('/', async (ctx, next) => { ctx.response.body = '<h1>Index</h1>'; }); // add router middleware:
app.use(router.routes()); app.listen(3000); console.log('app started at port 3000...');
注意導入koa-router
的語句最后的()
是函數調用
const router = require('koa-router')(); // 相當於:
const fn_router = require('koa-router'); const router = fn_router();
二、處理post請求
用router.get('/path', async fn)
處理的是get請求。如果要處理post請求,可以用router.post('/path', async fn)
。
用post請求處理URL時,我們會遇到一個問題:post請求通常會發送一個表單,或者JSON,它作為request的body發送,但無論是Node.js提供的原始request對象,還是koa提供的request對象,都不提供解析request的body的功能!
所以,我們又需要引入另一個middleware來解析原始request請求,然后,把解析后的參數,綁定到ctx.request.body
中。
koa-bodyparser
就是用來干這個活的。使用npm install
安裝。
接下來,修改app.js
,引入koa-bodyparser
:
const bodyParser = require('koa-bodyparser');
在合適的位置加上
app.use(bodyParser());
由於middleware的順序很重要,這個koa-bodyparser
必須在router
之前被注冊到app
對象上。
三、重構
所有的URL處理函數都放到app.js
里顯得很亂,而且,每加一個URL,就需要修改app.js
。隨着URL越來越多,app.js
就會越來越長。
如果能把URL處理函數集中到某個js文件,或者某幾個js文件中就好了,然后讓app.js
自動導入所有處理URL的函數。這樣,代碼一分離,邏輯就顯得清楚了。最好是這樣:
url2-koa/
+- controllers/
| |
| +- login.js <-- 處理login相關URL | |
| +- users.js <-- 處理用戶管理相關URL |
+- app.js <-- 使用koa的js |
+- package.json <-- 項目描述文件 |
+- node_modules/ <-- npm安裝的所有依賴包
我們先在controllers
目錄下編寫index.js
var fn_index = async (ctx, next) => { ctx.response.body = `<h1>Index</h1>
<form action="/signin" method="post">
<p>Name: <input name="name" value="koa"></p>
<p>Password: <input name="password" type="password"></p>
<p><input type="submit" value="Submit"></p>
</form>`; }; var fn_signin = async (ctx, next) => { var name = ctx.request.body.name || '', password = ctx.request.body.password || ''; console.log(`signin with name: ${name}, password: ${password}`); if (name === 'koa' && password === '12345') { ctx.response.body = `<h1>Welcome, ${name}!</h1>`; } else { ctx.response.body = `<h1>Login failed!</h1>
<p><a href="/">Try again</a></p>`; } }; module.exports = { 'GET /': fn_index, 'POST /signin': fn_signin };
這個index.js
通過module.exports
把兩個URL處理函數暴露出來。類似的,hello.js
把一個URL處理函數暴露出來:
var fn_hello = async (ctx, next) => { var name = ctx.params.name; ctx.response.body = `<h1>Hello, ${name}!</h1>`; } module.exports = { 'GET /hello/:name': fn_hello }
現在,我們修改app.js
,讓它自動掃描controllers
目錄,找到所有js
文件,導入,然后注冊每個URL:
// 先導入fs模塊,然后用readdirSync列出文件 // 這里可以用sync是因為啟動時只運行一次,不存在性能問題:
var files = fs.readdirSync(__dirname + '/controllers'); // 過濾出.js文件:
var js_files = files.filter((f)=>{ return f.endsWith('.js'); }); // 處理每個js文件:
for (var f of js_files) { console.log(`process controller: ${f}...`); // 導入js文件:
let mapping = require(__dirname + '/controllers/' + f); for (var url in mapping) { if (url.startsWith('GET ')) { // 如果url類似"GET xxx":
var path = url.substring(4); router.get(path, mapping[url]); console.log(`register URL mapping: GET ${path}`); } else if (url.startsWith('POST ')) { // 如果url類似"POST xxx":
var path = url.substring(5); router.post(path, mapping[url]); console.log(`register URL mapping: POST ${path}`); } else { // 無效的URL:
console.log(`invalid URL: ${url}`); } } }
四、Controller Middleware
最后,我們把掃描controllers
目錄和創建router
的代碼從app.js
中提取出來,作為一個簡單的middleware使用,命名為controller.js
:
const fs = require("fs") function addMapping(router, mapping) { for (var url in mapping) { if (url.startsWith('GET ')) { var path = url.substring(4); router.get(path, mapping[url]); console.log(`register URL mapping: GET ${path}`); } else if (url.startsWith('POST ')) { var path = url.substring(5); router.post(path, mapping[url]); console.log(`register URL mapping: POST ${path}`); } else { console.log(`invalid URL: ${url}`); } } } function addControllers(router, dir) { // 先導入fs模塊,然后用readdirSync列出文件 // 這里可以用sync是因為啟動時只運行一次,不存在性能問題:
var files = fs.readdirSync(__dirname + dir); // 過濾出.js文件:
var js_files = files.filter(f => { return f.endsWith('.js'); }); // 處理每個js文件:
for (var f of js_files) { console.log(`process controller: ${f}...`); // 導入js文件:
let mapping = require(__dirname + '/controllers/' + f); addMapping(router, mapping); } } module.exports = function (dir) { let controllers_dir = dir || '/controllers', router = require("koa-router")() addControllers(router, controllers_dir) return router.routes() }
這樣一來,我們在app.js
的代碼又簡化了:
const Koa = require("koa") const bodyParser = require("koa-bodyparser") const controller = require("./controller") const app = new Koa() app.use(bodyParser()) // add router middleware:
app.use(controller()); app.listen(8080) console.log("app started at port 8080")
經過重新整理后的工程url2-koa
目前具備非常好的模塊化,所有處理URL的函數按功能組存放在controllers
目錄,今后我們也只需要不斷往這個目錄下加東西就可以了,app.js
保持不變。