web開發(處理URL)

處理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(); }});

這麼寫是可以運行的,但是好像有點蠢。

Advertisements

應該有一個能集中處理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的語句最後的()是函數調用:

Advertisements

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保持不變。

Advertisements

你可能會喜歡