koa开发之使用ts装饰器

3,261 阅读5分钟

ts装饰器

装饰器的最佳使用场景是面向切面编程,它可以在不修改代码自身的前提下,给已有代码增加额外的行为

面向切面编程(AOP) :运行时,动态将代码切入到类的指定方法,指定位置上的编程思想就是切面编程。比如在两个类里的每个方法做日志,按照面向对象的设计在一个独立的类里面定义,上面两个类分别调用,这样就产生了耦合。而切面编程则在解耦。 装饰器是一种特殊的声明,可附加在类、方法、访问器、属性、参数声明上。

装饰器使用 @log 的形式,称为注解型装饰器,其中 log 必须能够演算为在运行时调用的函数。

方法装饰器

方法装饰器有三个参数:

  • target——当前对象
  • property——方法名称
  • descriptor——属性描述符
function log(target, property, descriptor) {
//target.property === descriptor.value
    var oldValue = descriptor.value;	//拿到老方法
    //设置新方法
    descriptor.value = function () {
        console.log('打印日志', arguments);
        return oldValue.apply(null, arguments);
    }
    return descriptor;
}

class Maths {
    @log
    add(a, b) {
        console.log(a + b);
    }
}

修改package.json配置

"scripts": {
    "start": "ts-node-dev ./src/index.ts -P tsconfig.json --no-cache",
    "build": "tsc -P tsconfig.json && node ./dist/index.js",
    "tslint": "tslint --fix -p tsconfig.json"
  },

npm start查看结果:

类装饰器

只有target 这一个参数,代表当前类

// 类装饰器
function anotationClass(target) {
    console.log('===== Class Anotation =====')
    console.log('target :', target)
}

// 方法装饰器
function anotationMethods (target, property, descriptor) {
        // target 
        console.log('===== Method Anotation ' + property + "====")
        console.log('target:', target)
        console.log('property:', property)
        console.log('descriptor:', descriptor)
}

@anotationClass
class Example {
    @anotationMethods
    instanceMember() { }
}
const example = new Example();
example.instanceMember()

查看结果: 并且方法装饰器先于类装饰器执行

装饰器原理

上面的方法装饰器 @anotationMethods 是语法糖

tsc index.ts编译生成js文件

var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
	//2.给desc属性描述符赋值
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    //3.遍历取出注解方法
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    //5.给被注解的方法重新赋值,值为注解的方法
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};

var Example = /** @class */ (function () {
    function Example() {
    }
    //4.执行注解方法
    Example.prototype.instanceMember = function () { };
    //1.执行__decorate方法
    __decorate([
        anotationMethods
    ], Example.prototype, "instanceMember");
    return Example;
}());
var aaa = new Example();
aaa.instanceMember();

装饰器运行过程:

  • 执行decorate方法
  • Object.getOwnPropertyDescriptor(target.prototype,proterty) 给属性描述符赋值
  • 取出注解方法
  • 执行注解方法
  • Object.defineProperty(target.prototype,proterty,descriptor) 给被注解的方法重新赋值,值为注解的方法

装饰器工厂

装饰器工厂就是一个简单的工厂函数,它返回一个装饰器函数,区别就在于调用装饰的时候传不传参数。如需要传参则升阶为装饰器工厂。

function color(value: string) { // 这是一个装饰器工厂
    return function (target) { //  这是装饰器
        // do something with "target" and "value"...
    }
}

class Example {
   @color('test')
   method(){}
}

装饰器实现路由定义

  • 创建路由 ./src/routes/user.ts

我们使用@get('/users'),@post('/users')实现装饰器路由

import * as Koa from 'koa'
const users = [{name: 'tom',age: 20}]
export default class User{
    @get('/users')
     public async list(ctx: Koa.Context){
        ctx.body = {ok:1,data:users}
    }
    
    @post('/users')
    public add(ctx: Koa.Context){
        users.push(ctx.request.body);
        ctx.body = {ok:1}
    }
}
  • 路由注册 ./utils/route-decors.ts @get('/users') 传入一个参数,所以我们先实现一个装饰器工厂函数
import * as koaRouter from 'koa-router'
type RouteOptions = {
    /**
     * 适用于某个请求比较特殊,需要单独制定前缀的情形
     */
    prefix?: string;
}
const router = new koaRouter()
const get = (path: string,options?: RouteOptions) => (target,property,descriptor) => {
	const url = options && options.prefix ? options.prefix + path : path
    // router.get('/user',async ctx =>{})
    router['get'](url,target[property])
}
const post = (path: string,options?: RouteOptions) => (target,property,descriptor) => {
	const url = options && options.prefix ? options.prefix + path : path
    // router.get('/user',async ctx =>{})
    router['post'](url,target[property])
}

我们看到上面的get,post实际上市重复的代码,解决get post put delete方法公用逻辑 我们需要进一步对原有函数进行柯里化升阶

const method = (method) => (path: string,options?: RouteOptions) => (target,property,descriptor) => {
	const url = options && options.prefix ? options.prefix + path : path
    // router.get('/user',async ctx =>{})
    router[method](url,target[property])
}
export const get = method('get')
export const post = method('post')

我们发现router变量 不符合函数式编程引用透明的特点,对后面移植不利,所以要再次进行柯里化升阶

const decorate = (router: koaRouter) => (method) => (path: string,options?: RouteOptions) => (target,property,descriptor) => {
	const url = options && options.prefix ? options.prefix + path : path
    // router.get('/user',async ctx =>{})
    router[method](url,target[property])
}
const method = decorate(router)

export const get = method('get')
export const post = method('post')

自动扫描routes下文件进行自动注册

import * as glob from 'glob';

//参数是文件夹名称,返回类型是koaRouter
export const load = (folder: string): koaRouter  => {
    // 定义扩展名
    const extname = '.{js,ts}'
    // 同步扫描,文件夹下所有以某扩展名结尾的文件
    glob.sync(require('path').join(folder,`./**/*${extname}`))
    .forEach(item => require(item)) //引入所有类,执行装饰器,这时router挂满配置信息,返回router,调用时用的是这个挂有配置信息的router
    return router
}

使用

/routes/user.ts iport {get,post} from '../utils/route-decors' index.ts

import * as Koa from 'koa'
import * as bodify from 'koa-body'
import { load } from './utils/decors';
import {resolve} from 'path'
app.use(bodify({
    multipart: true,
    // 使用非严格模式,解析delete请求的请求体
    strict: false
    })
)
const router = load(resolve(__dirname,'./routes'))
app.use(router.routes())
app.listen(3000,() => {
    console.log('3000端口已启动')
})

装饰器实现类级别路由守卫

./routes/user.ts

@middlewares([async function guard(ctx: Koa.Context,next: () => Promise<any>){
    console.log('guard',ctx.header);
    if(ctx.header.token){
        await next();
    }else{
        throw "请登录"
    }
}])
export default class User{

}

我们写一个类装饰器

import {get,post,middlewares} from '../utils/route-decors'
export const middlewares = (middlewares) =>(target) => {
    // 将中间件挂载在类原型上
    target.prototype.middlewares = middlewares
}

修改decorate方法, 由于执行顺序是先执行方法装饰器在执行类装饰器,我们设置pocess.nextTick()改变执行顺序,让类装饰器先执行,下一秒在执行路由注册

const method = (router) => (method) => (path:string,options ?: RouteOptions) => (target,property) => {
    // 晚一拍执行路由注册:因为需要等类装饰器执行完毕
    process.nextTick(() => {
    	//添加中间件数组
        const middlewares = [];
        // 从类实例中取出中间件
        if(target.middlewares){
            middlewares.push(...target.middlewares)
        }
        //如果方法装饰器也传入中间件,执行该操作
        if(options && options.middlewares){
            middlewares.push(...options.middlewares)
        }
        middlewares.push(target[property])
        const url = options && options.prefix ? options.prefix + path : path
        router[method](url,...middlewares)
    })
}

装饰器实现数据校验

./routes/user.ts

import {get,post,middlewares,query,body} from '../utils/route-decors'
export default class User{
    @get('/users')
    @query({
        age: { type: 'int', required: false, max: 200, convertType: 'int' },
    })
    public async list(ctx: Koa.Context){
        ctx.body = {ok:1,data:users}
    }
}

./utils/router-decors.ts

//引入parameter库 数据校验
import * as Parameter from 'parameter'

 const validator = (parameter) =>(params) => (rule) => (target,property,descriptor) => {
    const oldValue = descriptor.value;
    descriptor.value = function(){
        // 拿到ctx
        const ctx = arguments[0]
        // 获取参数:1.ctx.request.body  2.ctx.request.query
        const data = ctx.request[params]
        // 规则和参数对应做校验
        const error = parameter.validate(rule,data)
        console.log(error)
        if (error) throw new Error(JSON.stringify(error))
        return oldValue.apply(null,arguments)
    }
    return descriptor
}
const parameter = new Parameter()
const validate = validator(parameter)

export const query = validate('query')
export const body = validate('body')