koa-router

677 阅读11分钟

Koa-router 路由模块使用

koa-router模块可以使定义路由执行路由 分开,不需要使用if 判断ctx.request.path ,降低了耦合度.

路由原理

Koa 中间件解析URI

ctx.url 与 ctx.path 属性均是 ctx.request.url 与 ctx.request.path 的别名, ctx.url存储的是除 主域名外的子url路径,path 是除主域名外的url路 + GET 请求参数:


const Koa = require('koa');
const app = new Koa();

app.use(async ctx => {
    console.log(ctx.url); 
    console.log(ctx.path) 
})

app.listen(3000, () => {
    console.log('Example1 server running...')
});
请求url: http://127.0.0.1:3000/test?key1=value1&key2=value2
控制台输出:
ctx.url:  /test
ctx.url:  /test?key1=value1&key2=value2

根据当前 ctx.url 属性即可判断请求 url, 使用中间件对 /test 路径的 GET 进行处理:

const Koa = require('koa');
const app = new Koa();

app.use(async ctx => {
    const { url, method } = ctx;  // 对象解构
    if(url === '/test' && method === 'GET')
    {
        // 仅请求url为 /test  请求方式为: GET 时
        ctx.body = 'Test Page!';
    }
});

app.listen(3000, () => {
    console.log('Example2 server running....')
});
请求url: http://127.0.0.1:3000/test 时返回页面 Test Page.
其它请求因为 ctx.body 为空,导致 响应代码404, 返回默认的 Not fund 字符.

如果对个url进行处理,那就要添加更多的判断 或 中间件。

koa-router 原理

定义一个 Router 类,实现多个 请求地址的解析:

class Router
{
    constructor() {
        this._routers = [];    // 缓存路由规则
    }
    get(url, handler)
    {
        this._routers.push({   // 添加到路由缓存中
            url: url,
            method: 'GET', // 设置请求方式为GET
            handler
        })
    }
    routes(){
        return async (ctx, next) => {
            const {method, url} = ctx;
            const matchedRouter = this._routers.find(r => r.method === method && r.url === url);
            // 判断当前路由字符串 与 请求方式是否匹配
            if(matchedRouter && typeof matchedRouter.handler === 'function')  // 判断匹配的路由 处理器 是否有效
            {
                await matchedRouter.handler(ctx, next);
            }else
            {
                await next();
            }

        }
    }
}
// 如果封装为一个模块则暴露接口:
// module.exports = Router;

const Koa = require('koa');
const app = new Koa();
const router = new Router();

router.get('/test1', ctx => {ctx.body = 'Page Test1'});
router.get('/test2', ctx => {ctx.body = 'Page Test2'});
router.get('/test3', ctx => {ctx.body = 'Page Test3'});
router.get('/test4', ctx => {ctx.body = 'Page Test4'});

app.use(router.routes());

app.listen(3000, () => {
    console.log('Example3 server running...')
});
访问:
http://127.0.0.1:3000/test1
http://127.0.0.1:3000/test2  
http://127.0.0.1:3000/test3  
http://127.0.0.1:3000/test4
都是有效的解析路径,其它路径 响应代码404.

用于注册的函数, 可以对解析路由地址进行注册并添加到缓存变量中。每个请求都会将url、请求方式与缓存中的数据进行比较,如果匹配则执行处理函数。
上面仅对GET请求进行了处理,koa-router中间件支持更多的请求方式及注册方法.

请求方法

Koa-router 注册路由

如果没有安装 koa-router包 需要进行下载添加: npm install koa-router --save

const Koa = require('koa');
const Router = require('koa-router');
const app = new Koa();
const router = new Router();

// 注册路由:
router.get('/test1', ctx => {ctx.body = 'Page Test1'});
router.get('/test2', ctx => {ctx.body = 'Page Test2'});
router.get('/test3', ctx => {ctx.body = 'Page Test3'});
router.get('/test4', ctx => {ctx.body = 'Page Test4'});

app.use(router.routes());

app.listen(3000, () => {
    console.log('Example4 server running...');
});
访问:
http://127.0.0.1:3000/test1
http://127.0.0.1:3000/test2  
http://127.0.0.1:3000/test3  
http://127.0.0.1:3000/test4
都是有效的解析路径,其它路径 响应代码404.

Koa-router 支持请求方法

HTTP常用的 9 种 请求方法:

序号标准方法描述
1HTTP1.0GET请求指定的页面信息,并返回实体主体。
2HTTP1.0HEAD类似于 GET 请求,只不过返回的响应中没有具体的内容,用于获取报头。
3HTTP1.0POST向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST 请求可能会导致新的资源的建立和/或已有资源的修改。。
4HTTP1.1PUT从客户端向服务器传送的数据取代指定的文档的内容。
5HTTP1.1DELETE请求服务器删除指定的页面。
6HTTP1.1CONNECTHTTP/1.1 协议中预留给能够将连接改为管道方式的代理服务。
7HTTP1.1OPTIONS允许客户端查看服务器的性能。
8HTTP1.1TRACE回显服务器收到的请求,主要用于测试或诊断。
9HTTP1.1PATCH是对 PUT 方法的补充,用来对已知资源进行局部更新 。

虽然方法很多,但实际上用的就几个,下面测试一下 koa-router 支持的请求方式:

const Koa = require('koa');
const Router = require('koa-router');

const app = new Koa();
const router = new Router();

const method_array = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH'];

method_array.forEach(method => {
    const router_method = router[method.toLowerCase()]; // 将方法名称转换为小写
    const info = `koa-router模块${!router_method?'不':''}支持${method}请求方式!`
    if(router_method)
    {
        router_method.call(router, '/test', async ctx => {ctx.body = info});
    }
    console.log(info);
})
app.use(router.routes());

app.listen(3000, () => {
    console.log('Example5 server running...');
});
控制台输出:
koa-router模块支持GET请求方式!
koa-router模块支持HEAD请求方式!
koa-router模块支持POST请求方式!
koa-router模块支持PUT请求方式!
koa-router模块支持DELETE请求方式!
koa-router模块支持CONNECT请求方式!
koa-router模块支持OPTIONS请求方式!
koa-router模块支持TRACE请求方式!
koa-router模块支持PATCH请求方式!
Example5 server running...

使用 ApiPost等测试工具对地址http://127.0.0.1:3000/test 进行不同方式的访问即可查看效果。

符合 RESTful规范 的API

例如站点中通过API对 用户 数据的增删改查操作,在非RESTful架构中,可能被设计为如下所示:

http://api.test.com/addUser    // POST方法  请求发送新增用户信息
http://api.test.com/deleteUser // POST方法  请求发送删除的用户信息
http://api.test.com/updateUser // POST方法  请求发送要修改的用户信息
http://api.test.com/getUser    // GET方法   请求发送要查看的用户信息

而基于RESTful架构设计 的API就可以全局只提供唯一的URI: api.test.com/users 。针对不同的 method 请求方式从而达成不同的操作:

http://api.test.com/users          // POST方法      请求发送新增用户信息
http://api.test.com/users/:id      // DELETE方法    请求发送删除的用户信息
http://api.test.com/users/:id      // PUT方法       请求发送要修改的用户信息
http://api.test.com/users/:id      // GET方法       请求发送要查看的用户信息

在任意的HTTP请求中,遵从RESTful规范,可以把GET、POST、PUT、DELETE类型的请求分别对应“查”、“增”、“改”、“删”操作。接口实现:

const Koa = require('koa');
const Router = require('koa-router');
const BodyParser = require('koa-bodyparser');
const app = new Koa();
const router = new Router();

router.post('/users', ctx => {
    ctx.body = `新增用户ID=${ctx.request.body}的信息.`;
    // post 请求 application/x-www-form-urlencoded 数据
    // 因为此时用户的数据 id 还没有建立,所以不能通过 url 携带id数据
    
})
router.get('/users/:id', ctx => {
    ctx.body = `查看用户ID=${ctx.params.id}的信息.`;
});
router.delete('/users/:id', ctx => {
    ctx.body = `删除用户ID=${ctx.params.id}的信息.`;
});
router.put('/users/:id', ctx => {
    ctx.body = `修改用户ID=${ctx.params.id}的信息.`;
});
app.use(BodyParser()); // 注册中间件 post数据解析 模块

app.use(router.routes());

app.listen(3000, () => {
    console.log('Example6 server running...');
});

// application/x-www-form-urlencoded  是ajax form 表单的默认提交方式,该方式会对请求提交的数据进行字符串解码
//  ultipart/form-data 提交的方式是二进制数据流,一般用于文件的传输

// 以上DELETE访问方式为什么不携带数据进行提交,是因为HTTP规范 虽然没有规定 DELETE 请求不能携带数据。
// 但也没有规定这些访问方式可以携带数据,HTTP框架一般情况下,都是对其携带的数据进行忽略处理。
// 所以在这个例子中 bodyparse 模块并不能解析到 DELETE请求所提交的数据,只能使用URL参数进行处理.
接口测试:
1、http://127.0.0.1:3000/users      POST   请求 提交数据 id=17
返回: 新增用户ID=17的信息.
2、http://127.0.0.1:3000/users/17   DELETE 请求
返回:删除用户ID=17的信息.
3、http://127.0.0.1:3000/users/17   PUT    请求 可提交要修改的数据
返回:修改用户ID=17的信息.
4、http://127.0.0.1:3000/users/17   GET    请求
返回: 查看用户ID=17的信息.

除HTTP标准方法之外,koa-router 还提供了一个 all 方法,只要匹配路由,任意一种访问方式均调用该中间件(上一个中间件中必须调用 next函数),修改以上的例子:

router.post('/users', (ctx, next) => {  // 修改 post 函数 使其调用 next函数
    ctx.body = `新增用户ID=${ctx.request.body.id}的信息.`;
    next();
});
router.get('/users/:id', (ctx, next)=> {  // 修改 get函数使其调用 next 函数
    ctx.body = `查看用户ID=${ctx.params.id}的信息.`;
    next();
});
router.delete('/users/:id', ctx => {
    ctx.body = `删除用户ID=${ctx.params.id}的信息.`;
});
router.put('/users/:id', ctx => {
    ctx.body = `修改用户ID=${ctx.params.id}的信息.`;
});
router.get('/users', (ctx, next) => {  // 新添加一个路由 GET /users
    ctx.body = '/users';
    next();
});
router.all('/users/:id', (ctx, next) =>
{
    ctx.body += 'all 中间件被调用!';
    next();
});
app.use(BodyParser());

app.use(router.routes());

app.listen(3000, () => {
    console.log('Example6 server running...');
});
测试1
=============================================
访问: http://127.0.0.1/users/17    请求方式:GET
返回: 查看用户ID=17的信息.all 中间件被调用!  
说明all中间件被调用.

测试2
=============================================
访问: http://127.0.0.1/users/17    请求方式:DELETE
返回: 删除用户ID=0的信息.
DELETE 请求处理函数中没有调用 next 函数,所以all中间件并没有被执行
同时也说明了一个问题: 只有在上一个匹配的中间件中调用 next 函数 才会调用中间件 all 函数;

测试3
=============================================
访问: http://127.0.0.1/users       请求方式:GET
返回:/users
虽然 /users 路由在 get 函数中执行了 next 函数,但是并没有调用 all 中间件
说明了一个问题:all 函数只有在路由字符串 '/users/id' 匹配时,且上一个路由调用了 next 函数后才会被执行.

测试4
=============================================
访问: http://127.0.0.1/users      请求方式:POST  提交数据: id=17
返回:新增用户ID=17的信息.
即使 /users post 函数中执行了 next函数,但是all函数 因为不匹配路由字符串 '/users/:id' 所以也没有被调用.

测试5
=============================================
访问: http://127.0.0.1/users/17   请求方式: COPY
返回: undefinedall 中间件被调用!
虽然没有定义 COPY 请求的路由,但是 all 函数因为路由匹配 仍然被执行了.

统一API接口

根据上面的测试4情况,这时候就遇到一个问题。我们之前设计的API接口除 POST 请求外,均为 /users/:id 仅有 POST 请求为 /users,为了使接口的统一性,对POST接口进行修改:

http://api.test.com/users/:id    // POST方法 请求发送新增用户信息 
http://api.test.com/users/:id    // DELETE方法 请求发送删除的用户信息 
http://api.test.com/users/:id    // PUT方法 请求发送要修改的用户信息 
http://api.test.com/users/:id    // GET方法 请求发送要查看的用户信息

接口实现:

const Koa = require('koa');
const Router = require('koa-router');
const BodyParser = require('koa-bodyparser');
const app = new Koa();
const router = new Router();

router.post('/users/:id', (ctx, next) => {
    ctx.body = `新增用户接口,请求数据:${JSON.stringify(ctx.request.body)}`;
    next();
});
router.get('/users/:id', (ctx, next) => {
    ctx.body = `查看用户接口,用户:ID=${ctx.params.id}`;
    next();
});
router.delete('/users/:id', (ctx, next) => {
    ctx.body = `删除用户接口,用户:ID=${ctx.params.id}`;
    next();
});
router.put('/users/:id', (ctx, next) => {
    ctx.body = `修改用户接口,用户:ID=${ctx.params.id}, 请求数据:${JSON.stringify(ctx.request.body)}`;
    next();
});
router.all('/users/:id', (ctx, next) => {
    ctx.body += '    all函数被调用!';
    next();
});

app.use(BodyParser()).use(router.routes());

app.listen(3000, () => {
    console.log('Example7 server running...')
});
访问: http://127.0.0.1/users/0    请求方式: POST 提交数据: id=17, name=张三
返回: 新增用户接口,请求数据:{"id":"17","name":"张三"}    all函数被调用!

访问: http://127.0.0.1/users/17    请求方式: DELETE 
返回: 删除用户接口,用户:ID=17    all函数被调用!

访问: http://127.0.0.1/users/17    请求方式: GET
返回: 查看用户接口,用户:ID=17    all函数被调用!

访问: http://127.0.0.1/users/0     请求方式: PUT 提交数据: name=李四
返回: 修改用户接口,用户:ID=17, 请求数据:{"name":"李四"}    all函数被调用!

因为请求处理函数中均调用了 next 函数,所以 all 处理函数均被执行.但如上面的 Example6 中的 测试5 遇到的情况一样:

访问: http://127.0.0.1/users/17    请求方式: COPY
返回: undefined    all函数被调用!

COPY 请求 路由 /users/:id 匹配了 all 中的路由字符串, all 函数也被执行了, 并且 all 函数中的ctx.body += '...'语句,导致 koa 无法通过 ctx.body = undefined 来判断是否产生响应处理。

所以,一般情况下,在 all函数中 均是添加一些 响应头 如允许跨域 请求头等操作,当然也可以通过判断请求方式来控制仅对 允许的 请求方式执行操作:

const all_accepts_method = ['GET', 'POST', 'DELETE', 'PUT'];  // 添加允许的请求方法
router.all('/users/:id', (ctx, next) => {
    if(all_accepts_method.includes(ctx.method){
        ctx.body += '    all函数被调用!';
        next();
    }
});
访问: http://127.0.0.1/users/17  请求方式: GET
返回: 查看用户接口,用户:ID=17

访问: http://127.0.0.1/users/17  请求方式: COPY
返回:响应状态 404  Not Found

中间件断链

next 调用的不仅all处理函数

在使用 请求处理函数中 不执行 next 以避免执行 all 处理函数时,注意要将路由中间件注册到中间件的最后,否则将导致中间件断链的情况:

const Koa = require('koa');
const Router = require('koa-router');
const BodyParser = require('koa-bodyparser');
const app = new Koa();
const router = new Router();

router.post('/users/:id', (ctx, next) => {
    ctx.body = `新增用户接口,请求数据:${JSON.stringify(ctx.request.body)}`;
    next();
});
router.get('/users/:id', (ctx, next) => {
    ctx.body = `查看用户接口,用户:ID=${ctx.params.id}`;   // 将GET请求的 next 函数执行删 除掉,以避免调用 all 函数的执行
});
router.delete('/users/:id', (ctx, next) => {
    ctx.body = `删除用户接口,用户:ID=${ctx.params.id}`;
    next();
});
router.put('/users/:id', (ctx, next) => {
    ctx.body = `修改用户接口,用户:ID=${ctx.params.id}, 请求数据:${JSON.stringify(ctx.request.body)}`;
    next();
});
const all_accepts_method = ['GET', 'POST', 'DELETE', 'PUT'];  // 添加允许的请求方法
router.all('/users/:id', (ctx, next) => {
    console.log(ctx.method);
    if(all_accepts_method.includes(ctx.method)){
        ctx.body += '    all函数被调用!';
        next();
    }
});

app.use(BodyParser()).use(router.routes());

app.use(ctx => {  // 在路由中间件注册之后注册的中间件!
    ctx.set('X-Custom-Header', 'true');  // 设置一个自定义响应头
})

app.listen(3000, () => {
    console.log('Example7 server running...')
});
访问: http://127.0.0.1/users/0    请求方式: POST 提交数据: id=17, name=张三
返回: 新增用户接口,请求数据:{"id":"17","name":"张三"}    all函数被调用!
响应头中包含 自定义设置的数据项: X-Custom-Header  
说明在路由之后注册的 中间件被执行.


访问: http://127.0.0.1/users/17  请求方式: GET
返回: 查看用户接口,用户:ID=17
响应头中并不包含自定义设置的数据项,所以虽然在 get 处理函数中取消 next 函数,避免了调用 all函数,但同时也中断了 中间件链。 
导致了路由之后注册的中间件 均不会被执行.

高级路由特性

命名路由

通过名称来标识一个路由,特别是在拼接一个具体的URL显示在 链接 或执行 重定向 时显的更方便,下面创建 一个Router实例,并给其中的某一个路由设置名称:

// Example 8

// 设置此路由的名称为 user
router.get('user', '/users/:id', ctx => {
    // ...
});
// 通过调用路由名称 user 生成路由:  "/users/3"
router.url('user', {id: 3});
// url 函数 支持对象解构:
router.url('user', 3);

模拟用户登陆验证,如果用户未登陆则跳转至登陆页:

const Koa = require('koa');
const Router = require('koa-router');

const app = new Koa();
const router = new Router();

router.get('login', '/login', ctx => {
    const login_user = ctx.cookies.get('login_user');  // 获取cookie
    if(login_user)
    {
        ctx.body = `已登陆用户:${login_user}`; // 如果检测到登陆 cookie 则显示字符串已登陆
    }else
    {
        // 否则显示 form 表单 提供登陆验证
        ctx.type = 'text/html';  
        ctx.body = `
        <form method="post">
            <button>点击添加登陆cookie</button>
        </form>
        `;
    }
});
router.post('/login', ctx => {
    // 添加用户登陆验证 cookie
    ctx.cookies.set('login_user', 'id1', {maxAge: 10000, sameSize:'lax'});  // 10秒钟后过期
    // // 获取来源页面:
    const source = ctx.query.source;  // 获取登陆页 的 来源页
    if(source)
    {
        // 跳转到来源页面
        ctx.redirect(router.url(source)); // 跳转到来源页面
    }else
    {
        // 跳转到 主页 或其它位置 
        ctx.redirect(router.url('login'));
    }

});

router.get('settings', '/settings', ctx => {
    // 检测用户是否登陆
    const login_user = ctx.cookies.get('login_user');  // 获取cookie
    if(login_user)
    {
        // 正常显示设置页
        ctx.body = `用户设置页面, 当前登陆用户:${login_user}.`;
    }else
    {
        // 跳转到登陆页面
        ctx.redirect(router.url('login') + '?source=settings');
    }

});

app.use(router.routes());
app.listen(3000, () => {
    console.log('Example10 server running...')
});
1、访问: http://127.0.0.1/settings 将重定向至登陆 /login
2、点击 login 页面的 设置登陆cookie按钮 将重定向至 /settings
3、直接访问 login 页 设置登陆 cookies 状态下,将被 重定向至 GET 请求的/login,显示已登陆 此处可以设置重定向至 /index 或其它 url

为每一个列表用户生成一个查看详情 url:

const Koa = require('koa');
const Router = require('koa-router');

const app = new Koa();
const router = new Router();

// 创建一个命名路由
const users = new Map();
for(let index = 1; index <= 50; index++)
{
    // 生成测试用户数据
    index = index.toString();
    users.set(index, {id: index, name: `测试用户名称【${index.padStart(2, '0')}】`, age: Math.floor(Math.random() * 21 + 10), sex: Math.random() > 0.5});
}
router.get('user', '/user/:id', ctx => {
    // 添加命名路由,名称为: user
    let user_data = users.get(ctx.params.id);
    if(user_data)
    {
        ctx.body = user_data;
    }
});
router.get('/users', ctx => {
    // 全部用户数据页
    ctx.type = 'text/html';
    let user_element = Array.from(users.values()).map(
        user => `<li>${user.name}  <button onclick="window.open('${router.url('user', user.id)}')">查看用户数据</button></li><br>`).join('\n');
    // 使用命名路由生成 url
    if(user_element)
    {
        ctx.body = `<ol>${user_element}</ol>>`;
    }else
    {
        ctx.body = '<p>暂无用户数据!</p>';
    }

});

app.use(router.routes());

app.listen(3000, () => {
    console.log('Example10 server running...');
})

多中间件路由

koa-router 支持单 个路由多中间件的处理。通过这个特性,能够为一个路由添加特殊的中间件,也可以把一个路由要做的事情拆分成多个步骤去实现:

const Koa = require('koa');
const Router = require('koa-router');

const app = new Koa();
const router = new Router();

router.get('/', (ctx, next) => {
    ctx.body = '路由/中的第一个中间件\n';
    next();
}, (ctx, next) => {
    ctx.body += '路由/中的第二个中间件\n';
    next();  // 注意: 这个位置不调用 next 移交下个中间件的控制权 中间件链同样会断
}); // 同一个路由中使用两个处理函数,则控制函数将依次执行。

app.use(router.routes());

app.use(ctx => {
    ctx.body += '最后一个中间件'
});

多请求方式路由

一个处理函数同时注册两个路由的方式:

const Koa = require('koa');
const Router = require('koa-router');

const app = new Koa();
const router = new Router();

router.register('/', ['get', 'post'], ctx => {
    ctx.body = `请求方式: ${ctx.method}`;
});
app.use(router.routes());

app.listen(3000, () => {
    console.log('Example11 server running...');
});
访问: http://127.0.0.1/    GET方式请求
返回:GET方式请求

访问: http://127.0.0.1/    POST方式请求
返回: POST方式请求

访问: http://127.0.0.1/    PUT方式请求
返回: 响应代码404 Not Found

相比单独定义函数,然后在注册路由请求方式时分别定义,这种方式更简便一些。

嵌套路由

路由实例调用 router.routes() 函数,本身就会返回一个中间件。如 Example3 中所示,所以多层路由之间可以实现嵌套调用,对匹配剩余URL进行二次处理:

例如设计一个IT论坛,有 python、javascript、nodejs 三个版块,而三个版块中又有不同的 文章,路由如下:

http://127.0.0.1:3000/              首页
http://127.0.0.1:3000/1/            编程语言版块页面
http://127.0.0.1:3000/1/articles/   编程语言文章列表
http://127.0.0.1:3000/1/articals/1/ 文章地址

接口实现:

const Koa = require('koa');
const Router = require('koa-router');

const language_router = new Router();
const article_router = new Router();
const app = new Koa();

// 添加测试数据
const language_map = new Map();
language_map.set('1', 'javascript');
language_map.set('2', 'python');
language_map.set('3', 'nodejs');

// 添加编程语言路由
language_router.get('/', (ctx, next) => {
    ctx.body = '论坛首页';
    next();
});

language_router.get('/:lid', (ctx, next) => {
    let language = language_map.get(ctx.params.lid);
    if(language)
    {
        ctx.body = `编程语言${language}版块页面`;
    }
    next();
});

// 添加编程语言文章路由
article_router.get('/', (ctx, next) => {
    // Router实例的use方法 注册的 嵌套路由中间件,可以接收到外层 Router 实例的 url 参数 lid
    let language = language_map.get(ctx.params.lid);
    if(language) {
        ctx.body = `编程语言${language}文章列表`;
        next();
    }
});
article_router.get('/:aid', (ctx, next) => {
    let language = language_map.get(ctx.params.lid);
    if(language)
    {
        ctx.body = `编程语言${language}文章:${ctx.params.aid}详情页面`;
        next();
    }
});

// 使用Router实例 .use 方法 注册路由
language_router.use('/:lid/articles', article_router.routes()); 
// 将 url 中 /:id/articles 剩余部分交由 article_router 继续处理.

// 注册 语言 路由
app.use(language_router.routes());

app.listen(3000, () => {
    console.log('Example12 server running...');
});
访问: 127.0.0.1:3000
返回:论坛首页

访问: 127.0.0.1:3000/1
返回:编程语言javascript版块页面

访问: 127.0.0.1:3000/2
返回: 编程语言python版块页面

访问: 127.0.0.1:3000/3
返回: 编程语言nodejs版块页面

访问: 127.0.0.1:3000/4
返回: 响应代码 404 Not found

访问:127.0.0.1:3000/1/articles
返回: 编程语言javascript文章列表

访问:127.0.0.1:3000/1/articles/1
返回: 编程语言javascript文章:1详情页面

多层路由嵌套可以使 嵌套(多级) url的处理,更简单、优雅。

嵌套路由中注册多中间件

在上面的例子中, article_router 使用了 language_router 解析以后的剩余路径再次进行解析,所以如果在 language_router 中存在逻辑,那需要在 artical_router 中再重复一次,如:

let language = language_map.get(ctx.params.lid);
if(language) {
    ...
}

解决这种情况,可以将逻辑判断独立出来,使用 多中间件路由 进行处理:

const Koa = require('koa');
const Router = require('koa-router');

const language_router = new Router();
const article_router = new Router();
const app = new Koa();

// 添加测试数据
const language_map = new Map();
language_map.set('1', 'javascript');
language_map.set('2', 'python');
language_map.set('3', 'nodejs');

// 添加 language 路由逻辑
function exist_language(ctx, next){
    let language = language_map.get(ctx.params.lid);
    if(language)
    {
        ctx.state.language = language;  // 将数据定义在 state 属性上,在下一个中间件中继续使用
        next();
    }
}

// 添加编程语言路由
language_router.get('/', (ctx, next) => {
    ctx.body = '论坛首页';
    next();
});

language_router.get('/:lid', exist_language, (ctx, next) => {
    ctx.body = `编程语言${ctx.state.language}版块页面`;
    next();
});

// 添加编程语言文章路由
article_router.get('/', exist_language, (ctx, next) => {
    // Router实例的use方法 注册的 嵌套路由中间件,可以接收到外层 Router 实例的 url 参数 lid
    ctx.body = `编程语言${ctx.state.language}文章列表`;
    next();
});
article_router.get('/:aid', exist_language, (ctx, next) => {
    ctx.body = `编程语言${ctx.state.language}文章:${ctx.params.aid}详情页面`;
    next();
});

// use 注册嵌套中间件,同样可以 注册多个中间件处理函数:
language_router.use('/:lid/articles', exist_language,  article_router.routes());

// 注册 语言 路由
app.use(language_router.routes());

app.listen(3000, () => {
    console.log('Example13 server running...');
});

嵌套路由中注册多个中间件,可以将 URL 多层嵌套函数分离解析,提高代码复用性。在解析多层级 URL 参数情况下非常有用。

路由前缀

嵌套路由适用于 多层URL参数的解析,但有时候虽然会有多层级 URL 但并不一定存在动态参数,在不解析动态参数的多层级URL中,应使用 路由前缀,如下面一个学生管理的例子:

http://127.0.0.1               学校首页
http://127.0.0.1/students      学生列表
http://127.0.0.1/students/1    学生详情页  使用 RESTful 增删改查
http://127.0.0.1/grads         班级列表
http://127.0.0.1/grads/1       班级详情    使用RESTful 增删改查
http://127.0.0.1/teachers      老师列表
http://127.0.0.1/teachers/1    老师详情    使用RESTful 增删改查

其中 students、grads、teachers 字符串是固定的,实现逻辑也不相同,在这个例子中使用 嵌套 路由,会导致逻辑代码集中,使用路由前缀来分离各路由之间的逻辑:

const Koa = require('koa');
const Router = require('koa-router');

const app = new Koa();
// const student_router = new Router({prefix: '/students'});
// const grads_router = new Router({prefix: '/grads'});
// const teachers_router = new Router({prefix: '/grads'});
const other_routers = Router();

// 如果路由逻辑过多,可以单独创建一个文件夹 routes 然后将每一个 Router 实例单独分离实现业务逻辑
const RegisterRouter = (prefix, root = null, get = null, post = null, del = null, put = null) => {
    const router = new Router({prefix: prefix});
    const path = '/:id';
    if(root) router.get('/', root);
    if(get) router.get(path, get);
    if(post) router.post(path, post);
    if(del) router.delete(path, del);
    if(put) router.put(path, put);
    return router;
}
// 生成学生路由
const student_router = RegisterRouter('/students',
    (ctx, next) => {
        ctx.body = '学生列表页,单独业务逻辑!';
        next();
    },
    (ctx, next) => {
        ctx.body = `查看学生详细信息${ctx.params.id},单独业务逻辑!`;
        next()
    },
    (ctx, next) => {
        ctx.body = `添加学生详细信息${ctx.params.id},单独业务逻辑!`;
        next()
    },
    (ctx, next) => {
        ctx.body = `删除学生详细信息${ctx.params.id},单独业务逻辑!`;
        next()
    },
    (ctx, next) => {
        ctx.body = `修改学生详细信息${ctx.params.id},单独业务逻辑!`;
        next()
    }
);
// 生成班级路由
const grad_router = RegisterRouter('/grad',
    (ctx, next) => {
        ctx.body = '班级列表页,单独业务逻辑!';
        next();
    },
    (ctx, next) => {
        ctx.body = `查看班级详细信息${ctx.params.id},单独业务逻辑!`;
        next()
    },
    (ctx, next) => {
        ctx.body = `添加班级详细信息${ctx.params.id},单独业务逻辑!`;
        next()
    },
    (ctx, next) => {
        ctx.body = `删除班级详细信息${ctx.params.id},单独业务逻辑!`;
        next()
    },
    (ctx, next) => {
        ctx.body = `修改班级详细信息${ctx.params.id},单独业务逻辑!`;
        next()
    }
);

// 生成老师路由
const teacher_router = RegisterRouter('/teacher',
    (ctx, next) => {
        ctx.body = '教师列表页,单独业务逻辑!';
        next();
    },
    (ctx, next) => {
        ctx.body = `查看教师详细信息${ctx.params.id},单独业务逻辑!`;
        next()
    },
    (ctx, next) => {
        ctx.body = `添加教师详细信息${ctx.params.id},单独业务逻辑!`;
        next()
    },
    (ctx, next) => {
        ctx.body = `删除教师详细信息${ctx.params.id},单独业务逻辑!`;
        next()
    },
    (ctx, next) => {
        ctx.body = `修改教师详细信息${ctx.params.id},单独业务逻辑!`;
        next()
    }
);

// 主页路由
other_routers.get('/', (ctx, next) => {
    ctx.body = '学校首页'
    next();
})
// 注册路由
app
    .use(student_router.routes())
    .use(grad_router.routes())
    .use(teacher_router.routes())
    .use(other_routers.routes());

app.listen(3000, () => {
    console.log('Example14 server running...');
});

嵌套路由 不同的是,路由前缀是一个固定的字符串,不能添加动态的URL参数。

正则URL参数

path-to-regexp模块学习笔记

koa-router 支持URL参数,该参数会被添加到 ctx.params 中。参数可以是一个正则表达式,这个功能是通过 path-to-regxp 实现的,原理是把URL字符串转化成正则对象:

const Koa = require('koa');
const Router = require('koa-router');
const app = new Koa();
router = new Router();

router.get('/users/:id(\d{4})', ctx => {
    // 正则匹配 id 仅支持 0-9数字 必须是4位
    ctx.body = JSON.stringify(ctx.params);
});

app.use(router.routes());

app.listen(3000, () => {
    console.log('Example15 server running...');
});

例如图片的获取URL,在很多场景中需要分别获取原图与缩略图:

http://127.0.0.1/images/10210.png        获取原图像
http://127.0.0.1/images/10210.png@small  获取缩略图
const Koa = require('koa');
const Router = require('koa-router');
const app = new Koa();
const router = new Router({prefix: '/images'});

router.get('/:img(\d+.png){@:size(small)}?', ctx => {
    ctx.body = `【获取图片URL】  路径: ${ctx.params['img']}, 图片尺寸: ${ctx.params['size']?'缩略图':'全尺寸.'} `;
})

app.use(router.routes());

app.listen(3000, () => {
    console.log('Example15 server running...');
});

完结!