你可能會用到的一個路由適配器

前言

此時狀態有點像上學時寫做文,開篇老是"拉"不出來,憋的難受。javascript

原文地址html

源碼地址前端

憋的難受

從背景出發

先後端分離後,前端童鞋會須要處理一些node層的工做,好比模板渲染、接口轉發、部分業務邏輯等,比較經常使用的框架有koa、koa-router等。vue

如今咱們須要實現這樣一個需求:java

  1. 用戶訪問/fe的時候,頁面展現hello fe
  2. 用戶訪問/backend的時候,頁面展現hello backend

你是否是在想,這需求俺根本不用koakoa-router,原生的node模塊就能夠搞定。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}`)
})


複製代碼

確實是,對於很簡單的需求,用上框架彷佛有點浪費,可是對於以上的實現,也有缺點存在,好比react

  1. 須要咱們本身去解析路徑。
  2. 路徑的解析和邏輯的書寫耦合在一塊。若是將來有更多更復雜的需求須要實現,那就gg了。

因此接下來咱們來試試用koakoa-router怎麼實現git

app.jsgithub

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將路由配置和控制器分離開來,使咱們將注意力放在路由配置和控制器的實現上。但願對您能有一點點幫助。

原文地址

源碼地址

相關文章
相關標籤/搜索