我是如何在项目中用装饰器简化数据访问层的

1,949 阅读7分钟

1、你在项目中的数据访问层是否还长这样?

import request from '@/utils/request'
export function login(data) {
  return request({
    url: '/user/login',
    method: 'post',
    data
  })
}

export function getInfo(token) {
  return request({
    url: '/user/info',
    method: 'get',
    params: { token }
  })
}

export function logout() {
  return request({
    url: '/user/logout',
    method: 'post'
  })
}

随着我年纪的增长,我思考了2个问题:

1、如果有同事因为不仔细,把元数据写错了怎么办?

2、能不能不用每次都去写这些元数据代码?

2、从Nest.js或者Spring MVC框架中得到的一些启示。

import { Controller, Get } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}

如果我们前端的数据访问层也能写成如nest.js的这种风格,那么,我们的代码看起来将会是相当的简洁。所以,从这个例子,我们已经确定了我们的设计目标,如果我们能得到一组装饰器,那么,我们的数据访问层将会改写为以下形式:

import { Controller, GET, POST } from '@/decorators/request';
@Controller('/user')
export class UserApi {
	
    @POST()
    login() {}

    @GET('/info')
    getInfo() {}
    
    @POST()
    logout() {}

}

3、回到原点,背上行囊,从装饰器出发。

这是一段摘录自阮一峰老师的《ES6入门》一书的话:

装饰器(Decorator)是一种与类(class)相关的语法,用来注释或修改类和类方法。许多面向对象的语言都有这项功能,目前有一个提案将其引入了 ECMAScript。

装饰器是装饰模式在语法层面的提供,通过装饰器,我们可以在某个横切面完成我们的业务逻辑,如预判,如果不满足,直接终止该方法的执行,或执行方法之后,需要的一些补救行为。这些横切面跟我们的业务代码没有或者少有耦合,更容易帮助我们编写出优秀的软件系统。

装饰器其实就是一个普通的函数,写成@ + 函数名,放在类名或者类的成员方法(或属性)名之前,因为有变量的提升,普通函数不能被装饰。目前ES支持两种类型的装饰器。

3.1、类装饰器

// 定义装饰器
//如果不需要接收参数
function Controller(Target) {
    //悄悄的做一些事儿
    return Target;
}

//如果需要接收参数
function Controller(path) {
    return function(Target){
    	//悄悄的做一些事儿
        return Target;
    } 
}

//使用装饰器
//当不需要接收参数的时候
@Controller
class Demo{}

//当需要接收参数的时候
@Controller('/login')
class Demo {}

类的装饰器接收一个参数,target,这个target就是类本身。

3.2、 方法(或属性)装饰器

//如果不需要接收参数
function GET(target, prop, descriptor) {
	//悄悄的做一些操作
    return descriptor;
}
//如果需要接收参数
function GET(path){
	return function(target,prop, descriptor) {
    	// 悄悄的做一些操作。
    	return descriptor;
    }
}

//使用装饰器
//当不需要接收参数的时候
class Demo{
    @GET
    login() {}
}
//当需要接收参数的时候
class Demo{
    @GET('/login')
    login(){}
}

方法的装饰器有3个参数,顺序分别是target,prop,descriptor,第一个参数target是被装饰类的原形对象,第二个参数是被装饰方法或属性的字段名,第三个参数是一个descriptor,它的类型是PropertyDecorator。由于笔者在之前的博文有讲解过这个类型,因此,此处仅贴出它的类型定义代码 不再详细讲解,不太了解的读者可参考MDN或笔者早先的博文。

PropertyDescriptor的定义如下:

interface PropertyDescriptor {
    configurable?: boolean;
    enumerable?: boolean;
    value?: any;
    writable?: boolean;
    get?(): any;
    set?(v: any): void;
    // 对于babel,还可能出现initializer,这是一个函数,用于描述属性初始化操作的代码块
}

4、编写装饰器

4.1、 前置知识,reflect-metadata库

GitHub地址:reflect-metadata

我们不应该修改语言本身的定义以外的属性(如给descriptor加一些奇奇怪怪的字段,或者直接在function定义一些奇奇怪怪的字段,或者在Object,或者自己去是现实一个记录元数据的对象等),否则可能会给我们的程序带来潜在的隐患。reflect-metadata可以给我们提供这些便利,使用这个库以后,可以方便的记录我们传递的元数据参数。

4.2 、装饰器的实现

根据笔者在3年项目开发中对axios的使用感受,提炼出了以下方法:

import "reflect-metadata";
//这就是我们正常的对axios的统一封装的实例。
import http from "@/utils";
function hasPrefix(str) {
  return str.startsWith("/");
}

function normalize(str) {
  var url = !hasPrefix(str) ? "/" + str : str;
  return url;
}

/**
 * 定义ResponseType
 * @param responseType 响应的类型
 */
export function ResponseType(responseType) {
  return function(target, prop, descriptor) {
    Reflect.defineMetadata("responseType", responseType, target, prop);
    return descriptor;
  };
}

/**
 * 需要对路由进行RESTful重写的路由
 */
export function RESTful(rewritePath) {
  return function(target, prop, descriptor) {
    Reflect.defineMetadata("rewritePath", rewritePath, target, prop);
    return descriptor;
  };
}

/**
 * 定义Controller
 * @param controller
 */
export function Controller(controller) {
  return function(Target) {
  	//记住在controller部分的path
    Reflect.defineMetadata("controller", controller, Target.prototype);
    return Target;
  };
}

/**
 * 定义访问的附加Headers
 * @param headers
 */
export function Headers(headers) {
  return function(target, prop, descriptor) {
  	//记住要传递的headers
    Reflect.defineMetadata("headers", headers, target, prop);
    return descriptor;
  };
}

/**
 * 强制指定某些参数作为querystring传递
 * @param {String[]} reserveAsParams
 */
export function Param(reserveAsParams) {
  return function(target, prop, descriptor) {
  	//定义要强制作为querystring传递的参数键集合
    Reflect.defineMetadata("params", reserveAsParams, target, prop);
    return descriptor;
  };
}

export function Request(requestPath, requestMethod) {
  return function(target, prop, descriptor) {
    var method = requestMethod || "GET";
    var path = requestPath || prop;
    descriptor.value = function() {
      /**
       *由于目前JS的尚未支持参数装饰器,因此,函数只能接收一个参数,且该参数必须是对象。
       */
      //如果参数是多个的话,给出提醒
      if (arguments.length > 1) {
        throw `can not call this function with argument more than one`;
      }
      // 如果蚕食是一个,且是非对象类型的话,这个参数将会被忽略
      if (arguments.length == 1 && Object.prototype.toString.call(arguments[0]) !== "[object Object]") {
        console.warn("the param  only one will be ignore if you call this function with a basic data-type ");
      }
      var url = "";
      var controller = Reflect.getMetadata("controller", target);
      var headers = Reflect.getMetadata("headers", target, prop);
      var rewritePath = Reflect.getMetadata("rewritePath", target, prop);
      var responseType = Reflect.getMetadata("responseType", target, prop);
      var hasRequestBody = ["post", "put", "patch"].includes(method.toLowerCase());
      //自动忽略非对象参数
      var arg = Object.prototype.toString.call(arguments[0]) === "[object Object]" ? arguments[0] : {};
      //用作放在请求体上的数据
      var data = hasRequestBody ? arg : undefined;
      //用作放在查询字符串的数据
      var params = hasRequestBody ? {} : arg;
      //对于post这类请求,强制通过params传递给后端的数据,
      var reserveAsParams = Reflect.getMetadata("params", target, prop);
      if (hasRequestBody && Array.isArray(reserveAsParams)) {
        //将指定的key从data拷贝到params上去
        reserveAsParams.forEach((key) => {
          params[key] = data[key];
          delete data[key];
        });
      }
      controller && (url += `${normalize(controller)}`);
      url += `${normalize(path)}`;
      //如果需要对URL进行重写的话,处理需要进行重写的部分,剩余的部分
      if (rewritePath) {
        var eties = Object.entries(params);
        eties.forEach(([prop, value]) => {
          var regExp = new RegExp("\\$\\{" + prop + "\\}");
          //如果能匹配到的话,说明此参数需要进行重写,需要从params里面移除
          if (regExp.test(rewritePath)) {
            delete params[prop];
          }
          rewritePath = rewritePath.replace(regExp, value);
        });
        //合并重写的path
        url += `${normalize(rewritePath)}`;
      }
      return http({
        url,
        method,
        data,
        headers,
        responseType,
        //如果没有一个需要通过queryString发送给后台的数据,则不会处理查询字符串
        params: Object.keys(params).length > 0 ? params : undefined,
      });
    };
    return descriptor;
  };
}

export function GET(path) {
  return Request(path, "GET");
}

export function POST(path) {
  return Request(path, "POST");
}

export function DELETE(path) {
  return Request(path, "DELETE");
}

export function PUT(path) {
  return Request(path, "PUT");
}

export function HEAD(path) {
  return Request(path, "HEAD");
}

export function PATCH(path) {
  return Request(path, "PATCH");
}

在数据访问类中使用:

import { Controller, POST, GET, ResponseType, Param, Headers, RESTful } from '@/decorators/request' 
@Controller('/app')
export class HomeApi {
	
    @GET()
    detail(){}
    /*
    等价于
    detail(params) {
    	return http({
            url: '/app/detail',
            method: 'get',
            params,
        })
    }
    */
    
    @GET('/list')
    getDataList() {}

    /*
    等价于
    getDataList(params) {
    	return http({
            url: '/app/list',
            method: 'get',
            params
        })
    }
    */
    
    @POST('/entity')
    saveEntity() {}
    /*
    等价于
    saveEntity(data) {
        return http({
            url: '/app/list',
            method: 'post',
            data
        })
    }
    */
    
    @GET('/entity')
    @RESTful('/${name}/${age}')
    getEntity(){}
    /*
    等价于
    getEntity(params = {}){
    	const { name, age, ...rest } = params;
    	return http({
            url:`/app/entity/${name}/${age}`,
            method:'get',
            params: rest,
        });
    }
    
    */
    
    @GET('/download')
    @ResponseType('blob')
    download(){}
    
    /*
    等价于
    download() {
    	return http({
            url: '/app/download',
            method: 'get',
            responseType: 'blob',
        })
    }
    */
    
    @POST('/create')
    @Headers({
    	"Content-Type":"application/json"
    })
    save(){}
    /*
     等价于
     save() {
     	return http({
            url: '/app/create',
            method: 'post',
            headers: {
            	"Content-Type": 'application/json'
            }
        })
     }
    */
    
    
    @POST('/submit')
    @Param(['time'])
    submit(){}
    /*
    等价于
    submit(data = {}) {
    	const { time, ...rest } = data;
        return http({
            url: '/app/submit',
            method: 'post',
            data: rest,
            params: {
            	time
            }
        })
     }
    */
}

因为我们的数据访问层现在是按照装饰器这种风格写的,由于目前ES还不支持参数装饰器(TS支持),当我们传递多余一个参数或者传递一个基本类型的参数时候,无法识别要传递给后台的key,因此,我们在调用的时候必须写成仅含有一个参数,且这个参数必须是对象的这种统一格式,多少还是有一点儿遗憾。😣

5、总结

1、借助装饰器,我们可以少写很多代码,简化我们的开发。

2、在日常的开发中,我们绝大部分场景都在写url,可以避免因单词的拼写错误,导致数据传递出错的问题。

由于笔者水平有限,写作过程中难免出现错误,若有纰漏,请各位读者指正,你们的意见将会帮助我更好的进步。本文乃笔者原创,若转载,请联系作者本人,邮箱404189928@qq.com🥰