Angular中无处不在的注解(当然,更准确地说,注解是属于TypeScript而非Angular,只是通过Angular体现),它为何产生,又有何妙用?
注解的产生背景和意义——给你想要处理的程序打个标
上一篇文章中我介绍了“依赖注入”,说到了依赖注入框架,其实是个巨大的Map。那么,它如何记录他的键值对呢,比如一个在src/app/car/carService
的服务要告诉框架,它需要被管理,被在应用启动时就被构造出来,以Java为例,需要放在配置文件中,类似这样:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/jee
http://www.springframework.org/schema/jee/spring-jee.xsd">
<jee:jndi-lookup id="dataSource" jndi-name="jdbc/jpetstore"/>
<!-- 这就是被管理的对象 -->
<bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager" />
<!-- other <bean/> definitions here -->
</beans>
其中的bean的id就是键,class的路径,就是要让框架管理的全路径(唯一)的对象。
这样的作法当需要依赖注入框架管理的对象越来越多时,每加一个bean都记录它的全路径,填在这里,很复杂,这也是10年前的人难以入门JavaWeb的原因,过多的配置、以及引如jar包、排除jar包冲突等事情要做,自学的话,把项目启动起来都费劲。
而Java在JDK 1.5版本中刚好提供了注解
的能力。设计它的本意是一种注释机制
,比如,我的一个方法过时了,不希望别人再用了,可以这样写
// 添加这个注解表明此方法已过时
@Deprecated
public void getReadedDirPath() {
// ...
}
public void getReadDirPath() {
}
这样别人调用getReadedDirPath这个方法时,就会有编译警告,IDE上也有相应提示,如图
也就是说,注解的实质是标注,作用是可以在程序运行的任何时刻获取你标注的类、方法、属性的信息
,而借助这个能力,很多框架开始优化它的配置文件,以Spring为例,它是一个依赖注入框架,既然要管理的对象需要知道它的全路径,是否可以以注解
的手段代替它,进而省去繁杂的配置文件?当然是可以的,于是Spring这么做了,Hibernate、Mybatis等等所有现在还会被使用到的JavaWeb开发框架,都这么做了。于是,代码就有了这样的变化。
别的不说,至少代码清晰多了,要配置的内容也少了。
来都来了,只打个标吗——代码增强
上面介绍了Java中的注解,可以实现打标的能力,那么打标有什么好的使用场景吗?
有的。那就是代码增强。
既然可以在程序运行过程中拿到打标的内容,我们可以在这个时机加入一些我们的逻辑,就是代码增强。举个例子,比如你需要在所有Java的控制器方法中加入日志,最佳实践是这样做:
// 先定义一个自定义注解,不用在乎代码的含义,知道他定义了个SysLog的注解就好
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SysLog {
String value() default "";
}
在某些代码中打标
/**
* 一个修改登录用户密码的方法,把@SysLog注解加在这里
*/
@SysLog("修改密码")
@RequestMapping("/password")
public R password(String password, String newPassword){
// 业务逻辑
...
}
找到打标内容,执行增强的代码
// 不需要看懂代码,只需要看我的注释
@Aspect
@Component
public class SysLogAspect {
@Autowired
private SysLogService sysLogService;
// 处理哪个标,也即处理哪种注解
@Pointcut("@annotation(com.esurer.common.annotation.SysLog)")
public void logPointCut() {
}
// 遇到上面的注解,用这个方法处理
@Around("logPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
long beginTime = System.currentTimeMillis();
Object result = point.proceed();
long time = System.currentTimeMillis() - beginTime;
saveSysLog(point, time);
return result;
}
private void saveSysLog(ProceedingJoinPoint joinPoint, long time) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
SysLogEntity sysLog = new SysLogEntity();
SysLog syslog = method.getAnnotation(SysLog.class);
if(syslog != null){
// 拿到注解上的描述
sysLog.setOperation(syslog.value());
}
String className = joinPoint.getTarget().getClass().getName();
String methodName = signature.getName();
sysLog.setMethod(className + "." + methodName + "()");
Object[] args = joinPoint.getArgs();
try{
String params = new Gson().toJson(args[0]);
sysLog.setParams(params);
}catch (Exception e){
}
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
sysLog.setIp(IPUtils.getIpAddr(request));
String username = ((SysUserEntity) SecurityUtils.getSubject().getPrincipal()).getUsername();
sysLog.setUsername(username);
sysLog.setTime(time);
sysLog.setCreateDate(new Date());
//保存系统日志
sysLogService.save(sysLog);
}
}
上面的例子说明,在Java中,注解的作用就是打标,它的最佳实践是给代码增强“打辅助”,它不能单独完成代码增强
, 那么,在TypeScript中,注解的使用又是怎样的?
TS中的注解怎么理解——打标+代码增强,我全都要
我相信看完上面的涵盖注解的Java代码,前端的同学都会觉得过于复杂了,其实它复杂的原因只有一个——Java是不能把函数作为参数的
。打过的标,要怎么处理,需要另外引入一个类(类似上个例子中最后一段很长的代码),将他们关联起来处理。那么,在灵活的JS/TS中,函数可以作为参数,它写起代码来是怎么样的呢?
举个很经典的例子:我需要用两个😂
的表情去包裹某个属性的值,当然,可以手动去改,只是如果要改的地方很多,可能比较麻烦。如果你将它看作是一种增强,可以这样写:
export class AppComponent {
// 想对flavor这个属性做个增强
@Emoji()
flavor = 'vanilla';
}
将注解打标和代码增强合起来,写这样一个方法:
function Emoji() {
// target相当于是上面的AppComponent,TS中的类都类似一个字典对象,key相当于是其索引值(属性值)
return function(target: Object, key: string | symbol) {
// 所以这样可以拿到上述目标对象类的被打标属性
let val = target[key];
const getter = () => {
return val;
};
const setter = (next) => {
// 这里就相当于是代码增强
val = `😂${next}😂`;
};
// 这里类似MVVM框架,令你可以实现对象劫持,对它的设值方法劫持、增强
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true,
});
};
}
这样写代码的好处往小了说,就是修改所有被@Emoji()修饰的表情时,很方便,比如需要把😂
替换成😎
的时候,只需要改上述一处代码即可。简单来说,TS中的注解,实现了打标+代码增强的能力,可以把它看成一个函数,它也可以接收函数参数。
。既然可以接收函数参数,也用一个经典的例子演示下:
需求:假如你的前端代码有些重要业务,现在要让他们在触发前都出现一个二次确认是否继续的弹窗。
实现:通过注解增强
export class AppComponent {
@Confirmable('Are you sure?')
handleClick() {
// 业务逻辑
console.log("had clicked.");
}
}
添加它的相应方法
export function Confirmable(message: string) {
// 类似的,这里需要返回一个函数,多了个入参,descriptor,属于js中的知识,通过它可以拿到一个对象中每个属性的描述符,拿到它
// 就可以拿到这个方法执行的“时机”,进而对它进行增强
return function (target: Object, key: string | symbol, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function( ... args: any[]) {
// 这里就是增强,添加弹窗
const allow = window.confirm(message);
if (allow) {
// 弹窗中点击确认,继续执行原本的方法
const result = original.apply(this, args);
return result;
} else {
return null;
}
};
return descriptor;
};
}
代码增强到底是什么——持续优化
上面我提到很多次代码增强,那么到底代码增强是什么,还有前面举得Java代码的例子,熟悉Java的同学都知道,它叫AOP,它的本质又是什么?
可以从一个很简单的例子做解释:
我相信很多同学走上程序员这条路的第二步(第一步是打印helloworld)都是编写这样一道题:三角形打印
题目类似这样
给定一个n,打印n阶三角形(类似下图,n=5)
考虑到题目的严谨性,这个n应该有个范围,比如是1-5,那么这道题我们完全可以使用穷举法,把每种n的取值对应的结果直接通过print之类的函数打出来就行了。但是为什么大家都不这样写?
因为太麻烦了,而且,没有什么乐趣,反之,如果你观察到它的规律
,发现根据n的值可以组合空格和星号排列,这样写出的代码,很简练,也易于在需求变化时修改
for (i = 0; i < n; i++) {
for (j = 0; j < (n - i); j++) {
printf(" ");
}
for (j = 0; j < 2 * i + 1; j++) {
printf("*");
}
printf("\n");
}
同样的道理,如果你写了很多业务代码,举个例子,很多业务都需要写查询数据库的代码,如果之前的代码都是直接查询数据库的,而现在鼓励把缓存当数据库,查询数据库之前先查缓存,相当于要在原本的代码基础上通过修改做个增加,当然,你可以把所有业务对象的代码都改下,亦或者观察它的规律
,对于所有同样要增加的操作
,是不是可以通过更简练的方式
增强它的能力,而不是每个业务代码的多次复制粘贴?如下图,所有的原本业务之前之后可以统一的增强。
这就是标准的AOP(面向切面编程),其中切面的意思就是,把你的业务想象成一个类似“汉堡”的结构,通过纵切,可以看清里面的每一层都是什么
观察整个汉堡的切面,就相当于在观察你的代码逻辑了,如果有个逻辑,需要添加到你的多处业务中,不妨试试使用注解去增强你的代码。
没有最好,只有最适合你的
总结一下:这篇文章主要想给大家介绍注解的用途、原理,
注解的最佳实践就是打标加代码增强。实现这个的步骤往往是:
- 找到注解打标的代码;
- 找到要执行增强代码的时机;
- 织入增强代码;
其中“找到要执行增强代码的时机”这一步,往往源于你对业务的理解,比如“纵切”你的代码,通过观察切面寻找规律,进而通过注解引起上述三个动作的触发。
不过,以我最开始举的使用注解替代配置文件的例子来说,使用注解是比配置文件简化很多,但是同样它多了耦合。因为配置文件本来是跟代码分离的,使用注解则是将这个关系耦合到代码中了,所以,不是任何时刻注解都优于配置文件的
。同样,后面介绍的AOP,本质上是对代码的增强,这个可以理解为一种模式,我觉得这种模式是值得大家了解的,但不是任何时候都必须要使用模式,没有必要生搬硬套
。就像前两天我们团队的同学在讨论,个别设计模式有什么异同,其实我感觉并不需要区分的那么清楚,知道每种模式为什么那么写代码就好。比如责任链模式,它的特点是能写出类似obj.handle().handle()
的代码,那么可以不关注一个场景是不是责任链模式,而是在你像写出类似代码的时候,知道需要通过继承,实现handle方法,并返回obj对象,即可,这样就能保证你的链式调用继续下去。你可以反模式,可以用你喜欢的方式去解决你遇到的问题,寻找最适合你的方式去“野蛮生长”。说到模式,Rx.js中倒是有不少设计模式,下一篇文章我们会给大家简单介绍下Rx.js如何理解,以及它的设计理念。
最后
如果你对这些WEB前沿技术也有兴趣,欢迎你对我们的文章一键三联,以及关注我们接下来的开源项目————OpenTiny。欢迎微信搜索我们的小助手:opentiny-official
,拉你进群,了解它最新的动态