装饰器(Decorator)

装饰器(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);
  • 1、装饰器第一个参数是类的原型对象;上例是Girl.prototype
  • 2、第二个参数是所要装饰的属性名;
  • 3、第三个参数是该属性的描述对象;

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

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斤');

其他使用场景:

  • 1、core-decorators是一个第三方模块,提供了几个常见的装饰器,通过它可以更好地理解装饰器。
  • 2、Mixin ; “混入”
  • 3、Trait; traits-decorator也是一种装饰器,效果与 Mixin 类似,但是提供更多功能,比如防止同名方法的冲突、排除混入某些方法、为混入的方法起别名等等;Trait 不允许“混入”同名方法
@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: 各种下单操作 
}

缺点:

  • 1、业务逻辑跟埋点事件混为一谈;
  • 2、可读性,可拓展性差;
  • 3、事件的主次参数等不明确;
  • 4、代码冗余严重
    。。。
//优化代码:
//针对简单的通用按钮点击事件埋点:
//直接约定固定的必要的参数
@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;但是想每个方法前实现一个开关控制;

  • step1: 我们会很容易想到,定义一个通用的preCheck函数,然后每个api执行时,先预校验下;
  • step2: 自然就会想到如何用自定义注解完善呢?

1.创建自定义注解类:

import java.lang.annotation.*;

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

    String value() default "";
}

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

  • @Documented:注解信息会被添加到Java文档中
  • @Retention:注解的生命周期,表示注解会被保留到什么阶段,可以选择编译阶段、类加载阶段,或运行阶段
  • @Target:注解作用的位置,ElementType.METHOD表示该注解仅能作用于方法上

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
    }

大功告成~

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容