在学习nestjs之前,我们改写下express服务,这样更有利于理解nestjs。
对express服务改写后,主要有这么几个文件:
- 入口文件
index.ts:负责起一个http服务,用来监听用户发起的http请求; - 路由文件
router.ts:对不同的请求进行处理,但是,具体的处理放在controller文件中; 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.get和router.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)
这段代码很简单,就是定义了两个get、post两个装饰器,在装饰器里面通过元数据Reflect.defineMetadata上添加了path和method两个元数据,例如,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有点像了,只不过它实现了控制反转和依赖注入。