你可能會用到的一個路由介面卡

謙龍發表於2019-02-16

前言

此時狀態有點像上學時寫作文,開篇總是”拉”不出來,憋的難受。

原文地址

原始碼地址

憋的難受

從背景出發

前後端分離後,前端童鞋會需要處理一些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/

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`))

      1. 可以指定具體的檔案,這樣pure-koa-router讀取指定的檔案內容作為路由配置 const routes = path.join(__dirname, `../routes/tasks.js`)
      2. 可以直接指定檔案匯出的內容 (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將路由配置和控制器分離開來,使我們將注意力放在路由配置和控制器的實現上。希望對您能有一點點幫助。

原文地址

原始碼地址

相關文章