最近学习了typescript
的很多知识,于是就想来用一用,于是就用typscript
改写了下原来利用express
写的后台服务,改完之后真香。
这里主要用到了typescript
的类、装饰器和元数据进行改写,先来看看利用改写之后的express
服务长什么样子。
改写后的样子
express
主要有三个比较重要的文件:
- 入口文件
index.ts
:负责起一个http
服务,用来监听用户发起的http
请求; - 路由文件
router.ts
:对不同的请求进行处理,但是,具体的处理逻辑放在controller
文件中; - 处理逻辑的文件
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')
})
路由文件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!')
}
}
}
)
处理逻辑的文件LoginController.ts
import 'reflect-metadata'
import { Request, Response } from 'express'
import { controller, get, post } from '../decorator'
import { getResponseResult } from '../utils/resultModel'
@controller('/')
export class LoginController {
constructor() {}
@post('/login')
login(req: Request, res: Response): void {
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
res.json(getResponseResult(true))
} else {
res.end('login error!')
}
}
}
@get('/logout')
logout(req: Request, res: Response): void {
if (req.session) {
req.session.isLogin = undefined
}
res.json(getResponseResult(true))
}
}
现在提供了一个LoginController
类来处理登录相关的所有逻辑。包括一个登录接口/login
和一个登出接口/logout
。
但是,代码里面并没有和router
绑定的逻辑,传统的express
的代码,通常是通过router.get
和router.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)
这段代码很简单,就是定义了两个get
、post
两个装饰器,在装饰器里面通过元数据Reflect.defineMetadata
在LoginController
的方法login
和logout
上添加了path
和method
两个元数据,例如,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)
}
}
}
}
}
首先,遍历类LoginController
原型target.prototype
上的方法,即login
和logout
,从它们身上获取上面定义的元数据path
和method
:
const path: string = Reflect.getMetadata('path', target.prototype, key)
const method: Methods = Reflect.getMetadata('method', target.prototype, key)
然后,获取路由对应的处理函数handler
:
const handle = target.prototype[key]
接着,获取元数据中间件middleware
,中间件的元数据定义如下:
// 定义中间件
export function use(middleware: RequestHandler) {
return function (target: any, key: string) {
Reflect.defineMetadata('middleware', middleware, target, key)
}
}
// 获取中间件
const middleware: RequestHandler = Reflect.getMetadata('middleware', target.prototype, key)
最后,把获取的path
, method
, middleware
绑定到路由上,这样就完成了路由的处理逻辑。
import { router } from '../router'
if (middleware) {
router[method](fullpath, middleware, handle)
} else {
router[method](fullpath, handle)
}
为什么要用装饰器重构呢?
在回答这个问题之前,看看经过这样改造后,新增两个接口怎么写?
import 'reflect-metadata'
...
// 中间件:验证用户是否登录
const checkLogin = (req: Request, res: Response, next: NextFunction): void => {
const isLogin = !!req.session?.isLogin
if (isLogin) {
next()
} else {
res.json(getResponseResult(null, 'please login'))
}
}
@controller('/api')
export class CrowllerController {
// 注册路径及方法
@get('/getData')
// 注册中间件
@use(checkLogin)
getData(req: Request, res: Response): void {
...
}
@get('/showData')
@use(checkLogin)
showData(req: Response, res: Response): void {
...
}
}
可以看到,当新增接口时,就可以新建一个文件,然后创建一个新的类CrowllerController
,所有的逻辑都可以写在类里。
有没有发现,这有点像eggjs
里Controller
类了。
这样写的最大好处是,整个逻辑非常清晰明了。
同时,通过装饰器可以把各个功能都单独提出来,和业务逻辑实现解耦,如下图所示:
把各个业务功能看出一条线,这条线在执行过程中会被日志,安全,鉴权等功能切一刀,这种开发模式就是 面向切面编程(Aspect Oriented Programming
,简称AOP
)。
在不使用装饰器的情况下,实现日志功能的时候,需要把日志功能嵌入到业务功能里面,这样就不符合软件开发原则了,如下代码都混在一块了。
class Foo {
fn1() {
// 打印日志
log()
console.log('业务功能1')
}
fn2() {
// 打印日志
log()
console.log('业务功能2')
}
}
所以,需要使用装饰器把日志功能单独抽离出来:
function log(target: any, key: string, descriptor: PropertyDescriptor) {
const oldValue = descriptor.value // fn1 函数
// 重新定义 fn1 函数
descriptor.value = function () {
console.log(`记录日志...`)
return oldValue.apply(this, arguments)
}
}
class Foo {
@log // 不影响业务功能的代码,只是加了一个 log 的“切面”
fn1() {
console.log('业务功能1')
}
}
const f = new Foo()
f.fn1()
这样就实现了业务功能和日志功能的分离解耦。
可以看到,AOP
和 OOP
并不冲突,它们相辅相成。
大名鼎鼎的nestjs
就是采用这种编程方式。
总结
typescript
不仅仅提供了类型提示,它还扩展了很多 JavaScript
在语法层面没有实现的功能,这些功能在编写高质量的代码过程中是非常好用的。上面通过改写express
服务就能很好的体现了typescript
的好处。
在开发中,其实不用typescript
也能很好的完成项目,但是,当你所引用的第三方库都是用typescript
写的,你阅读的源码也是typescript
,这个时候不是你愿不愿意用的问题,而且大势所逼。
所以,用起来吧。