JavaScript基礎之用koa處理url
在hello-koa工程中,我們處理http請求一律返回相同的HTML,這樣雖然非常簡單,但是用瀏覽器一測,隨便輸入任何URL都會返回相同的網頁。
正常情況下,我們應該對不同的URL呼叫不同的處理函式,這樣才能返回不同的結果。例如像這樣寫:
app.use(async (ctx, next) => {
if (ctx.request.path === '/') {
ctx.response.body = 'index page';
} else {
await next();
}
});
app.use(async (ctx, next) => {
if (ctx.request.path === '/test') {
ctx.response.body = 'TEST page';
} else {
await next();
}
});
app.use(async (ctx, next) => {
if (ctx.request.path === '/error') {
ctx.response.body = 'ERROR page';
} else {
await next();
}
});
這麼寫是可以執行的,但是好像有點蠢。
應該有一個能集中處理URL的middleware,它根據不同的URL呼叫不同的處理函式,這樣,我們才能專心為每個URL編寫處理函式。
koa-router
為了處理URL,我們需要引入koa-router這個middleware,讓它負責處理URL對映。
我們把上一節的hello-koa工程複製一份,重新命名為url-koa。
先在package.json中新增依賴項:
"koa-router": "7.0.0"
然後用npm install安裝。
接下來,我們修改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();
然後,我們使用router.get(‘/path’, async fn)來註冊一個GET請求。可以在請求路徑中使用帶變數的/hello/:name,變數可以通過ctx.params.name訪問。
再執行app.js,我們就可以測試不同的URL:
輸入首頁:http://localhost:3000/
輸入:http://localhost:3000/hello/koa
處理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就是用來幹這個活的。
我們在package.json中新增依賴項:
"koa-bodyparser": "3.2.0"
然後使用npm install安裝。
下面,修改app.js,引入koa-bodyparser:
const bodyParser = require('koa-bodyparser');
在合適的位置加上:
app.use(bodyParser());
由於middleware的順序很重要,這個koa-bodyparser必須在router之前被註冊到app物件上。
現在我們就可以處理post請求了。寫一個簡單的登入表單:
router.get('/', 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>`;
});
router.post('/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>`;
}
});
注意到我們用var name = ctx.request.body.name || ”拿到表單的name欄位,如果該欄位不存在,預設值設定為”。
類似的,put、delete、head請求也可以由router處理。
重構
現在,我們已經可以處理不同的URL了,但是看看app.js,總覺得還是有點不對勁。
所有的URL處理函式都放到app.js裡顯得很亂,而且,每加一個URL,就需要修改app.js。隨著URL越來越多,app.js就會越來越長。
如果能把URL處理函式集中到某個js檔案,或者某幾個js檔案中就好了,然後讓app.js自動匯入所有處理URL的函式。這樣,程式碼一分離,邏輯就顯得清楚了。最好是這樣:
url2-koa/
|
+- .vscode/
| |
| +- launch.json <-- VSCode 配置檔案
|
+- controllers/
| |
| +- login.js <-- 處理login相關URL
| |
| +- users.js <-- 處理使用者管理相關URL
|
+- app.js <-- 使用koa的js
|
+- package.json <-- 專案描述檔案
|
+- node_modules/ <-- npm安裝的所有依賴包
於是我們把url-koa複製一份,重新命名為url2-koa,準備重構這個專案。
我們先在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}`);
}
}
}
如果上面的大段程式碼看起來還是有點費勁,那就把它拆成更小單元的函式:
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) {
var files = fs.readdirSync(__dirname + '/controllers');
var js_files = files.filter((f) => {
return f.endsWith('.js');
});
for (var f of js_files) {
console.log(`process controller: ${f}...`);
let mapping = require(__dirname + '/controllers/' + f);
addMapping(router, mapping);
}
}
addControllers(router);
確保每個函式功能非常簡單,一眼能看明白,是程式碼可維護的關鍵。
Controller Middleware
最後,我們把掃描controllers目錄和建立router的程式碼從app.js中提取出來,作為一個簡單的middleware使用,命名為controller.js:
const fs = require('fs');
function addMapping(router, mapping) {
...
}
function addControllers(router, dir) {
...
}
module.exports = function (dir) {
let
controllers_dir = dir || 'controllers', // 如果不傳引數,掃描目錄預設為'controllers'
router = require('koa-router')();
addControllers(router, controllers_dir);
return router.routes();
};
這樣一來,我們在app.js的程式碼又簡化了:
...
// 匯入controller middleware:
const controller = require('./controller');
...
// 使用middleware:
app.use(controller());
...
經過重新整理後的工程url2-koa目前具備非常好的模組化,所有處理URL的函式按功能組存放在controllers目錄,今後我們也只需要不斷往這個目錄下加東西就可以了,app.js保持不變。