1. 程式人生 > >JavaScript基礎之用koa處理url

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