logo

鱼肚的博客

Don't Repeat Yourself

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时有效的路径。