美文网首页Koakoa
你可能会用到的一个路由适配器

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

作者: 谦龙 | 来源:发表于2018-08-19 18:40 被阅读5次

    前言

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

    原文地址

    源码地址

    憋的难受

    从背景出发

    前后端分离后,前端童鞋会需要处理一些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将路由配置和控制器分离开来,使我们将注意力放在路由配置和控制器的实现上。希望对您能有一点点帮助。

    原文地址

    源码地址

    相关文章

      网友评论

        本文标题:你可能会用到的一个路由适配器

        本文链接:https://www.haomeiwen.com/subject/rmthiftx.html