装饰器(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);
- 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埋点插件整理
-
react-tag-component
-
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
}
大功告成~
通过这个方式,还可以完善接口操作日志收集或者流控等场景。