你可能会用到的一个路由适配器

前言

此时状态有点像上学时写作文,开篇总是"拉"不出来,憋的难受。

原文地址

源码地址

憋的难受

从背景出发

前后端分离后,前端童鞋会需要处理一些node层的工作,比如模板渲染、接口转发、部分业务逻辑等,比较常用的框架有koa、koa-router等。

现在我们需要实现这样一个需求:

  1. 用户访问/fe的时候,页面展示hello fe
  2. 用户访问/backend的时候,页面展示hello backend

你是不是在想,这需求俺根本不用koakoa-router,原生的node模块就可以搞定。

const http = require('http')
const url = require('url')
const PORT = 3000

http.createServer((req, res) => {
  let { pathname } = url.parse(req.url)
  let str = 'hello'

  if (pathname === '/fe') {
    str += ' fe'
  } else if (pathname === '/backend') {
    str += ' backend'
  }

  res.end(str)
}).listen(PORT, () => {
  console.log(`app start at: ${PORT}`)
})


确实是,对于很简单的需求,用上框架似乎有点浪费,但是对于以上的实现,也有缺点存在,比如

  1. 需要我们自己去解析路径。
  2. 路径的解析和逻辑的书写耦合在一块。如果未来有更多更复杂的需求需要实现,那就gg了。

所以接下来我们来试试用koakoa-router怎么实现

app.js

const Koa = require('koa')
const KoaRouter = require('koa-router')

const app = new Koa()
const router = new KoaRouter()
const PORT = 3000

router.get('/fe', (ctx) => {
  ctx.body = 'hello fe'
})

router.get('/backend', (ctx) => {
  ctx.body = 'hello backend'
})

app.use(router.routes())
app.use(router.allowedMethods())

app.listen(PORT, () => {
  console.log(`app start at: ${PORT}`)
})


通过上面的处理,路径的解析倒是给koa-router处理了,但是整体的写法还是有些问题。

  1. 匿名函数的写法没有办法复用
  2. 路由配置和逻辑处理在一个文件中,没有分离,项目一大起来,同样是件麻烦事。

接下来我们再优化一下,先看一下整体的目录结构

├──app.js // 应用入口
├──controller // 逻辑处理,分模块
│   ├──hello.js
│   ├──aaaaa.js
├──middleware // 中间件统一注册
│   ├──index.js
├──routes // 路由配置,可以分模块配置
│   ├──index.js
├──views // 模板配置,分页面或模块处理,在这个例子中用不上
│   ├──index.html

预览一下每个文件的逻辑

app.js 应用的路口

const Koa = require('koa')
const middleware = require('./middleware')
const app = new Koa()
const PORT = 3000

middleware(app)

app.listen(PORT, () => {
  console.log(`app start at: ${PORT}`)
})

routes/index.js 路由配置中心

const KoaRouter = require('koa-router')
const router = new KoaRouter()
const koaCompose = require('koa-compose')
const hello = require('../controller/hello')

module.exports = () => {
  router.get('/fe', hello.fe)
  router.get('/backend', hello.backend)

  return koaCompose([ router.routes(), router.allowedMethods() ])
}

controller/hello.js hello 模块的逻辑

module.exports = {
  fe (ctx) {
    ctx.body = 'hello fe'
  },
  backend (ctx) {
    ctx.body = 'hello backend'
  }
}

middleware/index.js 中间件统一注册

const routes = require('../routes')

module.exports = (app) => {
  app.use(routes())
}

写到这里你可能心里有个疑问?

image

一个简单的需求,被这么一搞看起来复杂了太多,有必要这样么?

答案是:有必要,这样的目录结构或许不是最合理的,但是路由、控制器、view层等各司其职,各在其位。对于以后的扩展有很大的帮助。

不知道大家有没有注意到路由配置这个地方

routes/index.js 路由配置中心

const KoaRouter = require('koa-router')
const router = new KoaRouter()
const koaCompose = require('koa-compose')
const hello = require('../controller/hello')

module.exports = () => {
  router.get('/fe', hello.fe)
  router.get('/backend', hello.backend)

  return koaCompose([ router.routes(), router.allowedMethods() ])
}

每个路由对应一个控制器去处理,很分离,很常见啊!!!这似乎也是我们平时在前端写vue-router或者react-router的常见配置模式。

但是当模块多起来的来时候,这个文件夹就会变成

const KoaRouter = require('koa-router')
const router = new KoaRouter()
const koaCompose = require('koa-compose')
// 下面你需要require各个模块的文件进来
const hello = require('../controller/hello')
const a = require('../controller/a')
const c = require('../controller/c')

module.exports = () => {
  router.get('/fe', hello.fe)
  router.get('/backend', hello.backend)
  // 配置各个模块的路由以及控制器
  router.get('/a/a', a.a)
  router.post('/a/b', a.b)
  router.get('/a/c', a.c)
  router.get('/a/d', a.d)

  router.get('/c/a', c.c)
  router.post('/c/b', c.b)
  router.get('/c/c', c.c)
  router.get('/c/d', c.d)

  // ... 等等    
  return koaCompose([ router.routes(), router.allowedMethods() ])
}

有没有什么办法,可以让我们不用手动引入一个个控制器,再手动的调用koa-router的get post等方法去注册呢?

比如我们只需要做以下配置,就可以完成上面手动配置的功能。

routes/a.js

module.exports = [
  {
    path: '/a/a',
    controller: 'a.a'
  },
  {
    path: '/a/b',
    methods: 'post',
    controller: 'a.b'
  },
  {
    path: '/a/c',
    controller: 'a.c'
  },
  {
    path: '/a/d',
    controller: 'a.d'
  }
]

routes/c.js

module.exports = [
  {
    path: '/c/a',
    controller: 'c.a'
  },
  {
    path: '/c/b',
    methods: 'post',
    controller: 'c.b'
  },
  {
    path: '/c/c',
    controller: 'c.c'
  },
  {
    path: '/c/d',
    controller: 'c.d'
  }
]

然后使用pure-koa-router这个模块进行简单的配置就ok了

const pureKoaRouter = require('pure-koa-router')
const routes = path.join(__dirname, '../routes') // 指定路由
const controllerDir = path.join(__dirname, '../controller') // 指定控制器的根目录

app.use(pureKoaRouter({
  routes,
  controllerDir
}))

这样整个过程我们的关注点都放在路由配置上去,再也不用去手动require一堆的文件了。

简单介绍一下上面的配置

{
  path: '/c/b',
  methods: 'post',
  controller: 'c.b'
}

path: 路径配置,可以是字符串/c/b,也可以是数组[ '/c/b' ],当然也可以是正则表达式/\c\b/

methods: 指定请求的类型,可以是字符串get或者数组[ 'get', 'post' ],默认是get方法,

controller: 匹配到路由的逻辑处理方法,c.b 表示controllerDir目录下的c文件导出的b方法,a.b.c表示controllerDir目录下的/a/b 路径下的b文件导出的c方法

源码实现

接下来我们逐步分析一下实现逻辑

可以点击查看源码

整体结构

module.exports = ({ routes = [], controllerDir = '', routerOptions = {} }) => {
  // xxx

  return koaCompose([ router.routes(), router.allowedMethods() ])
})

pure-koa-router接收

  1. routes
    1. 可以指定路由的文件目录,这样pure-koa-router会去读取该目录下所有的文件 (const routes = path.join(__dirname, '../routes'))
    2. 可以指定具体的文件,这样pure-koa-router读取指定的文件内容作为路由配置 const routes = path.join(__dirname, '../routes/tasks.js')
    3. 可以直接指定文件导出的内容 (const routes = require('../routes/index'))
  2. controllerDir、控制器的根目录
  3. routerOptions new KoaRouter时候传入的参数,具体可以看koa-router

这个包执行之后会返回经过koaCompose包装后的中间件,以供koa实例添加。

参数适配

assert(Array.isArray(routes) || typeof routes === 'string', 'routes must be an Array or a String')
assert(fs.existsSync(controllerDir), 'controllerDir must be a file directory')

if (typeof routes === 'string') {
  routes = routes.replace('.js', '')

  if (fs.existsSync(`${routes}.js`) || fs.existsSync(routes)) {
    // 处理传入的是文件
    if (fs.existsSync(`${routes}.js`)) {
      routes = require(routes)
    // 处理传入的目录  
    } else if (fs.existsSync(routes)) {
      // 读取目录中的各个文件并合并
      routes = fs.readdirSync(routes).reduce((result, fileName) => {
        return result.concat(require(nodePath.join(routes, fileName)))
      }, [])
    }
  } else {
    // routes如果是字符串则必须是一个文件或者目录的路径
    throw new Error('routes is not a file or a directory')
  }
}

路由注册

不管routes传入的是文件还是目录,又或者是直接导出的配置的内容最后的结构都是是这样的

routes内容预览

[
  // 最基础的配置
  {
    path: '/test/a',
    methods: 'post',
    controller: 'test.index.a'
  },
  // 多路由对一个控制器
  {
    path: [ '/test/b', '/test/c' ],
    controller: 'test.index.a'
  },
  // 多路由对多控制器
  {
    path: [ '/test/d', '/test/e' ],
    controller: [ 'test.index.a', 'test.index.b' ]
  },
  // 单路由对对控制器
  {
    path: '/test/f',
    controller: [ 'test.index.a', 'test.index.b' ]
  },
  // 正则
  {
    path: /\/test\/\d/,
    controller: 'test.index.c'
  }
]


主动注册

let router = new KoaRouter(routerOptions)
let middleware

routes.forEach((routeConfig = {}) => {
  let { path, methods = [ 'get' ], controller } = routeConfig
  // 路由方法类型参数适配
  methods = (Array.isArray(methods) && methods) || [ methods ]
  // 控制器参数适配
  controller = (Array.isArray(controller) && controller) || [ controller ]

  middleware = controller.map((controller) => {
    // 'test.index.c' => [ 'test', 'index', 'c' ]
    let controllerPath = controller.split('.')
    // 方法名称 c
    let controllerMethod = controllerPath.pop()

    try {
      // 读取/test/index文件的c方法
      controllerMethod = require(nodePath.join(controllerDir, controllerPath.join('/')))[ controllerMethod ]
    } catch (error) {
      throw error
    }
    // 对读取到的controllerMethod进行参数判断,必须是一个方法
    assert(typeof controllerMethod === 'function', 'koa middleware must be a function')

    return controllerMethod
  })
  // 最后使用router.register进行注册
  router.register(path, methods, middleware)


源码的实现过程基本就到这里了。

结尾

pure-koa-router将路由配置和控制器分离开来,使我们将注意力放在路由配置和控制器的实现上。希望对您能有一点点帮助。

原文地址

源码地址

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,099评论 18 139
  • 原文链接:http://www.jianshu.com/p/6b816c609669 前传 出于兴趣最近开始研究k...
    悬笔e绝阅读 7,112评论 1 11
  • 目录 一、项目结构的核心思想 二、项目目录结构 三、资源路径编译规则 四、index.html 五、build目录...
    科研者阅读 11,227评论 0 40
  • 什么是半途而废? 为什么会半途而废? 如何走出半途而废的困境? 作为创业狗, 对“半途而废”这个词理解深刻, 因为...
    kevinalliswell阅读 299评论 0 0
  • 又到了一年的平安夜,街道两旁的商店门口都挂起来了五颜六色的小彩灯,一闪一闪的与夜幕中的星星遥相辉映...
    三三两两2333阅读 215评论 2 3