前言
此時(shí)狀態(tài)有點(diǎn)像上學(xué)時(shí)寫作文,開篇總是"拉"不出來溶浴,憋的難受翘鸭。
從背景出發(fā)
前后端分離后,前端童鞋會(huì)需要處理一些node層的工作戳葵,比如模板渲染就乓、接口轉(zhuǎn)發(fā)、部分業(yè)務(wù)邏輯等拱烁,比較常用的框架有koa生蚁、koa-router等。
現(xiàn)在我們需要實(shí)現(xiàn)這樣一個(gè)需求:
- 用戶訪問
/fe
的時(shí)候戏自,頁面展示hello fe - 用戶訪問
/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)存在绿店,比如
- 需要我們自己去解析路徑。
- 路徑的解析和邏輯的書寫耦合在一塊庐橙。如果未來有更多更復(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ù)用
- 路由配置和邏輯處理在一個(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è)疑問虎韵?
一個(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)邏輯
整體結(jié)構(gòu)
module.exports = ({ routes = [], controllerDir = '', routerOptions = {} }) => {
// xxx
return koaCompose([ router.routes(), router.allowedMethods() ])
})
pure-koa-router
接收
-
routes
- 可以指定路由的文件目錄喊废,這樣pure-koa-router會(huì)去讀取該目錄下所有的文件 (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'))
-
controllerDir
、控制器的根目錄 -
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)幫助。