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