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

4 阅读3分钟

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

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

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

入口文件index.ts

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

import { Router } from 'express'

export const router = Router()

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

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

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文件

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来处理路由和对应的逻辑,如下代码:

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

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

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

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

@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的逻辑。

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方法上的元数据为:

{ path: '/login', method: 'post' }

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

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

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)
        }
      }
    }
  }
}

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

import { router } from '../router'
if (middleware) {
  router[method](fullpath, middleware, handle)
} else {
  router[method](fullpath, handle)
}

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

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

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