Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
qianlongo opened this issue Aug 18, 2018 · 0 comments
Open

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

qianlongo opened this issue Aug 18, 2018 · 0 comments

Comments

@qianlongo
Copy link
Owner

qianlongo commented Aug 18, 2018

前言

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

原文地址

源码地址

憋的难受

从背景出发

前后端分离后,前端童鞋会需要处理一些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())
}

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

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

答案是:有必要,这样的目录结构或许不是最合理的,但是路由、控制器、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将路由配置和控制器分离开来,使我们将注意力放在路由配置和控制器的实现上。希望对您能有一点点帮助。

原文地址

源码地址

@qianlongo qianlongo changed the title 你可能需要的koa-router适配器 你可能会用到的一个路由适配器 Aug 19, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant