nestjs学习2:利用typescript改写express服务

在学习nestjs之前,我们改写下express服务,这样更有利于理解nestjs

express服务改写后,主要有这么几个文件:

  1. 入口文件index.ts:负责起一个http服务,用来监听用户发起的http请求;
  2. 路由文件router.ts:对不同的请求进行处理,但是,具体的处理放在controller文件中;
  3. controller.ts专门进行路由处理;

入口文件index.ts

js 复制代码
import express, { Request, Response, NextFunction } from 'express'
import cookieSession from 'cookie-session'
# 必须引入,让装饰器执行
import './controller/LoginController'
import { router } from './router'

const app = express()

# 处理请求体的application/json数据
app.use(express.json())
# 处理form表单数据
app.use(express.urlencoded({ extended: false }))

# 处理cookie-session
app.use(
  cookieSession({
    name: 'session',
    // 用来生成sessionid的秘钥
    keys: ['pk2#42'],
    maxAge: 48 * 60 * 60 * 1000
  })
)

app.use(router)

app.listen('7001', () => {
  console.log('listen at 7001')
})

注意:在入口文件中必须要引入controller文件。

路由文件router.ts

js 复制代码
import { Router } from 'express'

export const router = Router()

这里的路由文件并没有处理任何逻辑,实例化之后直接导出,这与之前的样子区别很大。

原来是长这样的,它在路由文件中耦合了处理逻辑部分:

js 复制代码
router.post(
  '/login',
  (req: RequestWithBody, res: Response, next: NextFunction) => {
    const { password } = req.body
    const isLogin = req.session?.isLogin
    if (isLogin) {
      res.end('already login')
    } else {
      if (password === '123' && req.session) {
        req.session.isLogin = true
        req.session.userId = '1234567890'
        res.json(getResponseResult(true))
      } else {
        res.end('login error!')
      }
    }
  }
)

每个接口都要写一个router.post这样的代码,是不是感觉挺啰嗦的。

controller文件

js 复制代码
import 'reflect-metadata'
import { Request, Response } from 'express'
import { controller, get, post } from '../decorator'

@controller('/')
export class LoginController {
  constructor() {}

  @post('/login')
  login(req: Request, res: Response): void {
    ...
  }

  @get('/logout')
  logout(req: Request, res: Response): void {
    ...
  }
}

现在提供了一个LoginController类来处理登录相关的所有逻辑。包括一个登录接口/login和一个登出接口/logout

但是,代码里面并没有和router绑定的逻辑,传统的express的代码,通常是通过router.getrouter.post来处理路由和对应的逻辑,如下代码:

js 复制代码
import { Router, Request, Response, NextFunction } from 'express'
router.post('/login', (req: Request, res: Response, next: NextFunction) => {
    ...
})
router.get('/logout', checkLogin, (req, res, next) => {
  ...
})

那它是到底怎么实现路由逻辑的呢?

答案是通过装饰器和元数据来实现的。

方法的装饰器:绑定请求方法和请求路径

js 复制代码
@controller('/')
export class LoginController {
  @post('/login')
  login(req: Request, res: Response): void {}

  @get('/logout')
  logout(req: Request, res: Response): void {}
}

它包含三个装饰器,分别是get,post,controller,我们首先看看get、post的逻辑。

js 复制代码
enum Methods {
  get = 'get',
  post = 'post'
}
function getRequestDecorator(type: Methods) {
  return function (path: string) {
    # target就是类的原型对象
    return function (target: LoginController, key: string) {
      Reflect.defineMetadata('path', path, target, key)
      Reflect.defineMetadata('method', type, target, key)
    }
  }
}
export const get = getRequestDecorator(Methods.get)
export const post = getRequestDecorator(Methods.post)

这段代码很简单,就是定义了两个getpost两个装饰器,在装饰器里面通过元数据Reflect.defineMetadata上添加了pathmethod两个元数据,例如,login方法上的元数据为:

js 复制代码
{ path: '/login', method: 'post' }

类的装饰器:获取绑定的元数据

装饰器controller用来修饰类LoginController,这里需要知道,方法的装饰器是先于类的装饰器之前执行,所以,能在类的装饰器上获取到在方法的装饰器上定义的元数据。

js 复制代码
export function controller(root: string) {
  // target就是类的构造函数,通过target.prototype获取类的原型
  return function (target: new (...args: any[]) => any) {
    for (let key in target.prototype) {
      // 获取路由
      const path: string = Reflect.getMetadata('path', target.prototype, key)
      // 获取请求方法
      const method: Methods=Reflect.getMetadata('method',target.prototype,key)
      // 获取对应的处理函数
      const handle = target.prototype[key]
      // 获取中间件
      const middleware: RequestHandler = Reflect.getMetadata(
        'middleware',target.prototype,key)
      // 拼接路由
      if (path && method) {
        let fullpath = ''
        if (root === '/') {
          if (path === '/') {
            fullpath = '/'
          } else {
            fullpath = path
          }
        } else {
          fullpath = `${root}${path}`
        }
        // 绑定router
        if (middleware) {
          router[method](fullpath, middleware, handle)
        } else {
          router[method](fullpath, handle)
        }
      }
    }
  }
}

可以看到,最终的落脚点在这里:

js 复制代码
import { router } from '../router'
if (middleware) {
  router[method](fullpath, middleware, handle)
} else {
  router[method](fullpath, handle)
}

所以,我需要在入口文件中引入controller文件,这样就能执行装饰器了。

这样改写之后,你如果新增一个模块,比如用户模块,你只需要创建一个UserController的类即可。

你是不是发现和nestjs有点像了,只不过它实现了控制反转和依赖注入。

相关推荐
Eric_见嘉6 天前
NestJS 🧑‍🍳 厨子必修课(九):API 文档 Swagger
前端·后端·nestjs
XiaoYu200214 天前
第3章 Nest.js拦截器
前端·ai编程·nestjs
XiaoYu200215 天前
第2章 Nest.js入门
前端·ai编程·nestjs
实习生小黄16 天前
NestJS 调试方案
后端·nestjs
当时只道寻常19 天前
NestJS 如何配置环境变量
nestjs
濮水大叔1 个月前
VonaJS是如何做到文件级别精确HMR(热更新)的?
typescript·node.js·nestjs
ovensi1 个月前
告别笨重的 ELK,拥抱轻量级 PLG:NestJS 日志监控实战指南
nestjs
ovensi1 个月前
Docker+NestJS+ELK:从零搭建全链路日志监控系统
后端·nestjs