Koa中动态修改路由
背景
前几天遇到一个技术问题,Koa-router如何在一定条件之下,动态地修改路由?
具体来说,是使 foo.example.com 和 example.com/foo 都能访问到相同的路由,即子域名和子路由具有相同的效果。
问题
我们可以很容易地创建一个middleware,通过ctx.request.hostname
获取到是否包含子域名 subdomain。
1const subDomainMiddleware = async (ctx, next) => { 2 const hostname = ctx.request.hostname 3 const domainNames = hostname.split('.') 4 if (domainNames.length > 2) { 5 // 有subdomain 6 ctx.appName = domainNames[0] 7 } else { 8 // 无subdomain,取路径的第一段当subdomain 9 ctx.appName = ctx.path.split('/')[0] 10 } 11 // 做一些其它与subDomain关联的配置 12 return next(); 13}
上述的代码,通过子域名或第一级路径的方式获取到了子应用的名称ctx.appName
。
但是当我们想要让二者共享同一套router的时候,就遇到了问题。
先考虑简单的情况,假设有如下的 router 定义:
1import * as Router from 'koa-router'; 2const router = new Router(); 3 4router.get('/', async (ctx) => { ctx.body = 'Hello World!'; }); 5router.get('/test', async (ctx) => { ctx.body = 'test';}); 6 7export const routes = router.routes();
为了同时支持有子域名的情况(不用忽略一级路径)和没有子域名的情况(需要忽略掉第一级路径),最直观的想法是加入如下的改动
1import * as Router from 'koa-router'; 2const router = new Router(); 3 4router.get('/', async (ctx) => { ctx.body = 'Hello World!'; }); 5router.get('/test', async (ctx) => { ctx.body = 'test';}); 6 7// 复制一份带通配符的 8router.get('/:appName/', async (ctx) => { ctx.body = 'Hello World!'; }); 9router.get('/:appName/test', async (ctx) => { ctx.body = 'test';}); 10 11export const routes = router.routes();
但是如上的代码是有bug的,对于 foo.example.com
的访问方式来说,本来应该只有 /
和/test
两个路由,但是现在还会有 /bar/test
之类的路由,产生了多余的路由。
另外还有一个问题,就是这两份router是有可能产生冲突的。如 appName
恰好是test,则 /test
这个路由会出现两次,有冲突。
解决方案
搜索了好久,没发现有人提过类似的问题,就翻了下源码。
刚好看到了如下的一段代码:
1var dispatch = function dispatch(ctx, next) { 2 debug('%s %s', ctx.method, ctx.path); 3 4 var path = router.opts.routerPath || ctx.routerPath || ctx.path; 5 var matched = router.match(path, ctx.method); 6 var layerChain, layer, i; 7 8 if (ctx.matched) { 9 ctx.matched.push.apply(ctx.matched, matched.path); 10 } else { 11 ctx.matched = matched.path; 12 } 13 14 ctx.router = router; 15 16 if (!matched.route) return next(); 17 18 // 其它代码省略 19}
这里有一句关键的代码:var path = router.opts.routerPath || ctx.routerPath || ctx.path;
从这行代码中可以看出,router中在使用ctx.path(实际的路径)之前,会先判断ctx.routerPath是否有值,并优先使用它。
所以我们可以通过修改ctx.routerPath,达到在指定条件下忽略url中第一层路径的需求。
具体代码如下:
1const subDomainMiddleware = async (ctx, next) => { 2 const hostname = ctx.request.hostname 3 const domainNames = hostname.split('.') 4 if (domainNames.length > 2) { 5 // 有subdomain 6 ctx.appName = domainNames[0] 7 } else { 8 // 无subdomain,取路径的第一段当subdomain 9 ctx.appName = ctx.path.split('/')[0] 10 11 // 同时修改ctx.routerPath 12 ctx.routerPath = ctx.path.substr(ctx.appName.length + 1) || '/' 13 } 14 // 做一些其它与subDomain关联的配置 15 return next(); 16}
关键代码是ctx.routerPath = ctx.path.substr(ctx.appName.length + 1) || '/'
这一行。
结论
通过修改ctx.routerPath值,可以动态地调整koa中请求传递给koa-router时有效的路径。