你可能會(huì)用到的一個(gè)路由適配器

前言

此時(shí)狀態(tài)有點(diǎn)像上學(xué)時(shí)寫作文,開篇總是"拉"不出來溶浴,憋的難受翘鸭。

原文地址

源碼地址

憋的難受

從背景出發(fā)

前后端分離后,前端童鞋會(huì)需要處理一些node層的工作戳葵,比如模板渲染就乓、接口轉(zhuǎn)發(fā)、部分業(yè)務(wù)邏輯等拱烁,比較常用的框架有koa生蚁、koa-router等。

現(xiàn)在我們需要實(shí)現(xiàn)這樣一個(gè)需求:

  1. 用戶訪問/fe的時(shí)候戏自,頁面展示hello fe
  2. 用戶訪問/backend的時(shí)候邦投,頁面展示hello backend

你是不是在想,這需求俺根本不用koa擅笔、koa-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}`)
})


確實(shí)是猛们,對(duì)于很簡單的需求念脯,用上框架似乎有點(diǎn)浪費(fèi),但是對(duì)于以上的實(shí)現(xiàn)弯淘,也有缺點(diǎn)存在绿店,比如

  1. 需要我們自己去解析路徑。
  2. 路徑的解析和邏輯的書寫耦合在一塊庐橙。如果未來有更多更復(fù)雜的需求需要實(shí)現(xiàn)假勿,那就gg了。

所以接下來我們來試試用koakoa-router怎么實(shí)現(xiàn)

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. 匿名函數(shù)的寫法沒有辦法復(fù)用
  2. 路由配置和邏輯處理在一個(gè)文件中浆竭,沒有分離浸须,項(xiàng)目一大起來,同樣是件麻煩事兆蕉。

接下來我們?cè)賰?yōu)化一下,先看一下整體的目錄結(jié)構(gòu)

├──app.js // 應(yīng)用入口
├──controller // 邏輯處理,分模塊
│   ├──hello.js
│   ├──aaaaa.js
├──middleware // 中間件統(tǒng)一注冊(cè)
│   ├──index.js
├──routes // 路由配置,可以分模塊配置
│   ├──index.js
├──views // 模板配置羽戒,分頁面或模塊處理,在這個(gè)例子中用不上
│   ├──index.html

預(yù)覽一下每個(gè)文件的邏輯

app.js 應(yīng)用的路口

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 中間件統(tǒng)一注冊(cè)

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

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

寫到這里你可能心里有個(gè)疑問虎韵?

image

一個(gè)簡單的需求易稠,被這么一搞看起來復(fù)雜了太多,有必要這樣么包蓝?

答案是:有必要驶社,這樣的目錄結(jié)構(gòu)或許不是最合理的企量,但是路由、控制器亡电、view層等各司其職届巩,各在其位。對(duì)于以后的擴(kuò)展有很大的幫助份乒。

不知道大家有沒有注意到路由配置這個(gè)地方

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() ])
}

每個(gè)路由對(duì)應(yīng)一個(gè)控制器去處理恕汇,很分離,很常見盎蛳健qⅰ!颂暇!這似乎也是我們平時(shí)在前端寫vue-router或者react-router的常見配置模式缺谴。

但是當(dāng)模塊多起來的來時(shí)候,這個(gè)文件夾就會(huì)變成

const KoaRouter = require('koa-router')
const router = new KoaRouter()
const koaCompose = require('koa-compose')
// 下面你需要require各個(gè)模塊的文件進(jìn)來
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)
  // 配置各個(gè)模塊的路由以及控制器
  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() ])
}

有沒有什么辦法,可以讓我們不用手動(dòng)引入一個(gè)個(gè)控制器耳鸯,再手動(dòng)的調(diào)用koa-router的get post等方法去注冊(cè)呢湿蛔?

比如我們只需要做以下配置,就可以完成上面手動(dòng)配置的功能县爬。

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這個(gè)模塊進(jìn)行簡單的配置就ok了

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

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

這樣整個(gè)過程我們的關(guān)注點(diǎn)都放在路由配置上去阳啥,再也不用去手動(dòng)require一堆的文件了。

簡單介紹一下上面的配置

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

path: 路徑配置捌省,可以是字符串/c/b,也可以是數(shù)組[ '/c/b' ],當(dāng)然也可以是正則表達(dá)式/\c\b/

methods: 指定請(qǐng)求的類型苫纤,可以是字符串get或者數(shù)組[ 'get', 'post' ],默認(rèn)是get方法,

controller: 匹配到路由的邏輯處理方法纲缓,c.b 表示controllerDir目錄下的c文件導(dǎo)出的b方法,a.b.c表示controllerDir目錄下的/a/b 路徑下的b文件導(dǎo)出的c方法

源碼實(shí)現(xiàn)

接下來我們逐步分析一下實(shí)現(xiàn)邏輯

可以點(diǎn)擊查看源碼

整體結(jié)構(gòu)

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

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

pure-koa-router接收

  1. routes
    1. 可以指定路由的文件目錄喊废,這樣pure-koa-router會(huì)去讀取該目錄下所有的文件 (const routes = path.join(__dirname, '../routes'))
    2. 可以指定具體的文件祝高,這樣pure-koa-router讀取指定的文件內(nèi)容作為路由配置 const routes = path.join(__dirname, '../routes/tasks.js')
    3. 可以直接指定文件導(dǎo)出的內(nèi)容 (const routes = require('../routes/index'))
  2. controllerDir、控制器的根目錄
  3. routerOptions new KoaRouter時(shí)候傳入的參數(shù)污筷,具體可以看koa-router

這個(gè)包執(zhí)行之后會(huì)返回經(jīng)過koaCompose包裝后的中間件工闺,以供koa實(shí)例添加。

參數(shù)適配

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)) {
      // 讀取目錄中的各個(gè)文件并合并
      routes = fs.readdirSync(routes).reduce((result, fileName) => {
        return result.concat(require(nodePath.join(routes, fileName)))
      }, [])
    }
  } else {
    // routes如果是字符串則必須是一個(gè)文件或者目錄的路徑
    throw new Error('routes is not a file or a directory')
  }
}

路由注冊(cè)

不管routes傳入的是文件還是目錄瓣蛀,又或者是直接導(dǎo)出的配置的內(nèi)容最后的結(jié)構(gòu)都是是這樣的

routes內(nèi)容預(yù)覽

[
  // 最基礎(chǔ)的配置
  {
    path: '/test/a',
    methods: 'post',
    controller: 'test.index.a'
  },
  // 多路由對(duì)一個(gè)控制器
  {
    path: [ '/test/b', '/test/c' ],
    controller: 'test.index.a'
  },
  // 多路由對(duì)多控制器
  {
    path: [ '/test/d', '/test/e' ],
    controller: [ 'test.index.a', 'test.index.b' ]
  },
  // 單路由對(duì)對(duì)控制器
  {
    path: '/test/f',
    controller: [ 'test.index.a', 'test.index.b' ]
  },
  // 正則
  {
    path: /\/test\/\d/,
    controller: 'test.index.c'
  }
]


主動(dòng)注冊(cè)

let router = new KoaRouter(routerOptions)
let middleware

routes.forEach((routeConfig = {}) => {
  let { path, methods = [ 'get' ], controller } = routeConfig
  // 路由方法類型參數(shù)適配
  methods = (Array.isArray(methods) && methods) || [ methods ]
  // 控制器參數(shù)適配
  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
    }
    // 對(duì)讀取到的controllerMethod進(jìn)行參數(shù)判斷陆蟆,必須是一個(gè)方法
    assert(typeof controllerMethod === 'function', 'koa middleware must be a function')

    return controllerMethod
  })
  // 最后使用router.register進(jìn)行注冊(cè)
  router.register(path, methods, middleware)


源碼的實(shí)現(xiàn)過程基本就到這里了。

結(jié)尾

pure-koa-router將路由配置和控制器分離開來惋增,使我們將注意力放在路由配置和控制器的實(shí)現(xiàn)上叠殷。希望對(duì)您能有一點(diǎn)點(diǎn)幫助。

原文地址

源碼地址

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末诈皿,一起剝皮案震驚了整個(gè)濱河市林束,隨后出現(xiàn)的幾起案子像棘,更是在濱河造成了極大的恐慌,老刑警劉巖壶冒,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件缕题,死亡現(xiàn)場離奇詭異,居然都是意外死亡胖腾,警方通過查閱死者的電腦和手機(jī)烟零,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來咸作,“玉大人锨阿,你說我怎么就攤上這事⌒院辏” “怎么了群井?”我有些...
    開封第一講書人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長毫胜。 經(jīng)常有香客問我书斜,道長,這世上最難降的妖魔是什么酵使? 我笑而不...
    開封第一講書人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任荐吉,我火速辦了婚禮,結(jié)果婚禮上口渔,老公的妹妹穿的比我還像新娘样屠。我一直安慰自己,他們只是感情好缺脉,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開白布痪欲。 她就那樣靜靜地躺著,像睡著了一般攻礼。 火紅的嫁衣襯著肌膚如雪业踢。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評(píng)論 1 305
  • 那天礁扮,我揣著相機(jī)與錄音知举,去河邊找鬼。 笑死太伊,一個(gè)胖子當(dāng)著我的面吹牛雇锡,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播僚焦,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼锰提,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起欲账,我...
    開封第一講書人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤屡江,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后赛不,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體惩嘉,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年踢故,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了文黎。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡殿较,死狀恐怖耸峭,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情淋纲,我是刑警寧澤劳闹,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站洽瞬,受9級(jí)特大地震影響本涕,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜伙窃,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一菩颖、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧为障,春花似錦晦闰、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至鞋喇,卻和暖如春窿冯,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背确徙。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留执桌,地道東北人鄙皇。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像仰挣,于是被迫代替她去往敵國和親伴逸。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理膘壶,服務(wù)發(fā)現(xiàn)错蝴,斷路器洲愤,智...
    卡卡羅2017閱讀 134,659評(píng)論 18 139
  • 原文鏈接:http://www.reibang.com/p/6b816c609669 前傳 出于興趣最近開始研究k...
    懸筆e絕閱讀 7,217評(píng)論 1 11
  • 目錄 一、項(xiàng)目結(jié)構(gòu)的核心思想 二顷锰、項(xiàng)目目錄結(jié)構(gòu) 三柬赐、資源路徑編譯規(guī)則 四、index.html 五官紫、build目錄...
    科研者閱讀 11,383評(píng)論 0 40
  • 什么是半途而廢束世? 為什么會(huì)半途而廢酝陈? 如何走出半途而廢的困境? 作為創(chuàng)業(yè)狗毁涉, 對(duì)“半途而廢”這個(gè)詞理解深刻沉帮, 因?yàn)?..
    kevinalliswell閱讀 315評(píng)論 0 0
  • 又到了一年的平安夜,街道兩旁的商店門口都掛起來了五顏六色的小彩燈贫堰,一閃一閃的與夜幕中的星星遙相輝映...
    三三兩兩2333閱讀 227評(píng)論 2 3