最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 我是如何在项目中用装饰器简化数据访问层的

    正文概述 掘金(HsuYang)   2021-01-10   447

    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入门》一书的话:

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

    装饰器其实就是一个普通的函数,写成@ + 函数名,放在类名或者类的成员方法(或属性)名之前,因为有变量的提升,普通函数不能被装饰。目前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?


    起源地下载网 » 我是如何在项目中用装饰器简化数据访问层的

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元