express路由管理的几种自动化方法分享-js教程-PHP中文网

我们平时在使用express写代码的过程中,会根据类别,将路由分为多个不同的文件,然后在项目的入口文件(例如app.js)中将其依次挂载,例如:
1

2

3

4

5

6

7

const index = require('./routes/index')

const user = require('./routes/user')

// ...其他路由文件

 

app.use('/', index)

app.use('/user', user)

// ...挂载其他路由

但是当路由文件过多时,这样写会多出很多重复性的代码,而且当我添加一个新的路由模块时,除了编写路由文件本身,还需要到app.js入口文件中将新路由文件挂载上去,不够灵活,因此,我们需要想一些办法来管理我们的路由,使其能够自动化,免除频繁修改入口文件的操作。

管理思路
我们的项目目录主要是这样的:

1

2

3

4

5

6

7

├─routes

  ├─index.js

  ├─user.js

  ├─sub

    ├─index.js

    ├─a.js

├─app.js

首先,我们来看一下,express的路由管理主要由三部分组成,路由方法(method)、路由路径(path)和路由处理器(handle),一般情况下,路由方法和路由处理器是由路由文件自己来管理,在一个路由文件中,我们经常使用这样的写法:

1

2

3

4

5

6

7

8

9

10

// routes/user.js

const express = require('express')

const router = express.Router()

 

// 路由的方法,处理器和部分路径

router.get('/', function (req, res, next) {

  res.send('respond with a resource')

})

 

module.exports = router

然后在入口文件中添加上共通的路由前缀:

1

app.use('/user', require('./routes/user'))

根据这种思路,我们主要处理的就是路由路径这个部分。在这个部分我们有两种处理方式,一种是根据路径和文件名自动生成路由的共通路径前缀,路由文件只编写剩余不共通部分的路径;还有一种则是路径完全由路由文件自己来管理,在挂载时直接挂载到根路径'/'上。

管理实例
自动生成前缀
我们通过扫描项目目录,可以将文件在项目中的路径转化为express的路由路径模式,自动生成路由前缀,例如路由文件routes/sub/a.js就会为转化成路由前缀/sub/a,路由文件a.js中只要编写/sub/a后面的路径部分即可。

项目目录为:

1

2

3

4

5

6

7

8

├─routes

  ├─index.js

  ├─user.js

  ├─sub

    ├─index.js

    ├─a.js

├─app.js

├─helper.js

主要的实现代码为:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

// helper.js

const fs = require('fs')

const path = require('path')

 

/**

 * 将文件名修正为前缀

 *

 * @param {String} filename

 * @returns {String}

 */

function transform (filename) {

  return filename.slice(0, filename.lastIndexOf('.'))

    // 分隔符转换

    .replace(/\\/g, '/')

    // index去除

    .replace('/index', '/')

    // 路径头部/修正

    .replace(/^[/]*/, '/')

    // 路径尾部/去除

    .replace(/[/]*$/, '')

}

 

/**

 * 文件路径转模块名(去.js后缀)

 *

 * @param {any} rootDir 模块入口

 * @param {any} excludeFile 要排除的入口文件

 * @returns

 */

exports.scanDirModules = function scanDirModules (rootDir, excludeFile) {

  if (!excludeFile) {

    // 默认入口文件为目录下的 index.js

    excludeFile = path.join(rootDir, 'index.js')

  }

  // 模块集合

  const modules = {}

  // 获取目录下的第一级子文件为路由文件队列

  let filenames = fs.readdirSync(rootDir)

  while (filenames.length) {

    // 路由文件相对路径

    const relativeFilePath = filenames.shift()

    // 路由文件绝对路径

    const absFilePath = path.join(rootDir, relativeFilePath)

    // 排除入口文件

    if (absFilePath === excludeFile) {

      continue

    }

    if (fs.statSync(absFilePath).isDirectory()) {

      // 是文件夹的情况下,读取子目录文件,添加到路由文件队列中

      const subFiles = fs.readdirSync(absFilePath).map(v => path.join(absFilePath.replace(rootDir, ''), v))

      filenames = filenames.concat(subFiles)

    } else {

      // 是文件的情况下,将文件路径转化为路由前缀,添加路由前缀和路由模块到模块集合中

      const prefix = transform(relativeFilePath)

      modules[prefix] = require(absFilePath)

    }

  }

  return modules

}

然后,在路由目录的入口index文件下,加入这么一段代码(scanDirModules方法需要从之前编写的helper.js文件中引入):

1

2

3

4

5

6

const scanResult = scanDirModules(__dirname, __filename)

for (const prefix in scanResult) {

  if (scanResult.hasOwnProperty(prefix)) {

    router.use(prefix, scanResult[prefix])

  }

}

在app.js入口文件中只需要将所有路由相关代码改成一句:

1

app.use('/', require('./routes'))

这样就完成了路由前缀的自动生成和路由自动挂载了。

效果展示:

我们将routes/sub/a.js的内容定为:

1

2

3

4

5

6

7

8

9

// routes/sub/a.js

const express = require('express')

const router = express.Router()

 

router.get('/', function (req, res) {

  res.send('sub/a/')

})

 

module.exports = router

挂载效果:



访问结果:



这种自动生成前缀的方法,在路由目录层级不深时,可以起到很好的作用,但是当目录层级较多时,就会暴露出缺点:阅读代码时路由路径不明确,不能直观地看到完整路径,而且生成前缀的灵活性不高。

后者可以使用自定义导出对象和挂载方式的方法来解决,但是前者我暂时没有什么好的解决方法,因此我们来看一下之前提到的另一种自动化方法。

直接挂载到根路径
这种方法的扫描思路和前一种方法相似,不同之处在于,在编写路由文件的时候,我们需要写完整路由的路径,例如:

1

2

3

4

5

6

7

8

9

// routes/sub/a.js

const express = require('express')

const router = express.Router()

 

router.get('/sub/a', function (req, res) {

  res.send('sub/a/')

})

 

module.exports = router

扫描部分的代码修改为:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

exports.scanDirModulesWithoutPrefix = function scanDirModulesWithoutPrefix (rootDir, excludeFile) {

  if (!excludeFile) {

    // 默认入口文件为目录下的 index.js

    excludeFile = path.join(rootDir, 'index.js')

  }

  const modules = []

  let filenames = fs.readdirSync(rootDir)

  while (filenames.length) {

    // 路由文件相对路径

    const relativeFilePath = filenames.shift()

    // 路由文件绝对路径

    const absFilePath = path.join(rootDir, relativeFilePath)

    // 排除入口文件

    if (absFilePath === excludeFile) {

      continue

    }

    if (fs.statSync(absFilePath).isDirectory()) {

      // 是文件夹的情况下,读取子目录文件,添加到路由文件队列中

      const subFiles = fs.readdirSync(absFilePath).map(v => path.join(absFilePath.replace(rootDir, ''), v))

      filenames = filenames.concat(subFiles)

    } else {

      // 是文件的情况下,将模块添加到模块数组中

      modules.push(require(absFilePath))

    }

  }

  return modules

}

路由入口文件修改为:

1

2

3

4

5

// 获取 routes 目录下所有路由模块,并挂载到一个路由上

const routeModules = scanDirModulesWithoutPrefix(__dirname, __filename)

routeModules.forEach(routeModule => {

  router.use(routeModule)

})

挂载效果:



这种方法可以明确的看到路由的完整路径,在阅读代码时不会出现因为层级过深而导致出现阅读困难的情况,但是明显的缺点就是需要编写大量的路径相关代码,路径重用性又太低。

那么有没有一种方法,既能保证共通路径的重用性,又能保证代码的可阅读性呢?

有,我们可以用JavaScript的装饰器(Decorator)来进行路由的管理。

装饰器实现路由管理
装饰器的思路来自于Java的MVC框架Spring MVC,在Spring MVC中,路由的编写方式是这样的:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

// 类上的 RequestMapping 注解用来设置共通的路径前缀

@Controller

@RequestMapping("/")

public class SampleController {

 

  // 方法上的 RequestMapping 注解用来设置剩余路径和路由方法

  @RequestMapping("/", method=RequestMethod.GET)

  public String index() {

    return "Hello World!";

  }

 

  // GetMapping 注解相当于已经指定了GET访问方法的 RequestMapping

  @GetMapping("/1")

  public String index1() {

    return "Hello World!1";

  }

}

在ES6之后,在js中编写类已经变得非常容易,我们也可以仿照 Spring MVC 的路由方式来管理express中的路由。

思路整理
关于JavaScript的装饰器,可以参考这两篇文章:

探寻 ECMAScript 中的装饰器 Decorator

JS 装饰器(Decorator)场景实战

在进行实现之前,我们先简单整理一下实现的思路。我的思路是,为了阅读方便,每一个路由文件包括一个类(Controller),每个类上有两种装饰器。

第一种装饰器是在类上添加的,用来将这个类下面的所有方法绑定到一个共通的路由前缀上;

而第二种装饰器则是添加到类中的方法上的,用来将方法绑定到一个指定的HTTP请求方法和路由路径上。

这两种装饰器也都接收剩余的参数,作为需要绑定的中间件。

除了编写装饰器本身之外,我们还需要一个注册函数,用来指定需要绑定的express对象和需要扫描的路由目录。

准备工作
为了使用装饰器这个特性,我们需要使用一些babel插件:

1

$ yarn add babel-register babel-preset-env babel-plugin-transform-decorators-legacy

编写.babelrc文件:

1

2

3

4

5

6

7

8

{

  "presets": [

    "env"

  ],

  "plugins": [

    "transform-decorators-legacy"

  ]

}

在app.js中注册babel-register:

1

require('babel-register')

注册函数编写
注册函数的功能较为简单,因此我们先来编写注册函数:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

let app = null

 

/**

 * 扫描并引入目录下的模块

 *

 * @private

 * @param {string} routesDir 路由目录

 */

function scanDirModules (routesDir) {

  if (!fs.existsSync(routesDir)) {

    return

  }

  let filenames = fs.readdirSync(routesDir)

  while (filenames.length) {

    // 路由文件相对路径

    const relativeFilePath = filenames.shift()

    // 路由文件绝对路径

    const absFilePath = path.join(routesDir, relativeFilePath)

    if (fs.statSync(absFilePath).isDirectory()) {

      // 是文件夹的情况下,读取子目录文件,添加到路由文件队列中

      const subFiles = fs.readdirSync(absFilePath).map(v => path.join(absFilePath.replace(routesDir, ''), v))

      filenames = filenames.concat(subFiles)

    } else {

      // require路由文件

      require(absFilePath)

    }

  }

}

 

/**

 * 注册express服务器

 *

 * @param {Object} options 注册选项

 * @param {express.Application} options.app express服务器对象

 * @param {string|Array} options.routesDir 要扫描的路由目录

 */

function register (options) {

  app = options.app

  // 支持扫描多个路由目录

  const routesDirs = typeof options.routesDir === 'string' ? [options.routesDir] : options.routesDir

  routesDirs.forEach(dir => {

    scanDirModules(dir)

  })

}

通过获取express的app对象,将其注册到文件的顶级变量app,可以让其余的装饰器函数访问到app对象从而完成路由注册。

routesDir可以是字符串也可以是字符串的数组,代表了需要扫描的路由目录,将其转化为字符串数组后依次进行扫描。

scanDirModules方法与之前的扫描方法类似,只是这里只需要将路由文件require进来就行,不需要返回。

装饰器编写
装饰器部分分为两部分,装饰类的路由装饰器Router和其余装饰方法的请求处理装饰器(Get, Post, Put, Delete, All, Custom)。

在方法装饰器的编写上,由于装饰器的行为相似,因此我们可以编写一个抽象函数,用来生成不同HTTP请求方法的不同装饰器。

抽象函数的具体代码为:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

/**

 * 生成对应HTTP请求方法的装饰器

 *

 * @param {string} httpMethod 请求方法

 * @param {string|RegExp} pattern 请求路径

 * @param {Array} middlewares 中间件数组

 * @returns {MethodDecorator}

 */

function generateMethodDecorator (httpMethod, pattern, middlewares) {

  return function (target, methodName, descriptor) {

    if (!target._routeMethods) {

      target._routeMethods = {}

    }

    // 为自定义方法生成对应的方法存储对象

    if (!target._routeMethods[httpMethod]) {

      target._routeMethods[httpMethod] = {}

    }

    target._routeMethods[httpMethod][pattern] = [...middlewares, target[methodName]]

    return descriptor

  }

}

这里的target表示类的原型对象,methodName则是需要装饰的类方法的名称,我们将类方法和它的前置中间件组成一个数组,存储到类原型对象上的_routeMethods属性中,以便类装饰器调用。

要生成一个HTTP请求方法的装饰器,只需要调用这个生成函数即可。

例如生成一个GET方法的装饰器,则只需要:

1

2

3

4

5

6

7

8

9

10

/**

 * GET 方法装饰器

 *

 * @param {string|RegExp} pattern 路由路径

 * @param {Array} middlewares 中间件数组

 * @returns {MethodDecorator}

 */

function Get (pattern, ...middlewares) {

  return generateMethodDecorator('get', pattern, middlewares)

}

路由装饰器(类装饰器)的代码为:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

/**

 * Router 类装饰器,使用在 class 上,生成一个带有共通前缀和中间件的路由

 *

 * @param {string|RegExp} prefix 路由前缀

 * @param {express.RouterOptions} routerOption 路由选项

 * @param {Array} middlewares 中间件数组

 * @returns {ClassDecorator}

 */

function Router (prefix, routerOption, ...middlewares) {

  // 判断是否有路由选项,没有则当做中间件来使用

  if (typeof routerOption === 'function') {

    middlewares.unshift(routerOption)

    routerOption = undefined

  }

 

  /**

   * 为类生成一个 router,

   * 该装饰器会在所有方法装饰器执行完后才执行

   *

   * @param {Function} target 路由类对象

   */

  return function (target) {

    const router = express.Router(routerOption)

    const _routeMethods = target.prototype._routeMethods

    // 遍历挂载路由

    for (const method in _routeMethods) {

      if (_routeMethods.hasOwnProperty(method)) {

        const methods = _routeMethods[method]

        for (const path in methods) {

          if (methods.hasOwnProperty(path)) {

            router[method](path, ...methods[path])

          }

        }

      }

    }

    delete target.prototype._routeMethods

    app.use(prefix, ...middlewares, router)

  }

}

这里的target是类对象,当装饰器对类进行处理时,我们生成一个新的express路由对象,将放置在类对象原型上的_routeMethods属性进行遍历,获取到对应的路由方法、路由路径和路由处理函数,并挂载到这个路由对象上。

需要注意,类装饰器的处理会放在方法装饰器之后进行,因此我们不能直接在方法装饰器上进行挂载,需要将其存储起来,在类装饰器上完成挂载工作。

编写路由文件
我们的路由文件也需要进行大幅度的改动,将其转化为下面类似的形式:

1

2

3

4

5

6

7

8

9

10

11

// routes/sub/a.js

// Router 和 Get 装饰器从你的装饰器文件中引入

@Router('/sub/a')

class SubAController {

  @Get('/')

  index (req, res, next) {

    res.send('sub/a/')

  }

}

 

module.exports = SubAController

挂载效果


用装饰器编写路由的相关代码我已经单独建立了一个github仓库,并发布成了一个npm包——express-derouter,欢迎各位star。

总结
以上就是我最近所思考的有关于express路由管理自动化的几种方法,其中装饰器挂载的方式由于js自身原因,在还原Spring MVC的其他功能上有所限制,如果你对更加强大的功能有要求的话,可以看看TypeScript基于express的一个MVC框架——nest,相信它应该更能满足你的需求。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/248916.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

dotnet不是内部或外部的命令,也不是可运行的程序或批处理文件

>>这台电脑>>属性>>高级系统设置>>环境变量>>系统变量>>Path>>编辑>> 变量值中添加 %SystemRoot%\system32;%SystemRoot%;%SystemRoot%\System32\Wbem; 即可。转载于:https://www.cnblogs.com/ZCrystal/p/10894591.html

转载 vue的基础使用

转载https://www.cnblogs.com/majj/p/9957597.html#top vue的介绍 前端框架和库的区别nodejs的简单使用vue的起步指令系统组件的使用过滤器的使用watch和computed钩子函数渐进式的JavaScript框架 vue react angualr作者:尤雨溪 facebook 谷歌公…

Express实现路由分发控制、RESTful API

Express实现路由分发控制、RESTful API 标签(空格分隔): Node.js 最近在用Express作为自己的WEB应用框架,其中最为迷惑的就是Express的路由控制和分发,在网上搜了很多资料,但是大部分的资料都是将Express的…

springboot运行jar包时候加载指定目录的其他jar支持包

最近发生一个小故障,调试好的项目,发布成jar包后无法找到oracle的驱动,研究了一下解决了。记录一下。写了一个run.sh脚本 #!/bin/bash cd ~ cd app nohup java -Djava.ext.dirs./lib -Doracle.jdbc.thinLogonCapabilityo3 -jar -Xms512m -Xmx…

OpenLayers3关于Map Export的Canvas跨域

一 Canvas跨域现象 地图导出是地图中常用的功能,并且OpenLayers3中也提供了两个地图导出的例子:http://openlayers.org/en/latest/examples/export-map.html http://openlayers.org/en/latest/examples/export-pdf.html。 看到这两个例子我们都很兴奋,直…

typescript-koa-postgresql 实现一个简单的rest风格服务器 —— 连接 postgresql 数据库...

接上一篇,这里使用 sequelize 来连接 postgresql 数据库 1、安装 sequelize,数据库驱动 pg yarn add sequelize sequelize-typescript pg reflect-metadata 2、新建配置文件夹 conf 及 配置文件 db.conf.ts /*** name: 数据库配置* param : undefined* r…

SmartGit使用教程

说明 官网的客户端是命令行形式的,有兴趣可以去了解下。这里针对图形界面的smartgit做一个使用说明。 软件下载和安装 下载地址[2016.12.16测试可以] 按需选择,如果不知道自己电脑是什么系统的,那我没话说了https://www.syntevo.com/smartgit/ 安装 …

jquery 下拉框 select2 运用 笔记

1,添加select2 样式 参考&#xff08;https://select2.org/ &#xff09; 2,Html: <select id"txtType" name"Type" class"form-control select2" multiple"multiple"> </select> 3,jquery section scripts{ $(documen…

Asp.Net MVC中Action跳转小结

首先我觉得action的跳转大致可以这样归一下类&#xff0c;跳转到同一控制器内的action和不同控制器内的action、带有参数的action跳转和不带参数的action跳转。 一、RedirectToAction(“Index”);//一个参数时在本Controller下&#xff0c;不传入参数。 二、RedirectToAction(A…

获取浏览器屏幕高度(js,jq) - 进击的小牛牛 - 博客园

javascript IE中&#xff1a; document.body.clientWidth > BODY对象宽度 document.body.clientHeight > BODY对象高度 document.documentElement.clientWidth > 可见区域宽度 document.documentElement.clientHeight > 可见区域高度 FireFox中&#xff1a; docum…

589-N叉树的前序遍历

N阶二叉树&#xff1a; class Tree {public int val;public List<Tree> children;public Tree() {}public Tree(int _val, List<Tree> _children) {val _val;children _children;}}迭代法遍历&#xff1a;public List<Integer> preorder(Tree root) {List&…

解析URL参数

1、拿到一个完整url后&#xff0c;如何解析该url得到里面的参数。 /*** 解析url中参数信息&#xff0c;返回参数数组*/ function convertUrlQuery($query) {$queryParts explode(&, $query);$params array();foreach ($queryParts as $param) {$item explode(, $param);…

第一个爬虫和测试

Python测试函数的方法之一是用&#xff1a;try……except def gameover(a,b):if a>10 and b>10 and abs(a-b)2:return Trueif (a>11 and b<11) or (a<11 and b>11):return Truereturn False try:agameover(10,11)print(a) except:print("Error") g…

JS组件系列——Bootstrap 树控件使用经验分享 - 懒得安分 - 博客园

前言&#xff1a;很多时候我们在项目中需要用到树&#xff0c;有些树仅仅是展示层级关系&#xff0c;有些树是为了展示和编辑层级关系&#xff0c;还有些树是为了选中项然后其他地方调用选中项。不管怎么样&#xff0c;树控件都是很多项目里面不可或缺的组件之一。今天&#xf…

算法第4章实践报告

1.实践题目&#xff1a;最小删数问题 2.问题描述&#xff1a;给定n位正整数a&#xff0c;去掉其中任意k≤n 个数字后&#xff0c;剩下的数字按原次序排列组成一个新 的正整数。对于给定的n位正整数a和正整数 k&#xff0c;设计一个算法找出剩下数字组成的新数最 小的删数方案。…

Vue 下拉刷新及无限加载组件 - 有你便是晴天 - 博客园

原文 https://github.com/wangdahoo/vue-scroller 主题 Vue.js Vue Scroller Vue Scroller is a foundational component ofVonic UI. In purpose of smooth scrolling, pull to refresh and infinite loading. Demo Change Logs v0.3.9 add getPosition method for scr…

用java编程实现集合的交、并、差和补运算

一、实验目的 掌握集合的交、并、差和补运算&#xff0c;并且使用计算机编程实现。 二、实验内容 通过编程实现求给定集合A和B的并集C&#xff08;CA∪B&#xff09;、交集C&#xff08;CA∩B&#xff09;、差集C&#xff08;CA-B&#xff09;、补集~CE-C的运算。 三、实验要求…

node.js项目中常量的配置 - 个人文章 - SegmentFault 思否

在项目中&#xff0c;我们常将一些常量信息做成配置项&#xff0c;如&#xff0c;数据库的链接配置&#xff0c;业务错误代码配资等等。 我们通过两种方式可以解决该问题。 系统环境变量的方式 配置文件的方式 下边&#xff0c;将以这两方面进行展开。 1. 系统环境变量 No…

MySQL create table语法中的key与index的区别

在create table的语句中&#xff0c;key和index混淆在一起&#xff0c;官方手册中的解释是这样&#xff1a; KEY is normally a synonym for INDEX. The key attribute PRIMARY KEY can also be specified as just KEY when given in a column definition. This was implemente…

蓝桥杯 历届试题 九宫重排 (bfs+康托展开去重优化)

Description 如下面第一个图的九宫格中&#xff0c;放着 1~8 的数字卡片&#xff0c;还有一个格子空着。与空格子相邻的格子中的卡片可以移动到空格中。经过若干次移动&#xff0c;可以形成第二个图所示的局面。我们把第一个图的局面记为&#xff1a;12345678.把第二个图的局面…