前言我们平时在使用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
