装饰器(Decorator)

2021-04-30  本文已影响0人  世玮

装饰器(Decorator)

装饰器是一种函数,写成@ + 函数名。它可以放在类和类方法的定义前面。

类上的装饰

@decorator
class A {}

// 等同于

class A {}
A = decorator(A) || A;

React 与 Redux 库结合使用时,运用装饰器,更容易理解:

class MyReactComponent extends React.Component {}

export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);

修改成:

@connect(mapStateToProps, mapDispatchToProps)
export default class MyReactComponent extends React.Component {}

方法上的装饰

function readonly(target, name, descriptor){
    console.log(target, name, descriptor);
    // descriptor对象原来的值如下
    // {
    //   value: specifiedFunction,
    //   enumerable: false,
    //   configurable: true,
    //   writable: true
    // };
    // descriptor.writable = false;
    return descriptor;
}
export default class Girl{
    constructor(props) {
        this.weight = "6.5斤";
        this.age = 1;
    }
    @readonly
    name(){
        return `${this.weight} ${this.age}`
    }
}
image
function readonly(target, name, descriptor){
  // descriptor对象原来的值如下
  // {
  //   value: specifiedFunction,
  //   enumerable: false,
  //   configurable: true,
  //   writable: true
  // };
  descriptor.writable = false;
  return descriptor;
}

readonly(Girl.prototype, 'name', descriptor);
// 类似于
Object.defineProperty(Girl.prototype, 'name', descriptor);

在实现一个日志输出场景:

function log(target, name, descriptor) {
    var oldValue = descriptor.value;

    descriptor.value = function() {
        console.log(`Calling ${name} with`, arguments);
        return oldValue.apply(this, arguments);
    };

    return descriptor;
}

export default class Girl{
    constructor(props) {
        this.weight = "6.5斤";
        this.age = 1;
    }

    @log
    fetchAge(newAge){
        this.age = newAge;
    }
}

  var girl = new Girl();
  girl.fetchAge(3); //这边可以看到日志打印
  console.log(girl.age);// 3

一个方法多个装饰的场景:

如果同一个方法有多个装饰器,会像剥洋葱一样,先从外到内进入,然后由内向外执行

function logNew(methodName) {
    console.log(methodName);
    return (target, name, descriptor)=>{
        console.log('evaluated-methodName', methodName);
        var oldValue = descriptor.value;
        descriptor.value = function() {
            console.log(`Calling ${methodName} with`, arguments);
            return oldValue.apply(this, arguments);
        };
        return descriptor;
    }
}

export default class Girl{
    constructor(props) {
        this.weight = "6.5斤";
        this.age = 1;
    }
    @logNew("fetchAge1")
    @logNew("fetchAge2")
    fetchAge(newAge){
        this.age = newAge;
    }
}

var girl = new Girl();
girl.fetchAge(3);

//fetchAge1
//fetchAge2
//evaluated-methodName fetchAge2
//evaluated-methodName fetchAge1
//Calling fetchAge1 with
//Calling fetchAge2 with

使用装饰器实现自动发布事件

import postal from 'postal';

function publish(topic, channel) {
    const channelName = channel || '/';
    const msgChannel = postal.channel(channelName);
    msgChannel.subscribe(topic, v => {
        console.log('频道: ', channelName);
        console.log('事件: ', topic);
        console.log('数据: ', v);
    });

    return function(target, name, descriptor) {
        const oldValue = descriptor.value;

        descriptor.value = function() {
            let value = oldValue.apply(this, arguments);
            msgChannel.publish(topic, value);
        };
    };
}


export default class Girl{
    constructor(props) {
        this.weight = "6.5斤";
        this.age = 1;
    }

    @publish('Girl.fetchWight', 'fetchWight')
    fetchWight(newWeight) {
        this.weight = newWeight;

        return this;
    }
}

var girl = new Girl();
girl.fetchWight('8.5斤');

其他使用场景:

@traits(TFoo, TBar::excludes('foo'))
class MyClass { }

@traits(TExample::excludes('foo','bar')::alias({baz:'exampleBaz'}))
class MyClass {}

@traits(TExample::as({excludes:['foo', 'bar'], alias: {baz: 'exampleBaz'}}))
class MyClass {}

react埋点插件整理

  1. react-tag-component

  2. trackpoint-tools

常见的埋点事件:

背景:刚接触这个需求的时候,如果面向过程的实现的时候,我们常常会把业务逻辑和埋点行为混为一谈;可能也尝试做了一些的变量或者函数的抽离,但是久而久之随着项目的拓展,相应的功能还是很难维护和可读。

场景1:页面加载埋点和格式化

//原始代码:
componentDidMount() {
    this.initPage();
    //...doSomething
    //然后进行页面初始化埋点:
   const properties={
      "page_name":"充值结果页",
      "btype": "对应的按钮类型",
      "project_type":"对应的项目类型",
    };
    pageViewEvent('result',properties)
}

//优化代码:
@boundPageView("testInit", "h5入口page", "testInit")
componentDidMount() {
    this.initPage();
    //...doSomething
}
//然后我们只要去思考怎么实现boundPageView;并增加了可读性。

场景2:按钮行为埋点

//原始代码; 比如某个banner点击
bannerClick = () => {
  //do banner click ...
  const eventInfo={
    "page_name":"结果页",
    "btn_name":"跳转结果页",
    "target_url":"",
    "btype": "对应的按钮类型",
    "project_type":"对象的项目类型",
  }
  trackEvent("banner_click",eventInfo)
  
  //todo somethings
};
//原始代码; 比如下单结果提交
payCommit = ()=>{
      //todo: 各种下单 操作
      const eventInfo={
        "sale_price":0,
        "creatorder_time": '2021-04-19 00: 00:00',
        "btype": "项目类型",
        //...其他好多参数
      }
      trackEvent("charge",eventInfo)
      //todo: 各种下单操作 
}

缺点:

//优化代码:
//针对简单的通用按钮点击事件埋点:
//直接约定固定的必要的参数
@trackBtnClick("btn_click", "testInit", '点击按钮', "testInit")
testBtnEvent = ()=>{
    console.log("testBtnEvent start");
}


//针对自定义参数的通用按钮点击事件埋点:
@trackBtnClickWithParam("btn_click", {
    "btn_name": "testInit",
    "btype": "点击按钮",
    "project_type": "testInit",
})
testBtnEventParams = ()=>{
    console.log("testBtnEventParams start");
}

//针对传值很多,且需要进行一些处理的参数;
//当前场景可以可以拆分主要实现,在装修器中传入通用的必要参数,
//其他细节参数,直接从转换好的参数中获取。
testPayEvent = (params)=>{
    console.log("testPayEvent start===>", JSON.stringify(params));

    this.payEventProcess({
        "sale_price": '',
        "original_price": '',
        "is_discontcoupon": false,
        "discountcoupon_id":'',
        "discountcoupon_price":0,
        "discountcoupon_name":'',
        "buy_num": 1,
        "charge_connect":"商品名称",
        "account": "充值账号",
        "creatorder_time":'2021-04-19 00:00:00',
    });
}

@trackBtnClickWithArgs("charge", {
    "project_type": "testInit",
    "btype": "test_recharge"
})
payEventProcess = (args)=>{
    console.log("testPayEvent end");
}

具体装饰器完整代码实现:

import curryN from 'lodash/fp/curryN'
import propSet from 'lodash/fp/set'
import isFunction from 'lodash/fp/isFunction'
// ...省略部分 埋点事件

/**
 * 绑定 页面初始化埋点
 * @param projectType
 * @param pageName
 * @param btype
 * @returns {function(*, *, *)}
 */
export function boundPageView(projectType, pageName, btype) {
    return (target, name, descriptor)=>{
        var fn = descriptor.value;
        descriptor.value = function() {
            console.log(projectType, pageName, btype);
            try{
                registerSuperProperty('project_type', projectType);
                const properties = {
                    "page_name": pageName,
                    "btype": btype,
                    "project_type": projectType,
                }
                pageViewEvent(pageName, properties);
            }catch (e) {
                console.log(e);
            }
            return fn.apply(this, arguments);
        };
        return descriptor;
    }
}

/**
 * 绑定按钮点击埋点
 * @param eventName
 * @param eventInfo
 * @returns {function(*, *, *)}
 *
 * @trackBtnClick(projectType, '用户访问首页', btype)
 * @track(before(() => {boundBtnClick(projectType, '用户访问首页', btype)}))
 */
export function boundBtnClick(eventName, eventInfo) {
    console.log(eventName,"=====》", eventInfo);
    trackEvent(eventName || "btn_click", eventInfo)
}

//普通按钮事件埋点
export function trackBtnClick(eventName, projectType, btnName, btype) {
    let partical = before((args)=>{
        const eventInfo={
            "btn_name": btnName,
            "btype": btype,
            "project_type":projectType,
        }
        boundBtnClick(eventName, eventInfo)
    })
    return track(partical)
}

//带自定义参数的按钮事件埋点
export function trackBtnClickWithParam(eventName, params) {
    let partical = before((args)=>{
        boundBtnClick(eventName, params)
    })
    return track(partical)
}

//带部分参数&&取参数作为自定义参数的按钮事件埋点
export function trackBtnClickWithArgs(eventName, params) {
    let partical = before((args)=>{
        boundBtnClick(eventName, {...args, ...params})
    })
    return track(partical)
}

//柯里化定义 埋点函数
export const before = curryN(2, (trackFn, fn) => function (...args) {
    // console.log(trackFn, fn);
    try {
        isFunction(trackFn) && trackFn.apply(this, args)
    } catch(e) {
        console.error(e)
    }

    return fn.apply(this, args)
})

//track 装饰器 ;执行相应的柯里化函数
export const track = partical => (target, key, descriptor) => {
    if (!isFunction (partical)) {
        throw new Error('trackFn is not a function ' + partical)
    }
    const value = function (...args) {
        return partical.call(this, descriptor.value, this).apply(this, args)
    }
    if (descriptor.initializer) {
        return propSet('initializer', function() {
            const value = descriptor.initializer.apply(this);
            return function (...args) {
                return partical.call(this, value, this).apply(this, args);
            }
        }, descriptor);
    }
    return propSet('value', value, descriptor)
}

以上,主要通过装饰器,实现在方法处理之前进行了一些优化;这边只是一个思路,像常见的事件防抖,日志的记录,在同步函数或者异步函数之后,定时任务,计算函数的执行时间等通用功能,都可以用装饰器巧妙的实现。

Spring项目中自定义注解的使用

场景: 自定义了一些api;但是想每个方法前实现一个开关控制;

1.创建自定义注解类:

import java.lang.annotation.*;

@Documented
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface EquityApiLock {

    String value() default "";
}

这里注解类上的三个注解称为元注解,其分别代表的含义如下:

2.创建面向切面类:

@Component
@Aspect
@Slf4j
public class EquityApiAspect {


    @Pointcut("@annotation(PKG.EquityApiLock)")
    private void pointcut() {}

    @Before("pointcut() && @annotation(equityApiLock)")
    public void advice(EquityApiLock equityApiLock) {
        log.info("EquityApiLock check==>{}", equityApiLock.value());
        String equityLock = ConfigService.getProperty("equity.apiLock", "1");
        if(StringUtils.equals("1", equityLock)){
            throw new BusinessException(CommonExceptionConstants.PARAM_INVALID);
        }
    }

}

3、使用:

    @EquityApiLock(value="getAllowances")
    @GetMapping("/getAllowances")
    public ResultInfo<String> getAllowances(HttpServletRequest httpServletRequest) {
       //todo: do somethings
    }

大功告成~

通过这个方式,还可以完善接口操作日志收集或者流控等场景。

上一篇下一篇

猜你喜欢

热点阅读