当前位置: 首页 > 后端技术 > Node.js

express路由管理的几种自动化方法

时间:2023-04-03 17:17:23 Node.js

前言我们平时在使用express写代码的时候,会把路由按照类别分成多个不同的文件,然后在项目的入口文件中(比如app.js)依次挂载它们,例如:constindex=require('./routes/index')constuser=require('./routes/user')//...其他路由文件app.use('/',index)app.use('/user',user)//...挂载其他路由但是当路由文件太多时,这样写会造成很多重复的代码,当我添加新的路由模块时在同时,除了自己编写路由文件外,还需要在app.js入口文件中挂载新的路由文件,不够灵活。因此,我们需要想一些办法来管理我们的路由,使其能够自动化,避免频繁修改入口。文件操作。管理思路我们的项目目录主要是这样的:├─routes├─index.js├─user.js├─sub├─index.js├─a.js├─app.js首先我们来看一下,表达的路由器的路由管理主要由三部分组成,路由方法(method)、路由路径(path)和路由处理器(handle)。一般路由方法和路由处理器都是由路由文件自己管理的,在一个路由文件中,我们经常使用这样的写法://routes/user.jsconstexpress=require('express')constrouter=express.Router()//路由方法、处理器和部分路径router.get('/',function(req,res,next){res.send('respondwitharesource')})module.exports=router然后在入口文件中添加一个通用的路由前缀:app.use('/user',require('./routes/user'))按照这个思路,我们主要处理路由路径的部分。这部分我们有两种处理方式,一种是根据路径和文件名自动生成路由的公共路径前缀,路由文件只写剩下的公共部分的路径;另一种是路径完全由路由文件自己管理,挂载时直接挂载到根路径'/'。管理实例自动生成前缀通过扫描项目目录,我们可以将项目中文件的路径转换为快速路由路径模式,并自动生成路由前缀。比如路由文件routes/sub/a.js会被转换为路由前缀/sub/a,只需要在路由文件a.js中将路径部分写在/sub/a之后即可。项目目录为:├─routes├─index.js├─user.js├─sub├─index.js├─a.js├─app.js├─helper.js主要实现代码为://helper.jsconstfs=require('fs')constpath=require('path')/***将文件名固定为前缀**@param{String}文件名*@returns{String}*/functiontransform(filename){returnfilename.slice(0,filename.lastIndexOf('.'))//分隔符转换。replace(/\\/g,'/')//索引删除。replace('/index','/')//路径header/correction.replace(/^[/]*/,'/')//路径tail/removal.replace(/[/]*$/,'')}/***文件路径转换模块名(去掉.js后缀)**@param{any}rootDir模块入口*@param{any}excludeFile入口文件要排除*@returns*/exports.scanDirModules=functionscanDirModules(rootDir,excludeFile){if(!excludeFile){//默认入口文件是目录下的index.jsexcludeFile=path.join(rootDir,'index.js')}//模块集合constmodules={}//获取路由文件目录Queue中的一级子文件letfilenames=fs.readdirSync(rootDir)while(filenames.length){//路由文件的相对路径constrelativeFilePath=filenames.shift()//路由文件的绝对路径constabsFilePath=path.join(rootDir,relativeFilePath)//排除入口文件if(absFilePath===excludeFile){continue}if(fs.statSync(absFilePath).isDirectory()){//如果是文件夹,读取获取子目录文件并将它们添加到路由文件队列中)}else{//如果是文件,将文件路径转换为路由前缀,将路由前缀和路由模块添加到模块集合中constprefix=transform(relativeFilePath)modules[prefix]=require(absFilePath)}}returnmodules}然后在路由目录的入口index文件下,添加这样一段代码(scanDirModules方法需要从之前写的helper.js文件中引入):constscanResult=scanDirModules(__dirname,__filename)for(constprefixinscanResult){if(scanResult.hasOwnProperty(prefix)){router.use(prefix,scanResult[prefix])}}在app.js入口文件中,只需要将所有路由相关的代码改成一个即可句子:app.use('/',require('./routes'))这样就完成了路由前缀的自动生成和自动路由效果展示:我们设置routes/sub/a.js的内容为://routes/sub/a.jsconstexpress=require('express')constrouter=express.Router()router.get('/',function(req,res){res.send('sub/a/')})module.exports=router挂载效果:访问结果:这种自动生成前缀的方法可以在路由目录层级不深的时候使用它效果很好,但是当目录层次很多时,就会暴露出它的缺点:阅读代码时路由路径不清晰,不能直观看到完整路径,生成前缀的灵活性不高。后者可以通过自定义导出对象和挂载方式来解决,但是前者我还没有很好的解决方案,那么我们再来看看前面提到的另一种自动化方法。直接挂载到根路径的扫描思路和前面的方法类似,不同的是在写路由文件的时候,我们需要写完整的路由路径,例如://routes/sub/a.jsconstexpress=require('express')constrouter=express.Router()router.get('/sub/a',function(req,res){res.send('sub/a/')})module.exports=路由器扫描部分的代码修改为:exports.scanDirModulesWithoutPrefix=functionscanDirModulesWithoutPrefix(rootDir,excludeFile){if(!excludeFile){//默认入口文件是目录下的index.jsexcludeFile=path.join(rootDir,'index.js')}constmodules=[]letfilenames=fs.readdirSync(rootDir)while(filenames.length){//路由文件的相对路径constrelativeFilePath=filenames.shift()//路由的绝对路径文件常量absFilePath=路径。join(rootDir,relativeFilePath)//排除入口文件if(absFilePath===excludeFile){continue}if(fs.statSync(absFilePath).isDirectory()){//如果是文件夹,读取子目录文件,添加到路由文件队列constsubFiles=fs.readdirSync(absFilePath).map(v=>path.join(absFilePath.replace(rootDir,''),v))filenames=filenames.concat(subFiles)}else{//如果是文件,则将模块加入模块数组modules.push(require(absFilePath))}}returnmodules}路由入口文件改为://获取下所有路由routes目录模块,并挂载到路由constrouteModules=scanDirModulesWithoutPrefix(__dirname,__filename)routeModules.forEach(routeModule=>{router.use(routeModule)})挂载效果:该方法可以清楚的看到路由的完整路径,在阅读代码的时候,不会因为层次太深而难以阅读,但是明显的缺点是需要编写大量路径相关的代码,路径复用性太低。那么有没有办法保证通用性呢?路径的可重用性能否保证代码的可读性?是的,我们可以使用JavaScript装饰器(Decorator)来管理路由。装饰器实现路由管理装饰器的思想来源于Java的MVC框架SpringMVC。在SpringMVC中,路由是这样写的://类上的RequestMapping注解用于设置公共路径前缀@Controller@RequestMapping("/")publicclassSampleController{//方法上的RequestMapping注解用于设置剩余路径和路由方法@RequestMapping("/",method=RequestMethod.GET)publicStringindex(){return"HelloWorld!";}//GetMapping注解相当于指定了GET访问方式的RequestMapping@GetMapping("/1")publicStringindex1(){return"HelloWorld!1";}}ES6之后,用js写类变得非常容易,我们也可以像SpringMVC路由一样在express中管理路由。关于JavaScript装饰器的思考,可以参考这两篇文章:探索ECMAScript装饰器中的装饰器DecoratorJS装饰器(Decorator)场景实战在实现之前,先简单梳理一下实现的思路。我的想法是,为了阅读方便,每个路由文件都包含一个类(Controller),每个类上有两个装饰器。第一类装饰器被添加到类中,将类下的所有方法绑定到一个公共的路由前缀;第二种装饰器是在类的方法上添加装饰器,将方法绑定到指定的HTTP请求方法和路由路径上。两个装饰器还接收其余参数作为需要绑定的中间件。除了编写装饰器本身,我们还需要一个注册函数来指定要绑定的快递对象和要扫描的路由目录。准备为了使用装饰器的特性,我们需要使用一些babel插件:$yarnaddbabel-registerbabel-preset-envbabel-plugin-transform-decorators-legacy写一个.babelrc文件:"env"],"plugins":["transform-decorators-legacy"]}在app.js中注册babel-register:require('babel-register')注册函数注册函数的写法比较简单,下面就来先写注册Function:letapp=null/***扫描并导入目录下的模块**@private*@param{string}routesDir路由目录*/functionscanDirModules(routesDir){if(!fs.existsSync(routesDir)){return}letfilenames=fs.readdirSync(routesDir)while(filenames.length){//路由文件的相对路径constrelativeFilePath=filenames.shift()//路由文件的绝对路径constabsFilePath=path.join(routesDir,relativeFilePath)if(fs.statSync(absFilePath).isDirectory()){//如果是文件夹,读取子目录文件,加入路由文件队列constsubFiles=fs.readdirSync(absFilePath).map(v=>路径。join(absFilePath.replace(routesDir,''),v))filenames=filenames.concat(subFiles)}else{//需要路由文件require(absFilePath)}}}/***注册快递服务器**@param{Object}options注册选项*@param{express.Application}options.app快递服务器对象*@param{string|Array}options.routesDir扫描routingdirectory*/functionregister(options){app=options.app//支持扫描多个路由目录constroutesDirs=typeofoptions.routesDir==='string'?[options.routesDir]:options.routesDirroutesDirs.forEach(dir=>{scanDirModules(dir)})}通过获取express的app对象注册到文件的顶层变量app中,剩下的装饰器functions可以访问app对象完成路由注册。routesDir可以是字符串也可以是字符串数组,代表需要扫描的路由目录。将它们转换成字符串数组并按顺序扫描它们。scanDirModules方法和前面的扫描方法类似,只是只需要require路由文件,不需要返回。Decorator装饰器部分分为两部分,装饰类的路由装饰器Router和其余装饰方法的请求处理装饰器(Get、Post、Put、Delete、All、Custom)。在方法装饰器的编写中,由于装饰器的行为相似,我们可以写一个抽象函数,为不同的HTTP请求方法生成不同的装饰器。抽象函数的具体代码为:/***生成HTTP请求方法对应的装饰器**@param{string}httpMethod请求方法*@param{string|RegExp}pattern请求路径*@param{Array}middlewares中间件数组*@returns{MethodDecorator}*/functiongenerateMethodDecorator(httpMethod,pattern,middlewares){returnfunction(target,methodName,descriptor){if(!target._routeMethods){target._routeMethods={}}//为self定义方法,生成对应的方法存储对象[methodName]]returndescriptor}}这里的target表示类的原型对象,methodName是需要修饰的类方法名。我们将类方法及其前置中间件组成一个数组,存放在类原型对象的_routeMethods属性中,以供类装饰器调用。要为HTTP请求方法生成装饰器,只需调用此生成器函数即可。比如生成一个GET方法装饰器,只需要:/***GET方法装饰器**@param{string|RegExp}patternroutingpath*@param{Array}middlewaresarray*@returns{MethodDecorator}*/functionGet(pattern,...middlewares){returngenerateMethodDecorator('get',pattern,middlewares)}路由装饰器(类装饰器)的代码为:/***路由器类装饰器,用在class,generate具有公共前缀和中间件的路由**@param{string|RegExp}prefix路由前缀*@param{express.RouterOptions}routerOption路由选项*@param{Array}middlewares中间件数组*@returns{ClassDecorator}*/functionRouter(prefix,routerOption,...middlewares){//判断是否有路由选项,如果没有则作为中间件使用if(typeofrouterOption==='function'){middlewares.unshift(routerOption)routerOption=undefined}/***为类生成路由器,*装饰器将在所有方法装饰器执行完后执行**@param{Function}target路由类对象*/returnfunction(target){constrouter=express.Router(routerOption)const_routeMethods=target.prototype._routeMethods//遍历并挂载路由for(constmethodin_routeMethods){if(_routeMethods.hasOwnProperty(method)){constmethods=_routeMethods[method]for(constpathinmethods){if(methods.hasOwnProperty(path)){router[method](path,...methods[path])}}}}deletetarget.prototype._routeMethodsapp.use(prefix,...middlewares,router)}}这里的target是一个类对象,装饰器处理这个类的时候,我们生成一个新的expressRouting对象,遍历放置在类对象原型上的_routeMethods属性,获取对应的路由方法、路由路径和路由处理函数,挂载到这个路由对象上。注意类装饰器的处理会放在方法装饰器之后,我们不能直接挂载在方法装饰器上,需要存储起来,在类装饰器上完成挂载工作。编写路由文件我们的路由文件也需要做很大的改动,改造成类似下面的形式://routes/sub/a.js//RouterandGetdecoratorsimport@Routerfromyourdecoratorfiles('/sub/a')classSubAController{@Get('/')index(req,res,next){res.send('sub/a/')}}module.exports=SubAController挂载效果装饰我创建了一个github仓库用于与路由器路由相关的代码,并将其发布为npm包-express-derouter。欢迎加星。综上所述,以上就是我最近思考的关于快递路由管理自动化的一些方法。装饰器的挂载方式由于js本身限制了SpringMVC的其他功能的恢复。如果你对更强大的功能感兴趣有需求的话,可以看看TypeScript的一个基于express的MVC框架——nest,相信应该更能满足你的需求。文章博客地址:快递路由管理的几种自动化方法