前言
此时状态有点像上学时写作文, 开篇总是 "拉" 不出来, 憋的难受.
源码地址 https://github.com/qianlongo/pure-koa-router
从背景出发
前后端分离后, 前端童鞋会需要处理一些 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 方法
源码实现
接下来我们逐步分析一下实现逻辑
可以点击查看源码 https://github.com/qianlongo/pure-koa-router/blob/master/index.js
整体结构
- 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 时候传入的参数, 具体可以看 https://github.com/alexmingoia/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 将路由配置和控制器分离开来, 使我们将注意力放在路由配置和控制器的实现上. 希望对您能有一点点帮助.
来源: https://juejin.im/post/5b7947fb51882542d878d94d