찾다
웹 프론트엔드JS 튜토리얼빠른 라우팅 관리를 위한 여러 가지 자동화된 방법 공유

빠른 라우팅 관리를 위한 여러 가지 자동화된 방법 공유

Feb 23, 2018 pm 01:43 PM
express여러 종류오토메이션

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

const index = require('./routes/index')
const user = require('./routes/user')
// ...其他路由文件

app.use('/', index)
app.use('/user', user)
// ...挂载其他路由

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

管理思路

我们的项目目录主要是这样的:

├─routes
  ├─index.js
  ├─user.js
  ├─sub
    ├─index.js
    ├─a.js
├─app.js

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

// 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

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

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

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

管理实例

自动生成前缀

我们通过扫描项目目录,可以将文件在项目中的路径转化为express的路由路径模式,自动生成路由前缀,例如路由文件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.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文件中引入):

const scanResult = scanDirModules(__dirname, __filename)
for (const prefix in scanResult) {
  if (scanResult.hasOwnProperty(prefix)) {
    router.use(prefix, scanResult[prefix])
  }
}

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

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

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

效果展示:

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

// routes/sub/a.js
const express = require('express')
const router = express.Router()

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

module.exports = router

挂载效果:

빠른 라우팅 관리를 위한 여러 가지 자동화된 방법 공유

访问结果:

빠른 라우팅 관리를 위한 여러 가지 자동화된 방법 공유

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

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

直接挂载到根路径

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

// 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

扫描部分的代码修改为:

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
}

路由入口文件修改为:

// 获取 routes 目录下所有路由模块,并挂载到一个路由上
const routeModules = scanDirModulesWithoutPrefix(__dirname, __filename)
routeModules.forEach(routeModule => {
  router.use(routeModule)
})

挂载效果:

빠른 라우팅 관리를 위한 여러 가지 자동화된 방법 공유

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

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

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

装饰器实现路由管理

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

// 类上的 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插件:

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

编写.babelrc文件:

{
  "presets": [
    "env"
  ],
  "plugins": [
    "transform-decorators-legacy"
  ]
}

在app.js中注册babel-register

require('babel-register')

注册函数编写

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

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<string>} options.routesDir 要扫描的路由目录
 */
function register (options) {
  app = options.app
  // 支持扫描多个路由目录
  const routesDirs = typeof options.routesDir === 'string' ? [options.routesDir] : options.routesDir
  routesDirs.forEach(dir => {
    scanDirModules(dir)
  })
}</string>

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

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

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

装饰器编写

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

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

抽象函数的具体代码为:

/**
 * 生成对应HTTP请求方法的装饰器
 *
 * @param {string} httpMethod 请求方法
 * @param {string|RegExp} pattern 请求路径
 * @param {Array<function>} 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
  }
}</function>

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

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

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

/**
 * GET 方法装饰器
 *
 * @param {string|RegExp} pattern 路由路径
 * @param {Array<function>} middlewares 中间件数组
 * @returns {MethodDecorator}
 */
function Get (pattern, ...middlewares) {
  return generateMethodDecorator('get', pattern, middlewares)
}</function>

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

/**
 * Router 类装饰器,使用在 class 上,生成一个带有共通前缀和中间件的路由
 *
 * @param {string|RegExp} prefix 路由前缀
 * @param {express.RouterOptions} routerOption 路由选项
 * @param {Array<function>} 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)
  }
}</function>

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

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

编写路由文件

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

// 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,相信它应该更能满足你的需求。

相关推荐:

node.js开发-express路由与中间件的代码示例详解


위 내용은 빠른 라우팅 관리를 위한 여러 가지 자동화된 방법 공유의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
무대 뒤에서 : 어떤 언어의 힘이 자바 스크립트입니까?무대 뒤에서 : 어떤 언어의 힘이 자바 스크립트입니까?Apr 28, 2025 am 12:01 AM

JavaScript는 브라우저 및 Node.js 환경에서 실행되며 JavaScript 엔진을 사용하여 코드를 구문 분석하고 실행합니다. 1) 구문 분석 단계에서 초록 구문 트리 (AST)를 생성합니다. 2) 컴파일 단계에서 AST를 바이트 코드 또는 기계 코드로 변환합니다. 3) 실행 단계에서 컴파일 된 코드를 실행하십시오.

파이썬과 자바 스크립트의 미래 : 트렌드와 예측파이썬과 자바 스크립트의 미래 : 트렌드와 예측Apr 27, 2025 am 12:21 AM

Python 및 JavaScript의 미래 추세에는 다음이 포함됩니다. 1. Python은 과학 컴퓨팅 분야에서의 위치를 ​​통합하고 AI, 2. JavaScript는 웹 기술의 개발을 촉진하고, 3. 교차 플랫폼 개발이 핫한 주제가되고 4. 성능 최적화가 중점을 둘 것입니다. 둘 다 해당 분야에서 응용 프로그램 시나리오를 계속 확장하고 성능이 더 많은 혁신을 일으킬 것입니다.

Python vs. JavaScript : 개발 환경 및 도구Python vs. JavaScript : 개발 환경 및 도구Apr 26, 2025 am 12:09 AM

개발 환경에서 Python과 JavaScript의 선택이 모두 중요합니다. 1) Python의 개발 환경에는 Pycharm, Jupyternotebook 및 Anaconda가 포함되어 있으며 데이터 과학 및 빠른 프로토 타이핑에 적합합니다. 2) JavaScript의 개발 환경에는 Node.js, VScode 및 Webpack이 포함되어 있으며 프론트 엔드 및 백엔드 개발에 적합합니다. 프로젝트 요구에 따라 올바른 도구를 선택하면 개발 효율성과 프로젝트 성공률이 향상 될 수 있습니다.

JavaScript가 C로 작성 되었습니까? 증거를 검토합니다JavaScript가 C로 작성 되었습니까? 증거를 검토합니다Apr 25, 2025 am 12:15 AM

예, JavaScript의 엔진 코어는 C로 작성되었습니다. 1) C 언어는 효율적인 성능과 기본 제어를 제공하며, 이는 JavaScript 엔진 개발에 적합합니다. 2) V8 엔진을 예를 들어, 핵심은 C로 작성되며 C의 효율성 및 객체 지향적 특성을 결합하여 C로 작성됩니다.

JavaScript의 역할 : 웹 대화식 및 역동적 인 웹JavaScript의 역할 : 웹 대화식 및 역동적 인 웹Apr 24, 2025 am 12:12 AM

JavaScript는 웹 페이지의 상호 작용과 역학을 향상시키기 때문에 현대 웹 사이트의 핵심입니다. 1) 페이지를 새로 고치지 않고 콘텐츠를 변경할 수 있습니다. 2) Domapi를 통해 웹 페이지 조작, 3) 애니메이션 및 드래그 앤 드롭과 같은 복잡한 대화식 효과를 지원합니다. 4) 성능 및 모범 사례를 최적화하여 사용자 경험을 향상시킵니다.

C 및 JavaScript : 연결이 설명되었습니다C 및 JavaScript : 연결이 설명되었습니다Apr 23, 2025 am 12:07 AM

C 및 JavaScript는 WebAssembly를 통한 상호 운용성을 달성합니다. 1) C 코드는 WebAssembly 모듈로 컴파일되어 컴퓨팅 전력을 향상시키기 위해 JavaScript 환경에 도입됩니다. 2) 게임 개발에서 C는 물리 엔진 및 그래픽 렌더링을 처리하며 JavaScript는 게임 로직 및 사용자 인터페이스를 담당합니다.

웹 사이트에서 앱으로 : 다양한 JavaScript 애플리케이션웹 사이트에서 앱으로 : 다양한 JavaScript 애플리케이션Apr 22, 2025 am 12:02 AM

JavaScript는 웹 사이트, 모바일 응용 프로그램, 데스크탑 응용 프로그램 및 서버 측 프로그래밍에서 널리 사용됩니다. 1) 웹 사이트 개발에서 JavaScript는 HTML 및 CSS와 함께 DOM을 운영하여 동적 효과를 달성하고 jQuery 및 React와 같은 프레임 워크를 지원합니다. 2) 반응 및 이온 성을 통해 JavaScript는 크로스 플랫폼 모바일 애플리케이션을 개발하는 데 사용됩니다. 3) 전자 프레임 워크를 사용하면 JavaScript가 데스크탑 애플리케이션을 구축 할 수 있습니다. 4) node.js는 JavaScript가 서버 측에서 실행되도록하고 동시 요청이 높은 높은 요청을 지원합니다.

Python vs. JavaScript : 사용 사례 및 응용 프로그램 비교Python vs. JavaScript : 사용 사례 및 응용 프로그램 비교Apr 21, 2025 am 12:01 AM

Python은 데이터 과학 및 자동화에 더 적합한 반면 JavaScript는 프론트 엔드 및 풀 스택 개발에 더 적합합니다. 1. Python은 데이터 처리 및 모델링을 위해 Numpy 및 Pandas와 같은 라이브러리를 사용하여 데이터 과학 및 기계 학습에서 잘 수행됩니다. 2. 파이썬은 간결하고 자동화 및 스크립팅이 효율적입니다. 3. JavaScript는 프론트 엔드 개발에 없어서는 안될 것이며 동적 웹 페이지 및 단일 페이지 응용 프로그램을 구축하는 데 사용됩니다. 4. JavaScript는 Node.js를 통해 백엔드 개발에 역할을하며 전체 스택 개발을 지원합니다.

See all articles

핫 AI 도구

Undresser.AI Undress

Undresser.AI Undress

사실적인 누드 사진을 만들기 위한 AI 기반 앱

AI Clothes Remover

AI Clothes Remover

사진에서 옷을 제거하는 온라인 AI 도구입니다.

Undress AI Tool

Undress AI Tool

무료로 이미지를 벗다

Clothoff.io

Clothoff.io

AI 옷 제거제

Video Face Swap

Video Face Swap

완전히 무료인 AI 얼굴 교환 도구를 사용하여 모든 비디오의 얼굴을 쉽게 바꾸세요!

뜨거운 도구

맨티스BT

맨티스BT

Mantis는 제품 결함 추적을 돕기 위해 설계된 배포하기 쉬운 웹 기반 결함 추적 도구입니다. PHP, MySQL 및 웹 서버가 필요합니다. 데모 및 호스팅 서비스를 확인해 보세요.

Atom Editor Mac 버전 다운로드

Atom Editor Mac 버전 다운로드

가장 인기 있는 오픈 소스 편집기

VSCode Windows 64비트 다운로드

VSCode Windows 64비트 다운로드

Microsoft에서 출시한 강력한 무료 IDE 편집기

메모장++7.3.1

메모장++7.3.1

사용하기 쉬운 무료 코드 편집기

에디트플러스 중국어 크랙 버전

에디트플러스 중국어 크랙 버전

작은 크기, 구문 강조, 코드 프롬프트 기능을 지원하지 않음