我们平时在使用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,相信它应该更能满足你的需求。

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

  1. win7 linux error15,Win7开机黑屏error15:file not found的两种解决方法分享

    Win7开机黑屏error15:file not found怎么办?相信很多朋友都遇到过使用win7系统时,开机黑屏error15:file not found的,这种情况应该如何处理呢?感兴趣的朋友 ...

  2. 快速排序的两种实现方法(js)

    快速排序的基本思想:通过一趟排序,将待排记录分割成独立的两部分,其中一部分记录的关键字均比另外一部分记录的关键字小,则可分别对着两部分记录继续进行排序,以达到整个序列有序的目的.----------- ...

  3. 游戏二次元场景插画教程,常见的6种构图方法分享

    今天小编分享一个原画人二次元场景插画教程,这个教程是由原画人JW老师主讲的.主要分享常见的6种构图方法. 简单介绍一下 原画人JW老师,从业3年,现在是自由插画师兼独立游戏制作担任游戏主美.擅长意境的 ...

  4. mac php 连接mysql数据库_Mac环境下php操作mysql数据库的方法分享_PHP教程

    Mac环境下php操作mysql数据库的方法分享 今天在mac上搭建好了php的环境,我们就把php操作mysql数据库的方法分享给大家,有需要的小伙伴参考下. Mac本地环境搭建 在Mac系统,我们 ...

  5. python语言使用什么语句实现上下文管理协议_Python with语句上下文管理器两种实现方法分析...

    本文实例讲述了Python with语句上下文管理器.分享给大家供大家参考,具体如下: 在编程中会经常碰到这种情况:有一个特殊的语句块,在执行这个语句块之前需要先执行一些准备动作:当语句块执行完成后, ...

  6. 【干货】路由黑洞的5种解决方法大PK

    如何解决路由黑洞? 1.全互联(全互联的意思就是在一个AS内的所有的BGP路由器全部都建立我们Establish的关系) 2.RR(反射器) 3.联盟(一般用的不多) 4.将BGP路由引入到IGP,从 ...

  7. 项目前期管理的一种科学方法——可行性研究工作

    可行性研究在项目管理中作为投资决策和项目建设的重要依据具有十分重要的作用. 在西方经济发达国家,无论是投资者还是金融机构都非常重视项目的前期管理工作,不惜花费大量的人力.财力和物力进行项目的前期分析论 ...

  8. Word文档误删怎样恢复?6种实用方法分享给你

    如果您曾经因为没有保存微软Word文档而丢失了所有工作,那么您就会明白疼痛是多么明显. 幸运的是,自从在软盘上备份文件的黑暗时代以来,Word已经走过了漫长的道路.如今,如果您丢失了未保存的Word文 ...

  9. iphone怎么变android,如何将安卓变ios7 将安卓变iphone的两种简单方法分享

    第一种将安卓变ios7方法 如果你非常喜欢iOS 7的设计风格却又不愿意放弃Android系统带来的自由体验,或许让你的Android系统看起来像iOS 7是一个不错的选择.现在通过一些Android ...

最新文章

  1. id ajax,AJAX_ajax.js里面有内容显示效果,根据ID,ajax.js里面有内容显示效果,根 - phpStudy...
  2. 字符串中连续出现最多的子串 amp; 字符串中最长反复子串
  3. 几何画板200个经典课件_动态几何画板 Geogebra
  4. 如何安装微信支付证书到服务器,微信支付如何安装操作证书?
  5. ajax分批mysql_使用select2分批异步加载大量数据
  6. KISSY - A Powerful JavaScript Framework
  7. 大数据技术原理与应用——期末复习
  8. Vivado的下载和安装
  9. 充电w数测试软件,充电功率检测(cn.nowtool.battery) - 1.3.0 - 应用 - 酷安
  10. java severs_openssl简介-指令s_server
  11. C语言基础知识:地址与取地址符
  12. MapReducer的map和reducer的几种写法模板以及自定义数据类型
  13. 9大代理服务器软件的比较与分析之校园局域网代理蝴蝶
  14. remix下ballot.sol调试
  15. Unity 视频编码器问题解决方案(视频卡顿,花屏,黑屏)
  16. 不能说的hidden
  17. Flash媒体服务器
  18. win10+ubuntu18.04安装+sougou输入法
  19. 2023注册会计电子版教材
  20. 一文通俗搞懂线性无关特征向量个数≤特征值重数

热门文章

  1. 微信多开txt_在电脑上怎么实现微信多开的效果
  2. micropython是啥 知乎_嵌入式开发必备调试工具:Micro-Lab
  3. js封装函数_JavaScript基础-如何封装函数来改变元素的位置
  4. 道客巴巴vip账号共享2020_腾讯视频VIP怎么两个手机通用?
  5. iis mysql版本切换_MySQL+PHP配置 Windows系统IIS版(转)
  6. 将sublime text3添加到右键菜单中(可执行)
  7. aspx,ascx和ashx使用小结
  8. python类和实例化
  9. NOIP2005普及组第3题 采药 (背包问题)
  10. xxx征集系统项目目标文档