大家好,我是若川。文末留言送书,具体规则文末说明。另外为了鼓励大家多写源码共读笔记,我会在写了5次及以上笔记的作者群里也抽奖送这本书。以后也会有更多福利倾斜。
导读:Koa是一个Node框架,在Node开源社区中,Koa的使用范围非常广,掌握Koa的使用方法,就能轻松应对业界的一些BFF框架。本文介绍Koa在实际的业务场景中,路由如何做分割。
作者:刘江虹
来源:华章计算机(hzbook_jsj)
在实际的复杂业务场景中,简单的路由堆砌会使得路由文件越来越大,随着后续的项目不断迭代,开发人员的不断更替,如果所有的路由都写在一个文件里,会使得路由模块变得越来越难维护。
那么,Koa的项目要如何去解决路由难维护的问题呢?对于这个问题,由抖音电商前端架构师撰写的《Koa开发:入门、进阶与实战》给出了很好的解决方案,下面让我们结合这本书的内容来详细看一看Koa中路由的使用技巧。
在介绍路由分割以及文件路由之前,我们回忆一下koa-router这个中间件的使用。假如需要两个路由,一个是获取货物信息,一个是获取用户信息,那用koa-router实现的代码应该是这样的:
const Koa = require('koa')const app = new Koa()const Router = require('koa-router')const router = new Router()router.get('/goods/getInfo', async ( ctx ) => {ctx.body = 'this is koa book.'})router.get('/user/getInfo', async ( ctx ) => {ctx.body = 'my name is liujianghong.'})app.use(router.routes())app.listen(4000, () => {console.log('server is running, port is 4000')})
这样的写法相信读者应该已经掌握了,但这样写其实有个弊端,如果在一个实际的项目中,Node.js层的接口可能会很多,所有的路由都放在一个文件里,最终会变得越来越难维护,那实战中我们应该如何维护好路由的编写呢?本节将阐述两种方案。
1、路由分割
所谓路由分割,就是把所有路由按照类别划分,分别维护在不同的文件里。在实际的项目中,通常情况下,一个项目是由多人开发维护的,比如张三只维护货物相关的路由,李四只维护用户相关的路由,如果让两人在一个文件里维护,那随着项目越来越大,接口越来越多,难免会出现不好维护的情况。所以,路由分割就一定程度上解决了这样的问题,让路由易迭代、易维护。
本文提到的实例中有两个类型的路由,一个是货物的,一个是用户的,那么接下来,我们就对这两类路由进行分割。首先,按照类型可以把不同的路由写在不同的文件里,货物的路由文件代码如下:
// routers/goods.jsconst Router = require('koa-router')const router = new Router()// 设置路由前缀router.prefix('/goods')router.get('/getInfo', (ctx, next)=>{ctx.body = "this is koa book."})module.exports = router
用户的路由文件代码如下:
// routers/user.jsconst Router = require('koa-router')const router = new Router()router.prefix('/user')router.get('/getInfo', (ctx, next)=>{ctx.body = "my name is liujianghong."})module.exports = router
每个路由文件里面都使用了一个路由前缀的设置,这样方便分类。每个文件封装了不同类型的路由,接下来要做的就是把这些路由进行整合。Koa源码中有一个非常重要的实现是中间件的合并,其中就使用了koa-compose包,读者可以返回《Koa开发:入门、进阶与实战》一书的第3章复习一下。路由的合并也会用到koa-compose来进行实现,代码如下:
// routers/index.jsconst compose = require('koa-compose')const glob = require('glob')const { resolve } = require('path')registerRouter = () => {let routers = [];// 递归式获取当前文件夹下所有的js文件glob.sync(resolve(__dirname, './', '**/*.js'))// 排除index.js文件,因为这个文件不是具体的路由文件.filter(value => (value.indexOf('index.js') === -1)).forEach(router => {routers.push(require(router).routes())routers.push(require(router).allowedMethods())})return compose(routers)}module.exports = registerRouter
这里可以使用koa-compose来对koa-router进行整合,是因为koa-router里面的routers方法和allowedMethods方法和我们平时用的中间件里面的回调是一样的,读者如果感兴趣的话,可以看一下koa-router的源码。最后实现一个简单的server,即把整合后的路由引进来,代码如下:
// app.jsconst Koa = require('koa')const registerRouter = require('./routers')const app = new Koa()app.use(registerRouter())app.listen(4000, () => {console.log('server is running, port is 4000')})
运行app.js,我们在浏览器访问http://127.0.0.1:4000/goods/getInfo,效果如图1所示。
图1 访问货物路由运行结果
访问http://127.0.0.1:4000/user/getInfo,效果如图2所示。
图2 访问用户路由运行结果
2、文件路由
根据文件路径来匹配路由,也是实际的项目可能采取的一种方式,我们先了解一下什么是文件路由,比如,现在有这样一个项目,组织结构如图3所示。
图3 文件路由的项目结构
actions目录下的内容就是匹配路由的,比如前端有一个GET请求http://127.0.0.1:4000/goods/getInfo,那么最终会匹配到actions目录下goods/getInfo.js文件,最终会执行getInfo.js里面的逻辑。这么设计有以下几点优势:
● 依据项目中文件目录就能了解本项目都有哪些路由,不用查看路由文件,非常方便。
● 用文件路径来组织路由,对用户非常友好,便于开发。
接下来我们详细分析这种文件路由该如何实现。总共有两个步骤:第一步,定义goods/getInfo.js和user/getInfo.js两个文件内容,主要是定义一些属性,包括请求的方法类型(GET、POST等)、执行的回调;第二步,把请求的path映射到对应的文件路径上,当请求过来后,能够执行对应的文件内容。接下来看代码如何实现。
1)定义两个文件内容
actions/goods/getInfo.js文件的定义代码如下:
// actions/goods/getInfo.jsmodule.exports = {method: 'GET', handler: (ctx) => {ctx.body = "this is koa book."} }
actions/user/getInfo.js文件的定义代码如下:
// actions/user/getInfo.js module.exports = { method: 'GET', handler: (ctx) => { ctx.body = "my name is liujianghong." } }
两个文件都定义了两个属性,一个是method,一个是handler。method指的是请求的类型,这里method的配置主要是为了映射到唯一请求,比如请求路径都是/goods/getInfo,那方法类型既可以是GET请求,也可以POST请求,两个请求是不一样的。hander方法就是一个回调方法,用来处理相应的业务逻辑。
2)请求的path映射到对应的文件
首先请求的path可通过context对象来获取,比较方便,主要问题是文件路径如何处理。其实我们可以通过glob这个包来获取所有的文件,然后对路径再做相应的处理即可,代码如下:
const glob = require('glob')const path = require('path')const Koa = require('koa')const app = new Koa()// actions的绝对路径const basePath = path.resolve(__dirname, './actions')// 获取actions目录下所有的js文件,并返回其绝对路径const filesList = glob.sync(path.resolve(__dirname, './actions', '**/*.js'))// 文件路由映射表let routerMap = {}filesList.forEach(item => {// 解构的方式获取,当前文件导出对象中的method属性和handler属性const { method, handler } = require(item)// 获取和actions目录的相对路径,例如:goods/getInfo.jsconst relative = path.relative(basePath, item)// 获取文件后缀.jsconst extname = path.extname(item)// 剔除后缀.js,并在前面加一个"/",例如:/goods/getInfoconst fileRouter = '/' + relative.split(extname)[0]// 连接method,形成一个唯一请求,例如: _GET_/goods/getInfoconst key = '_' + method + '_' + fileRouter// 保存在路由表里routerMap[key] = handler})app.use(async (ctx, next) => {const { path, method } = ctx// 构建和文件路由匹配的形式:_GET_路由const key = '_' + method + '_' + path// 如果匹配到,就执行对应到handlerif (routerMap[key]) {routerMap[key](ctx)} else {ctx.body = 'no this router'}})app.listen(4000, () => {console.log('server is running, port is 4000')})
文件路由书写起来比较优雅,并且可以做到高度可配置,这样对每个请求可以实行个性化定制,我们在Koa实战中也会使用这种方式来做路由,到时候详细讲述企业级别的BFF架构中文件路由该如何设计。
无论是通过中间件的路由分割还是通过文件的路由分割,都在一定程度上能够优化路由的组织方式,方便后续的需求迭代。如果您想要了解更多有关Koa的高级应用,如用户鉴权机制、数据存储、进程管理等,推荐您详细阅读刘江虹老师的新作《Koa开发:入门、进阶与实战》。
作者介绍:刘江虹,字节跳动抖音电商前端架构师,目前主要负责业务架构中工程化等相关方向,拥有多年前端架构工作经验。独立开发过一款可对标Egg的BFF企业级框架,支撑公司线上服务超1000个,全栈前端技术专家,具有丰富的Node实战经验。著有畅销书《React.js实战》。
抽奖规则:在留言区留言为什么想要这本书,随机抽取「2位」理由充分 && 关注比较久 && 留言互动相对多的小伙伴。包邮送出本书。
另外为了鼓励大家多写源码共读笔记,我会在写了5次及以上笔记的作者群里也抽奖送这本书。以后也会有更多福利倾斜。
开奖时间4月18日(周一)晚8点。中奖者在开奖后1天内与我取得联系,否则视为放弃。我会在微信留言区回复中奖人。
中奖者开奖前必须是我的微信好友,且需是前端。羊毛党绕路。
总之最终解释权归我。
记得扫码加我微信 ruochuan12 , 参加源码共读。
点击阅读全文购买