像Vue-Router一样配置Node路由?

开发 前端 商务办公
前后端分离后,前端童鞋会需要处理一些node层的工作,比如模板渲染、接口转发、部分业务逻辑等,比较常用的框架有koa、koa-router等。

 [[440238]]

本文转载自微信公众号「前端胖头鱼」,作者前端胖头鱼 。转载本文请联系前端胖头鱼公众号。

前言

前后端分离后,前端童鞋会需要处理一些node层的工作,比如模板渲染、接口转发、部分业务逻辑等,比较常用的框架有koa、koa-router等。

现在我们需要实现这样一个需求:

  • 用户访问/fe的时候,页面展示hello fe
  • 用户访问/backend的时候,页面展示hello backend

你是不是在想,这需求俺根本不用koa、koa-router,原生的node模块就可以搞定。

  1. const http = require('http'
  2. const url = require('url'
  3. const PORT = 3000 
  4.  
  5. http.createServer((req, res) => { 
  6.   let { pathname } = url.parse(req.url) 
  7.   let str = 'hello' 
  8.  
  9.   if (pathname === '/fe') { 
  10.     str += ' fe' 
  11.   } else if (pathname === '/backend') { 
  12.     str += ' backend' 
  13.   } 
  14.  
  15.   res.end(str) 
  16. }).listen(PORT, () => { 
  17.   console.log(`app start at: ${PORT}`) 
  18. }) 

确实是,对于很简单的需求,用上框架似乎有点浪费,但是对于以上的实现,也有缺点存在,比如

  • 需要我们自己去解析路径。
  • 路径的解析和逻辑的书写耦合在一块。如果未来有更多更复杂的需求需要实现,那就gg了。

所以接下来我们来试试用koa和koa-router怎么实现

app.js

  1. const Koa = require('koa'
  2. const KoaRouter = require('koa-router'
  3.  
  4. const app = new Koa() 
  5. const router = new KoaRouter() 
  6. const PORT = 3000 
  7.  
  8. router.get('/fe', (ctx) => { 
  9.   ctx.body = 'hello fe' 
  10. }) 
  11.  
  12. router.get('/backend', (ctx) => { 
  13.   ctx.body = 'hello backend' 
  14. }) 
  15.  
  16. app.use(router.routes()) 
  17. app.use(router.allowedMethods()) 
  18.  
  19. app.listen(PORT, () => { 
  20.   console.log(`app start at: ${PORT}`) 
  21. }) 

通过上面的处理,路径的解析倒是给koa-router处理了,但是整体的写法还是有些问题。

  • 匿名函数的写法没有办法复用
  • 路由配置和逻辑处理在一个文件中,没有分离,项目一大起来,同样是件麻烦事。

接下来我们再优化一下,先看一下整体的目录结构

  1. ├──app.js // 应用入口 
  2. ├──controller // 逻辑处理,分模块 
  3. │   ├──hello.js 
  4. │   ├──aaaaa.js 
  5. ├──middleware // 中间件统一注册 
  6. │   ├──index.js 
  7. ├──routes // 路由配置,可以分模块配置 
  8. │   ├──index.js 
  9. ├──views // 模板配置,分页面或模块处理,在这个例子中用不上 
  10. │   ├──index.html 

预览一下每个文件的逻辑

app.js 应用的路口

  1. const Koa = require('koa'
  2. const middleware = require('./middleware'
  3. const app = new Koa() 
  4. const PORT = 3000 
  5.  
  6. middleware(app) 
  7.  
  8. app.listen(PORT, () => { 
  9.   console.log(`app start at: ${PORT}`) 
  10. }) 

routes/index.js 路由配置中心

  1. const KoaRouter = require('koa-router'
  2. const router = new KoaRouter() 
  3. const koaCompose = require('koa-compose'
  4. const hello = require('../controller/hello'
  5.  
  6. module.exports = () => { 
  7.   router.get('/fe', hello.fe) 
  8.   router.get('/backend', hello.backend) 
  9.  
  10.   return koaCompose([ router.routes(), router.allowedMethods() ]) 

controller/hello.js hello 模块的逻辑

  1. module.exports = { 
  2.   fe (ctx) { 
  3.     ctx.body = 'hello fe' 
  4.   }, 
  5.   backend (ctx) { 
  6.     ctx.body = 'hello backend' 
  7.   } 

middleware/index.js 中间件统一注册

  1. const routes = require('../routes'
  2.  
  3. module.exports = (app) => { 
  4.   app.use(routes()) 

写到这里你可能心里有个疑问?一个简单的需求,被这么一搞看起来复杂了太多,有必要这样么?

答案是:有必要,这样的目录结构或许不是最合理的,但是路由、控制器、view层等各司其职,各在其位。对于以后的扩展有很大的帮助。

不知道大家有没有注意到路由配置这个地方

routes/index.js 路由配置中心

  1. const KoaRouter = require('koa-router'
  2. const router = new KoaRouter() 
  3. const koaCompose = require('koa-compose'
  4. const hello = require('../controller/hello'
  5.  
  6. module.exports = () => { 
  7.   router.get('/fe', hello.fe) 
  8.   router.get('/backend', hello.backend) 
  9.  
  10.   return koaCompose([ router.routes(), router.allowedMethods() ]) 

每个路由对应一个控制器去处理,很分离,很常见啊!!!这似乎也是我们平时在前端写vue-router或者react-router的常见配置模式。

但是当模块多起来的来时候,这个文件夹就会变成

  1. const KoaRouter = require('koa-router'
  2. const router = new KoaRouter() 
  3. const koaCompose = require('koa-compose'
  4. // 下面你需要require各个模块的文件进来 
  5. const hello = require('../controller/hello'
  6. const a = require('../controller/a'
  7. const c = require('../controller/c'
  8.  
  9. module.exports = () => { 
  10.   router.get('/fe', hello.fe) 
  11.   router.get('/backend', hello.backend) 
  12.   // 配置各个模块的路由以及控制器 
  13.   router.get('/a/a', a.a) 
  14.   router.post('/a/b', a.b) 
  15.   router.get('/a/c', a.c) 
  16.   router.get('/a/d', a.d) 
  17.  
  18.   router.get('/c/a', c.c) 
  19.   router.post('/c/b', c.b) 
  20.   router.get('/c/c', c.c) 
  21.   router.get('/c/d', c.d) 
  22.  
  23.   // ... 等等     
  24.   return koaCompose([ router.routes(), router.allowedMethods() ]) 

有没有什么办法,可以让我们不用手动引入一个个控制器,再手动的调用koa-router的get post等方法去注册呢?

比如我们只需要做以下配置,就可以完成上面手动配置的功能。

routes/a.js

  1. module.exports = [ 
  2.   { 
  3.     path: '/a/a'
  4.     controller: 'a.a' 
  5.   }, 
  6.   { 
  7.     path: '/a/b'
  8.     methods: 'post'
  9.     controller: 'a.b' 
  10.   }, 
  11.   { 
  12.     path: '/a/c'
  13.     controller: 'a.c' 
  14.   }, 
  15.   { 
  16.     path: '/a/d'
  17.     controller: 'a.d' 
  18.   } 

routes/c.js

  1. module.exports = [ 
  2.   { 
  3.     path: '/c/a'
  4.     controller: 'c.a' 
  5.   }, 
  6.   { 
  7.     path: '/c/b'
  8.     methods: 'post'
  9.     controller: 'c.b' 
  10.   }, 
  11.   { 
  12.     path: '/c/c'
  13.     controller: 'c.c' 
  14.   }, 
  15.   { 
  16.     path: '/c/d'
  17.     controller: 'c.d' 
  18.   } 

然后使用pure-koa-router这个模块进行简单的配置就ok了

  1. const pureKoaRouter = require('pure-koa-router'
  2. const routes = path.join(__dirname, '../routes') // 指定路由 
  3. const controllerDir = path.join(__dirname, '../controller') // 指定控制器的根目录 
  4.  
  5. app.use(pureKoaRouter({ 
  6.   routes, 
  7.   controllerDir 
  8. })) 

这样整个过程我们的关注点都放在路由配置上去,再也不用去手动require一堆的文件了。

简单介绍一下上面的配置

  1.   path: '/c/b'
  2.   methods: 'post'
  3.   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方法

源码实现

接下来我们逐步分析一下实现逻辑

可以点击查看源码

整体结构

  1. module.exports = ({ routes = [], controllerDir = '', routerOptions = {} }) => { 
  2.   // xxx 
  3.  
  4.   return koaCompose([ router.routes(), router.allowedMethods() ]) 
  5. }) 

pure-koa-router接收

1.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'))

2.controllerDir、控制器的根目录

3.routerOptions new KoaRouter时候传入的参数,具体可以看koa-router

这个包执行之后会返回经过koaCompose包装后的中间件,以供koa实例添加。

参数适配

  1. assert(Array.isArray(routes) || typeof routes === 'string''routes must be an Array or a String'
  2. assert(fs.existsSync(controllerDir), 'controllerDir must be a file directory'
  3.  
  4. if (typeof routes === 'string') { 
  5.   routes = routes.replace('.js'''
  6.  
  7.   if (fs.existsSync(`${routes}.js`) || fs.existsSync(routes)) { 
  8.     // 处理传入的是文件 
  9.     if (fs.existsSync(`${routes}.js`)) { 
  10.       routes = require(routes) 
  11.     // 处理传入的目录   
  12.     } else if (fs.existsSync(routes)) { 
  13.       // 读取目录中的各个文件并合并 
  14.       routes = fs.readdirSync(routes).reduce((result, fileName) => { 
  15.         return result.concat(require(nodePath.join(routes, fileName))) 
  16.       }, []) 
  17.     } 
  18.   } else { 
  19.     // routes如果是字符串则必须是一个文件或者目录的路径 
  20.     throw new Error('routes is not a file or a directory'
  21.   } 

路由注册

不管routes传入的是文件还是目录,又或者是直接导出的配置的内容最后的结构都是是这样的

routes内容预览

  1.   // 最基础的配置 
  2.   { 
  3.     path: '/test/a'
  4.     methods: 'post'
  5.     controller: 'test.index.a' 
  6.   }, 
  7.   // 多路由对一个控制器 
  8.   { 
  9.     path: [ '/test/b''/test/c' ], 
  10.     controller: 'test.index.a' 
  11.   }, 
  12.   // 多路由对多控制器 
  13.   { 
  14.     path: [ '/test/d''/test/e' ], 
  15.     controller: [ 'test.index.a''test.index.b' ] 
  16.   }, 
  17.   // 单路由对对控制器 
  18.   { 
  19.     path: '/test/f'
  20.     controller: [ 'test.index.a''test.index.b' ] 
  21.   }, 
  22.   // 正则 
  23.   { 
  24.     path: /\/test\/\d/, 
  25.     controller: 'test.index.c' 
  26.   } 

主动注册

  1. let router = new KoaRouter(routerOptions) 
  2. let middleware 
  3.  
  4. routes.forEach((routeConfig = {}) => { 
  5.   let { path, methods = [ 'get' ], controller } = routeConfig 
  6.   // 路由方法类型参数适配 
  7.   methods = (Array.isArray(methods) && methods) || [ methods ] 
  8.   // 控制器参数适配 
  9.   controller = (Array.isArray(controller) && controller) || [ controller ] 
  10.  
  11.   middleware = controller.map((controller) => { 
  12.     // 'test.index.c' => [ 'test''index''c' ] 
  13.     let controllerPath = controller.split('.'
  14.     // 方法名称 c 
  15.     let controllerMethod = controllerPath.pop() 
  16.  
  17.     try { 
  18.       // 读取/test/index文件的c方法 
  19.       controllerMethod = require(nodePath.join(controllerDir, controllerPath.join('/')))[ controllerMethod ] 
  20.     } catch (error) { 
  21.       throw error 
  22.     } 
  23.     // 对读取到的controllerMethod进行参数判断,必须是一个方法 
  24.     assert(typeof controllerMethod === 'function''koa middleware must be a function'
  25.  
  26.     return controllerMethod 
  27.   }) 
  28.   // 最后使用router.register进行注册 
  29.   router.register(path, methods, middleware) 

源码的实现过程基本就到这里了。

结尾

pure-koa-router将路由配置和控制器分离开来,使我们将注意力放在路由配置和控制器的实现上。希望对您能有一点点帮助。

 

责任编辑:武晓燕 来源: 前端胖头鱼
相关推荐

2023-04-05 14:19:07

FlinkRedisNoSQL

2022-09-09 18:59:28

Vue类型枚举

2022-02-02 21:29:39

路由模式Vue-Router

2013-12-17 09:02:03

Python调试

2013-12-31 09:19:23

Python调试

2023-05-23 13:59:41

RustPython程序

2022-12-21 15:56:23

代码文档工具

2015-03-16 12:50:44

2021-05-20 08:37:32

multiprocesPython线程

2013-08-22 10:17:51

Google大数据业务价值

2021-11-11 08:20:47

Vue 技巧 开发工具

2015-02-05 13:27:02

移动开发模块SDK

2011-01-18 10:45:16

乔布斯

2012-06-08 13:47:32

Wndows 8Vista

2012-03-21 10:15:48

RIM越狱

2017-05-22 10:33:14

PythonJuliaCython

2021-09-07 10:29:11

JavaScript模块CSS

2012-06-14 09:48:11

OpenStackLinux

2015-04-09 11:27:34

2011-10-24 13:07:00

点赞
收藏

51CTO技术栈公众号