像Vue-Router一樣配置Node路由?
本文轉(zhuǎn)載自微信公眾號「前端胖頭魚」,作者前端胖頭魚 。轉(zhuǎn)載本文請聯(lián)系前端胖頭魚公眾號。
前言
前后端分離后,前端童鞋會需要處理一些node層的工作,比如模板渲染、接口轉(zhuǎn)發(fā)、部分業(yè)務(wù)邏輯等,比較常用的框架有koa、koa-router等。
現(xiàn)在我們需要實(shí)現(xiàn)這樣一個需求:
- 用戶訪問/fe的時候,頁面展示hello fe
 - 用戶訪問/backend的時候,頁面展示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í)是,對于很簡單的需求,用上框架似乎有點(diǎn)浪費(fèi),但是對于以上的實(shí)現(xiàn),也有缺點(diǎn)存在,比如
- 需要我們自己去解析路徑。
 - 路徑的解析和邏輯的書寫耦合在一塊。如果未來有更多更復(fù)雜的需求需要實(shí)現(xiàn),那就gg了。
 
所以接下來我們來試試用koa和koa-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處理了,但是整體的寫法還是有些問題。
- 匿名函數(shù)的寫法沒有辦法復(fù)用
 - 路由配置和邏輯處理在一個文件中,沒有分離,項(xiàng)目一大起來,同樣是件麻煩事。
 
接下來我們再優(yōu)化一下,先看一下整體的目錄結(jié)構(gòu)
- ├──app.js // 應(yīng)用入口
 - ├──controller // 邏輯處理,分模塊
 - │ ├──hello.js
 - │ ├──aaaaa.js
 - ├──middleware // 中間件統(tǒng)一注冊
 - │ ├──index.js
 - ├──routes // 路由配置,可以分模塊配置
 - │ ├──index.js
 - ├──views // 模板配置,分頁面或模塊處理,在這個例子中用不上
 - │ ├──index.html
 
預(yù)覽一下每個文件的邏輯
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)一注冊
- const routes = require('../routes')
 - module.exports = (app) => {
 - app.use(routes())
 - }
 
寫到這里你可能心里有個疑問?一個簡單的需求,被這么一搞看起來復(fù)雜了太多,有必要這樣么?
答案是:有必要,這樣的目錄結(jié)構(gòu)或許不是最合理的,但是路由、控制器、view層等各司其職,各在其位。對于以后的擴(kuò)展有很大的幫助。
不知道大家有沒有注意到路由配置這個地方
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() ])
 - }
 
每個路由對應(yīng)一個控制器去處理,很分離,很常見啊!!!這似乎也是我們平時在前端寫vue-router或者react-router的常見配置模式。
但是當(dāng)模塊多起來的來時候,這個文件夾就會變成
- const KoaRouter = require('koa-router')
 - const router = new KoaRouter()
 - const koaCompose = require('koa-compose')
 - // 下面你需要require各個模塊的文件進(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)
 - // 配置各個模塊的路由以及控制器
 - 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() ])
 - }
 
有沒有什么辦法,可以讓我們不用手動引入一個個控制器,再手動的調(diào)用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這個模塊進(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
 - }))
 
這樣整個過程我們的關(guān)注點(diǎn)都放在路由配置上去,再也不用去手動require一堆的文件了。
簡單介紹一下上面的配置
- {
 - path: '/c/b',
 - methods: 'post',
 - controller: 'c.b'
 - }
 
path: 路徑配置,可以是字符串/c/b,也可以是數(shù)組[ '/c/b' ],當(dāng)然也可以是正則表達(dá)式/\c\b/
methods: 指定請求的類型,可以是字符串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
- 可以指定路由的文件目錄,這樣pure-koa-router會去讀取該目錄下所有的文件 (const routes = path.join(__dirname, '../routes'))
 - 可以指定具體的文件,這樣pure-koa-router讀取指定的文件內(nèi)容作為路由配置 const routes = path.join(__dirname, '../routes/tasks.js')
 - 可以直接指定文件導(dǎo)出的內(nèi)容 (const routes = require('../routes/index'))
 
2.controllerDir、控制器的根目錄
3.routerOptions new KoaRouter時候傳入的參數(shù),具體可以看koa-router
這個包執(zhí)行之后會返回經(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)) {
 - // 讀取目錄中的各個文件并合并
 - 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傳入的是文件還是目錄,又或者是直接導(dǎo)出的配置的內(nèi)容最后的結(jié)構(gòu)都是是這樣的
routes內(nèi)容預(yù)覽
- [
 - // 最基礎(chǔ)的配置
 - {
 - 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
 - // 路由方法類型參數(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
 - }
 - // 對讀取到的controllerMethod進(jìn)行參數(shù)判斷,必須是一個方法
 - assert(typeof controllerMethod === 'function', 'koa middleware must be a function')
 - return controllerMethod
 - })
 - // 最后使用router.register進(jìn)行注冊
 - router.register(path, methods, middleware)
 
源碼的實(shí)現(xiàn)過程基本就到這里了。
結(jié)尾
pure-koa-router將路由配置和控制器分離開來,使我們將注意力放在路由配置和控制器的實(shí)現(xiàn)上。希望對您能有一點(diǎn)點(diǎn)幫助。















 
 
 








 
 
 
 