import express, { Request, Response, NextFunction } from 'express' import cookieSession from 'cookie-session' // 堆代码 duidaima.com // 必须引入,让装饰器执行 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。
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' }类的装饰器:获取绑定的元数据
export function controller(root: string) { // 堆代码 duidaima.com // 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的好处。