你可能会用到的一个路由适配器
前言
此时状态有点像上学时写作文,开篇总是"拉"不出来,憋的难受。
从背景出发
前后端分离后,前端童鞋会需要处理一些node层的工作,比如模板渲染、接口转发、部分业务逻辑等,比较常用的框架有koa、koa-router等。
现在我们需要实现这样一个需求:
- 用户访问
/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}`) })
确实是,对于很简单的需求,用上框架似乎有点浪费,但是对于以上的实现,也有缺点存在,比如
- 需要我们自己去解析路径。
- 路径的解析和逻辑的书写耦合在一块。如果未来有更多更复杂的需求需要实现,那就gg了。
所以接下来我们来试试用koa
和koa-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
处理了,但是整体的写法还是有些问题。
- 匿名函数的写法没有办法复用
- 路由配置和逻辑处理在一个文件中,没有分离,项目一大起来,同样是件麻烦事。
接下来我们再优化一下,先看一下整体的目录结构
├──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
接收
routes
可以指定路由的文件目录,这样pure-koa-router会去读取该目录下所有的文件 (const routes = path.join(__dirname, '../routes'))
- 可以指定具体的文件,这样pure-koa-router读取指定的文件内容作为路由配置 const routes = path.join(__dirname, '../routes/tasks.js')
- 可以直接指定文件导出的内容 (const routes = require('../routes/index'))
controllerDir
、控制器的根目录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
将路由配置和控制器分离开来,使我们将注意力放在路由配置和控制器的实现上。希望对您能有一点点帮助。