本文目的:

  • 理解并能按需使用各种事件绑定 API
  • 理解事件对象
  • 理解事件流

事件绑定

事件是用户或浏览器自身执行的某种动作,例如 onclickonload ,都是事件的名字。而响应某个事件的函数就叫做 事件处理程序(Event Handlers)。为事件绑定处理程序的方式有以下几种:

HTML

做法:在 HTML 元素中直接编写事件处理程序:

1
2
3
4
5
6
7
8
<!-- 输出“Clicked” —— 事件处理程序中,可以直接编写 JavaScript 代码 -->
<input type="button" value="Click Me" onclick="alert('Clicked')" />

<!-- 输出“click” —— 事件处理程序中,可以直接访问事件对象 event -->
<input type="button" value="Click Me" onclick="alert('event.type')" />

<!-- 输出“Click Me” —— 事件处理程序中,this 指向事件的目标元素 -->
<input type="button" value="Click Me" onclick="alert('this.value')" />

其中除了可以编写 JavaScript 代码,还可以调用外部脚本:

1
2
3
4
5
6
7
8
<!-- 事件处理程序中的代码在执行时,有权访问全局作用域中的任何代码 -->
<input type="button" value="Click Me" onclick="showMessage()" />

<script type="text/javascript">
function showMessage() {
alert("Hello world!");
}
</script>

特点:上述 onclick 事件将自动产生一个事件处理程序(函数),例如:

1
2
3
function onclick(event) {
alert('Clicked')
}

优点:简单、粗暴,浏览器兼容性好。

缺点:

  • 存在时差问题。用户可能会在 HTML 元素一出现在页面上时,就触发相应事件,但当时的事件处理程序有可能还未具备执行条件(例如事件处理程序所在的外部脚本文件还未加载或解析完毕),此时会引发 undefined 错误。
  • HTML 与 JavaScript 代码紧密耦合。如果要重命名事件处理程序,就要改动两个地方,容易改漏、改错。

DOM Level 0

做法:首先获取目标 HTML 元素的引用,然后将一个事件处理程序赋值给其指定的事件属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<input type="button" id='btn' value="Click Me" />

<script type="text/javascript">
var btn = document.getElementById('btn');

// 绑定事件处理程序
btn.onclick = function() {
alert('Clicked');
alert(this.id); // 输出“myDiv” —— this 指向事件的目标元素
}

// 删除事件处理程序
btn.onclick = null;
</script>

特点:本质上,DOM 0级事件处理程序 等于 HTML 事件处理程序,例如:

1
2
3
4
5
6
7
8
9
<input type="button" id='btn' value="Click Me" onclick="alert('Clicked')" />

<script type="text/javascript">
setTimeout(function() {
var btn = document.getElementById('btn');
alert(typeof btn.onclick); // 通过 HTML 的事件属性,访问其 HTML 事件处理程序,并输出其类型“function”
btn.onclick = null; // 几秒后,将会删除该按钮的事件处理程序
}, 3000);
</script>

优点:

  • 传统、常用、浏览器兼容性好。
  • 解决了 HTML 事件处理程序的两个缺点。

缺点:一个事件只能绑定唯一一个事件处理程序。

DOM Level 2

做法:目前最主流的写法,可以支持事件冒泡或捕获:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<input type="button" id='btn' value="Click Me" />

<script type="text/javascript">
var btn = document.getElementById('btn'),
showMessage = function() {
alert('Clicked');
alert(this.id); // 输出“myDiv” —— this 指向事件的目标元素
};

// 绑定事件处理程序。false 表示在“冒泡阶段”和“目标阶段”触发
btn.addEventListener('click', showMessage, false);

// 删除事件处理程序。注意,匿名函数无法移除
btn.removeEventListener('click', showMessage, false);
</script>

优点:一个事件可以绑定多个事件处理程序,以绑定的顺序执行。

缺点:浏览器兼容性差,IE8 及以下版本不支持。

IE

IE 实现了与 DOM 2 级类似的两个方法,只支持事件冒泡:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<input type="button" id='btn' value="Click Me" />

<script type="text/javascript">
var btn = document.getElementById('btn'),
showMessage = function() {
alert('Clicked');
alert(this === window); // 输出“true” —— 注意 this 指向 window
};

// 绑定事件处理程序。仅在“冒泡阶段”和“目标阶段”触发
btn.attachEvent('onclick', showMessage);

// 删除事件处理程序。注意,匿名函数无法移除
btn.detachEvent('onclick', showMessage);
</script>

优点:一个事件可以绑定多个事件处理程序,以绑定的顺序 逆序 执行。

缺点:浏览器兼容性差,仅支持 IE 及 Opera。

Cross-Browser

鉴于上述几种方式的各有优劣,为了以跨浏览器的方式处理事件,可以定义自己的 EventUtil

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var EventUtil = {

addHandler: function(element, type, handler){
if (element.addEventListener){
element.addEventListener(type, handler, false);
} else if (element.attachEvent){
element.attachEvent(“on” + type, handler);
} else {
element[“on” + type] = handler;
}
},

removeHandler: function(element, type, handler){
if (element.removeEventListener){
element.removeEventListener(type, handler, false);
} else if (element.detachEvent){
element.detachEvent(“on” + type, handler);
} else {
element[“on” + type] = null;
}
}

};

事件对象

在触发 DOM 上的某个事件时,会产生一个事件对象 event ,这个对象中包含着所有与事件有关的信息。尽管触发的事件类型不同,可用属性和方法也会不同,但是所有事件都会包含下列常用成员:

DOM Level 2 Type IE Type Description
type String type String 被触发的事件类型
eventPhase Integer - - 调用事件处理程序的所处阶段:1 表示捕获阶段,2 表示“处于目标”,3 表示冒泡阶段
target Element srcElement Element 事件的目标元素
currentTarget Element - - 当前正在处理事件的元素。如果事件处于目标元素,则 this === currentTarget === target
stopPropagation() Function cancelBubble Boolean 取消事件的进一步捕获或冒泡
preventDefault() Function returnValue Boolean 取消事件的默认行为

事件流

最后总结下与事件处理程序息息相关的“事件流”。事件流是指从页面中接收事件的顺序。但有意思的是,历史上 IE 和 Netscape 开发团队居然提出了 完全相反 的事件流概念 —— IE 使用“事件冒泡(Event Bubbling)”、Netscape 使用“事件捕获(Event Capturing)”。下图演示了这两种事件流的区别:

事件流(Event Flow)

下表列出了四种事件绑定所使用的事件流模型:

事件冒泡 or 事件捕获?
HTML 取决于 IE or Netscape
DOM Level 0 取决于 IE or Netscape
DOM Level 2 事件冒泡 + 事件捕获
IE 事件冒泡

下面重点讲解 DOM Level 2 事件处理程序所规定的事件流,其共包含三个阶段(其运行效果如上图从 1 到 10):

  1. 事件捕获阶段,可用于事件截获
  2. 处于目标阶段
  3. 事件冒泡阶段,可用于事件委托(Event Delegation)

下面这段代码演示了 DOM Level 2 的整个事件流:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!DOCTYPE html>
<html>
<body>
<input type="button" id='btn' value="Click Me" />

<script type="text/javascript">

// 仅在“事件捕获阶段”和“处于目标阶段”触发
document.body.addEventListener('click', function(event){
alert(event.eventPhase + ' body');
}, true);

// 仅在“事件冒泡阶段”和“处于目标阶段”触发
document.getElementById('btn').addEventListener('click', function(event){
alert(event.eventPhase + ' input');
}, false);

// 仅在“事件冒泡阶段”和“处于目标阶段”触发
document.addEventListener('click', function(event){
alert(event.eventPhase + ' document');
}, false);

</script>

</body>
</html>

点击 input 按钮,将依次输出:

1
1 body
2 input
3 document

可见,DOM Level 2 是同时支持事件冒泡 + 事件捕获的。

参考

  • 《JavaScript 高级程序设计》

一年前,为了优化这个博客的访问速度,我将 Pages 服务迁移 到了 GitCafe,没想到一年后 GitCafe 竟被 codeing.net 收购了,其服务将在五月底全面停止,真是令人叹息。

幸好 Coding Pages 支持免费绑定自定义域名,其配置也非常简单。在完成配置之后,只需要到 DNSPod 切换下 cname ,等待 DNS 解析生效即可。整个过程对网站用户透明。

最后,这里 列举了不少知名的 Pages 服务可供选择。不过对于国内用户来说,还是使用国内服务最快、最稳定。

工作中常用到一些并发编程类,在此做了一些简单、系统的总结。

JDK 包简介

JDK 中涉及到线程的包如下:

java.lang

内含基础并发类。

java.util.concurrent

JDK 5 引入的 Executor Framework ,用于取代传统的并发编程。

java.util.concurrent.locks

用于实现线程安全与通信。

java.util.concurrent.atomic

使用这些数据结构可以避免在并发程序中使用同步代码块。

java.lang 基础类

Runnable

异步任务需实现的接口。

Thread

程序中的执行线程。

属性

Thread 对象中保存了一些属性能够帮助我们来辨别每一个线程,知道它的状态,调整控制其优先级等。

ID

每个线程的独特标识。

Name

线程的名称。

Priority

线程对象的优先级。优先级别在 1-10 之间,1 是最低级,10 是最高级。不建议改变它们的优先级。

Daemon

是否为守护线程。

Java 有一种特别的线程叫做守护线程。这种线程的优先级非常低,通常在程序里没有其他线程运行时才会执行它。当守护线程是程序里唯一在运行的线程时,JVM 会结束守护线程并终止程序。

根据这些特点,守护线程通常用于在同一程序里给普通线程(也叫使用者线程)提供服务。它们通常无限循环的等待服务请求或执行线程任务。它们不能做重要的任务,因为我们不知道什么时候会被分配到 CPU 时间片,并且只要没有其他线程在运行,它们可能随时被终止。JAVA中最典型的这种类型代表就是垃圾回收器 GC

只能在 start() 方法之前可以调用 setDaemon() 方法。一旦线程运行了,就不能修改守护状态。

可以使用 isDaemon() 方法来检查线程是否是守护线程。

Thread.State

线程的状态,共六种:
NEW
RUNNABLE
BLOCKED
WAITING
TIME_WAITING
TERMINATED

Thread.UncaughtExceptionHandler

用于捕获和处理线程对象抛出的 Unchecked Exception 来避免程序终结。

方法

Thread 类提供了以下几类方法:

  • 线程协作
  • 线程中断
  • 线程让步
  • 线程睡眠
  • 线程合并
  • ……

ThreadLocal

ThreadLocal 存放的值是线程内共享的,线程间互斥的,主要用于在线程内共享一些数据。

ThreadGroup

java.util.concurrent

两种异步任务

无返回结果的异步任务

使用常规的 java.lang.Runnable

有返回结果的异步任务

Executor Framework 的一个重要优点是提供了 java.util.concurrent.Callable<V> 接口用于返回异步任务的结果。它的用法跟 Runnable 接口很相似,但它提供了两种改进:

  • 这个接口中主要的方法叫 call() ,可以返回结果。
  • 当你提交 Callable 对象到 Executor 执行者,你可以获取一个实现 Future 接口的对象,你可以用这个对象来控制和获取 Callable 对象的状态和结果。

线程池

为什么要用线程池?

线程的创建和销毁是有代价的。

如果请求的到达率非常高且请求的处理过程是轻量级的,那么为每个请求创建一个新线程将消耗大量的计算资源。

活跃的线程会消耗系统资源,尤其是内存。如果可运行线程数量多于可用处理器数量,则有些线程会被闲置;大量空闲线程会占用许多内存,给垃圾回收器带来压力,而且大量线程竞争 CPU 资源还会产生其它的性能开销。

可创建线程的数量上存在限制,如果创建太多线程,会使系统饱和甚至抛出 OutOfMemoryException

为了解决以上问题,从 Java 5 开始 JDK 并发 API 提供了 Executor Framework。核心接口是 Executor ,其子接口是 ExecutorService ,而 ThreadPoolExecutor 类则实现了这两个接口。

Executor Framework 主要用于将任务的创建与执行分离,避免使用者直接与万恶的 Thread 对象打交道。

使用 Executor Framework 的第一步就是创建一个 ThreadPoolExecutor 类的对象。你可以使用这个类提供的 四个构造方法Executors 工厂类来创建 ThreadPoolExecutor 。一旦有了执行者,你就可以提交 RunnableCallable 对象给执行者来执行。

使用 Executors 工厂类构造线程池

java.util.concurrent.Executors

ThreadPoolExecutor 虽然有四个不同的构造方法,但由于它们的复杂性(参数较多),Java 并发 API 提供 Executors 工厂类来构造执行者和其他相关对象,推荐使用。

常用方法:

1
newCachedThreadPool(...)
newFixedThreadPool(...)
newSingleThreadExecutor(...)
newScheduledThreadPool(...)

使用构造方法定制线程池

以参数最多的构造方法为例,理解下各参数的用途:

1
2
3
4
5
6
7
8
ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)

ThreadPoolExecutor

注意点:

  • 整个线程池的基本执行过程:创建初始线程 > 线程排队 > 创建扩容线程
  • 如果设置的 corePoolSize 和 maximumPoolSize 相同,则创建了固定大小的线程池。
  • 如果将 maximumPoolSize 设置为基本的无界值(如 Integer.MAX_VALUE),则允许池适应任意数量的并发任务。
  • 通过提供不同的 ThreadFactory 接口实现,可以改变被创建线程的名称、线程组、优先级、守护进程状态,等等。
  • 任务排队有三种通用策略,通过 BlockingQueue 接口可以实现更多策略。
  • 任务拒绝有四种预定义策略,通过 RejectedExecutionHandler 接口可以实现更多策略。

最后

本文暂不涉及:

  • 线程同步/锁的理论知识和 API 操作,这是一个很大的话题,需要后续另起一篇来总结。
  • 第三方并发工具或框架,如 Apache Commoms Lang 的 Concurrent 部分,如 Spring Framework 的一些并发类,使用它们可以简化并发编程。

本文演示如何动态加载脚本。即脚本在页面加载时不存在,但将来的某一时刻通过修改 DOM 动态添加脚本,从而实现按需加载脚本。

加载脚本文件

1
2
3
4
5
6
7
8
function loadScriptFile(url) {
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;

// 在执行到这行代码将 <script> 元素添加到页面之前,不会下载指定外部文件
document.body.appendChild(script);
}

内联脚本代码

1
2
3
4
5
6
7
8
function loadScriptString(code) {
var script = document.createElement('script');
script.type = 'text/javascript';
script.text = code;

// 在执行到这行代码将 <script> 元素添加到页面之前,不会下载指定外部文件
document.body.appendChild(script);
}

以这种方式加载的代码会在全局作用域中执行,而且当脚本执行后将立即可用。实际上,这样执行代码与在全局作用域中把相同的字符串传递给 eval() 是一样的。

有了一套成熟的分支模型以及配套的权限控制之后,接下来我们以一个例子来演示如何实践这套流程。

分支模型实践

创建版本分支

首先,项目管理员(Master)从 master 分支中创建出版本分支 release-* 进行新版本的开发,* 为发布日期:

1
2
3
4
5
$ git checkout -b release-20190101

do something and commit...

$ git push origin release-20190101

合并分支

版本开发完毕,Master 需要整理版本分支(例如从中挑选出能够发版的提交,剔除掉不能发版的提交),合并回 master 分支并进行发版。

标记新版本

发版完毕,Master 打 Tag 标记该新版本,以便后续回顾:

1
2
$ git tag release-20190101 -m "XX 项目 v1.0 版本"
$ git push origin release-20190101

注意,在默认情况下,git push 并不会把标签(tag)推送到远端仓库上,只有通过显式命令才能分享标签到远端仓库。其命令格式如同推送分支,运行 git push origin [tagname] 即可。如果要一次推送所有本地新增的标签上去,可以使用 --tags 选项。

清理分支

最后是一些清理工作,Master 需要删除已完成开发的版本分支,避免分支越来越多导致不好管理:

1
2
$ git branch -d release-20190101
$ git push --delete origin release-20190101

最后,列出所有远程和本地分支确认下:

1
$ git branch -a

创建特性分支

首先,开发人员(Developer)从 master 分支中创建出特性分支:

1
2
3
4
5
$ git checkout -b feature-test

do something and commit...

$ git push origin feature-test

定期合并

由于特性分支可能会跨版本开发,因此需要定期维护:主要的工作就是定期将 master 分支合并进来,保持同步。

决断代码

特性分支开发完成之后,如果想要筛选出将要被合并的提交有哪些,可以参考这里

总结

代码提交指南

  • 请不要在更新中提交多余的白字符(whitespace)。Git 有种检查此类问题的方法,在提交之前,先运行 git diff --check ,会把可能的多余白字符修正列出来。
  • 请将每次提交限定于完成一次逻辑功能。并且可能的话,适当地分解为多次小更新,以便每次小型提交都更易于理解。
  • 最后需要谨记的是提交说明的撰写。可以理解为第一行的简要描述将用作邮件标题,其余部分作为邮件正文。

分支管理指南

  • 主分支 master 一般不提交代码,只合并代码。
  • 各特性分支要定期将 master 分支合并进来,避免后续处理合并请求时产生冲突,以减轻项目管理员的工作负担。
  • 发版之后,项目管理员要记得打 tag 。

参考

除了 Git 命令,权限控制也是 Git 中极为重要的组成部分,本文主要介绍 GitLab 系统提供的最常用的权限控制功能。

分配成员角色

首先来了解下,Git 中的五种角色:

角色 描述
Owner Git 系统管理员
Master Git 项目管理员
Developer Git 项目开发人员
Reporter Git 项目测试人员
Guest 访客

每一种角色所拥有的权限都不同,如下图:

Git 权限控制

我们需要做的是,为项目成员分配恰当的角色,以限制其权限。

锁定受保护分支

在对 Git 不熟悉的时候,时常苦恼于各个分支不受约束,任何开发人员都可以向任何分支直接推送任何提交,各种未经审查的代码、花样百出的 Bug 就这样流窜在预发布分支上。

其实我们可以通过 GitLab 的受保护分支(Protected Branches)功能解决该问题,该功能可用于:

  • 阻止 Master 角色以外的开发人员直接向此类分支推送代码,保持稳定分支的安全性;
  • 在向受保护分支合并代码前,强制进行代码审查。

接下来我们就使用这项功能,锁定我们的受保护分支——主分支 master 和预发布分支 release-*,以阻止 Developer 直接向这两类分支中推送代码:

Git 受保护分支

锁定后,Developer 推送代码将会报错:

1
$ git push origin master
Counting objects: 4, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 283 bytes | 0 bytes/s, done.
Total 3 (delta 1), reused 1 (delta 0)
remote: GitLab: You are not allowed to access master!
remote: error: hook declined to update refs/heads/master
To git@website:project.git
 ! [remote rejected] master -> master (hook declined)
error: failed to push some refs to 'git@website:project.git'

发起合并请求

锁定受保护分支后,要么 Master 需要时刻、主动关注各特性分支的进度,要么 Developer 需要线下、口头向 Master 汇报其特性分支的进度,这两种做法都非常不便于 Master 管理每个预发布分支的合并,尤其在团队大、分支多的情况。

我们可以通过 GitLab 的发起合并请求(Merge Request)功能解决该问题,这样既可以让 Developer 更自如的掌控自己分支进度,在必要的时候才主动发起合并请求;又可以减轻 Master 的合并工作量和沟通成本,可谓一举两得。

新建合并请求

第一步,按表单要求填写合并请求。注意,对于 Developer 而言:

  • From 是你的特性分支 feature-*
  • To 只可能是预发布分支 release-*
  • TitleDescription 要填写恰当的分支描述;
  • Assign to 是该项目的 Master。

新建合并请求

审查合并请求

第二步,Master 收到合并请求后,进行代码审查。逐一查看 Commits 一栏提交的内容即可,对于需要改进的代码,可以直接在该行添加注释,非常方便。

接受合并请求

如果对整个请求还有疑问的地方,还可以通过底部的 Discussion 功能进行线上讨论。

处理合并请求

第三步,针对审查结果进行相应处理:

关闭

对于完全不合格的垃圾代码、或者废弃的特性分支的合并请求,Master 点击右上角的 Close 按钮即可。合并请求将被关闭,相当于扔进回收站。

改进

对于分支内需改进的代码,Developer 直接修正并推送即可,合并请求将会自动包含最新的推送提交。

接受

Master 审查无误后,可以接受该次合并请求。点击 Accept Merge Request 按钮将自动合并分支,勾选 Remove source-branch 将同时删除该特性分支。

整个自动合并过程如果以命令形式手工执行的话,步骤如下:

1
2
3
4
5
6
7
8
#Step 1. Update the repo and checkout the branch we are going to merge 
git fetch origin
git checkout -b test origin/feature-test

#Step 2. Merge the branch and push the changes to GitLab
git checkout release-2016.4.7
git merge --no-ff feature-test
git push origin release-2016.4.7

非快进式合并完成后,祖先图谱(graph)的展现结果如下:

1
*   be512fa (HEAD, origin/release-2016.4.7, release-2016.4.7)  Merge branch 'test' into 'release-2016.4.7'
|\
| * 1f52adf 测试
|/
*   a4febbb (tag: 1.0.0, origin/master) 格式化货币保留两位小数

最后需要注意的是,只有 Assignee 才能够接受合并请求,其它人只会被通知:

You don’t have permission to merge this MR

总结

GitLab 提供的上述功能非常实用,为项目的源码管理提供了有力的支持。

项目总归要协作开发,在此总结我在团队中推广使用的分支模型。

A successful Git branching model

分支模型

主分支(Main branches)

企业的项目开发不像开源的项目开发,通常只会有一个远程仓库。这种情况下,通常会有两个常驻分支:

Branch Name Is locked? Description
master YES 主干分支,仅用于发布新版本,平时不能在上面干活,只做代码合并、以及打标记(git tag)。
理论上,每当对 master 分支有一个合并提交操作,我们就可以使用 Git 钩子脚本来自动构建并且发布软件到生产服务器。
dev NO 开发分支,平时干活的地方。每当发版时,需要被合并到 master

对于简单的项目而言,这样的分支模型已经够用了。

辅助性分支(Supporting branches)

除了常驻分支,通常大的特性开发或生产缺陷修复还建议创建相应的临时分支。因为:

  1. 在分支上开发可以让你随意尝试,进退自如,比如碰上无法正常工作的特性或补丁,可以先搁在那边,直到有时间仔细核查修复为止。
  2. 团队中如果有代码审查流程,独立的分支还可以留给审查者抽空审查的时间和改进代码的余地,并将是否合并、是否发布的权利留给审查者,为代码质量设一道门槛。

每一类分支都有一个特定目的,如何命名每一类分支?建议用相关的主题关键字进行命名,并且建议将分支名称分置于不同命名空间(前缀)下,例如:

Branch Name May branch off from Must merge back into Is locked? Description
feature-* dev dev NO 特性分支,为了开发某种特定功能而建。
release-* dev dev
master
YES 预发布分支,为了新版本的发布做准备,一般命名为 release-<版本号>
hotfix-* master dev
master
NO 补丁分支,为了修复生产缺陷而建,一般命名为 hotfix-<issue 编号>

与主分支不同,这些辅助性分支总是有一个有限的生命期,因为他们在被合并到主分支之后,就会被移除掉。

参考

对比两个分支中,所有文件的详细差异,常用于合并操作之后确认有没有遗漏文件:

1
$ git diff branch1 branch2

对比两个分支中,指定文件的详细差异:

1
$ git diff branch1 branch2 文件名(带路径)

对比两个分支中,差异的文件列表:

1
$ git diff branch1 branch2 --stat

由于项目中散落着各种使用缓存的代码,这些缓存代码与业务逻辑代码交织耦合在一起既编写重复又难以维护,因此打算将这部分缓存代码抽取出来形成一个注解以便使用。

这样的需求最适合用 AOP 技术来解决了,来看看如何在 Spring 框架下使用 AOP 技术:

开启注解扫描

首先开启 Spring 注解扫描:

1
<context:component-scan base-package="your.package" />

以及开启 @AspectJ 切面注解扫描:

1
<aop:aspectj-autoproxy proxy-target-class="true" />

编写注解

然后使用 Java 语法编写一个注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package your.package;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* 方法级缓存
* 标注了这个注解的方法返回值将会被缓存
*/

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MethodCache {

/**
* 缓存过期时间,单位是秒
*/

int expire();

}

切面(Aspect)

最后是编写一个切面。注:如果你对切面的概念已经很清楚,可以跳过本小结。

什么是切面?通俗来说就是“何时何地发生何事”,其组成如下:

Aspect = Advice (what & when) + Pointcut (where)

其执行过程如下:

An aspect’s functionality (advice) is woven into a program’s execution at one or more join points.

通知(Advice)

通知(Advice)定义了何时(when)发生何事(what)

Spring AOP 的切面(Aspect)可以搭配下面五种通知(Adive)注解使用:

通知 描述
@Before The advice functionality takes place before the advised method is invoked.
@After The advice functionality takes place after the advised method completes, regardless of the outcome.
@AfterReturning The advice functionality takes place after the advised method successfully completes.
@AfterThrowing The advice functionality takes place after the advised method throws an exception.
@Around The advice wraps the advised method, providing some functionality before and after the advised method is invoked.

切点(Pointcut)

切点(Pointcut)定义了切面在何处(where)执行。

Spring AOP 的切点(Pointcut)使用 AspectJ 的“切点表达式语言(Pointcut Expression Language)”进行定义。但要注意的是,Spring 仅支持其中一个子集:

切面指示器(Aspectj Designator)

切点表达式的语法如下:

切点表达式(Pointcut Expression)

完成切面

使用注解来创建切面,是 AspectJ 5 所引入的关键特性。在 AspectJ 5 之前,编写 AspectJ 切面需要学习一种 Java 语言的扩展,很不友好。在此我们使用注解来实现我们的切面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package your.package;

import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;

import org.apache.commons.lang.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.kingdee.finance.cache.service.centralize.CentralizeCacheService;

/**
* 方法级缓存拦截器
*/

@Aspect
@Component
public class MethodCacheInterceptor {

private static final Logger logger = LoggerFactory.getLogger("METHOD_CACHE");
private static final String CACHE_NAME = "Your unique cache name";

@Autowired
private CentralizeCacheService centralizeCacheService;

/**
* 搭配 AspectJ 指示器“@annotation()”可以使本切面成为某个注解的代理实现
*/

@Around("@annotation(your.package.MethodCache)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
String cacheKey = getCacheKey(joinPoint);
Serializable serializable = centralizeCacheService.get(CACHE_NAME, cacheKey);
if (serializable != null) {
logger.info("cache hit,key [{}]", cacheKey);
return serializable;
} else {
logger.info("cache miss,key [{}]", cacheKey);
Object result = joinPoint.proceed(joinPoint.getArgs());
if (result == null) {
logger.error("fail to get data from source,key [{}]", cacheKey);
} else {
MethodCache methodCache = getAnnotation(joinPoint, MethodCache.class);
centralizeCacheService.put(CACHE_NAME, methodCache.expire(), cacheKey, (Serializable) result);
}
return result;
}
}

/**
* 根据类名、方法名和参数值获取唯一的缓存键
* @return 格式为 "包名.类名.方法名.参数类型.参数值",类似 "your.package.SomeService.getById(int).123"
*/

private String getCacheKey(ProceedingJoinPoint joinPoint) {
return String.format("%s.%s",
joinPoint.getSignature().toString().split("\\s")[1], StringUtils.join(joinPoint.getArgs(), ","));
}

private <T extends Annotation> T getAnnotation(ProceedingJoinPoint jp, Class<T> clazz) {
MethodSignature sign = (MethodSignature) jp.getSignature();
Method method = sign.getMethod();
return method.getAnnotation(clazz);
}

}

要注意的是,目前该实现存在两个限制:

  1. 方法入参必须为基本数据类型或者字符串类型,使用其它引用类型的参数会导致缓存键构造有误;
  2. 方法返回值必须实现 Serializable 接口;

投入使用

例如,使用本注解为一个“按 ID 查询列表”的方法加上五分钟的缓存:

1
2
3
4
@MethodCache(expire = 300)
public List<String> listById(String id) {
// return a string list.
}

总结

使用 AOP 技术,你可以在一个地方定义所有的通用逻辑,并通过声明式(declaratively)的方式进行使用,而不必修改各个业务类的实现。这种代码解耦技术使得我们的业务代码更纯粹、仅包含所需的业务逻辑。相比继承(inheritance)和委托(delegation),AOP 实现相同的功能,代码会更整洁。

前言

本文总结出一些广受认可的编程最佳实践,用于解决特定领域的问题。

编程最佳实践

避免使用全局变量

在 JavaScript 所有的糟糕特性之中,最为糟糕的一个就是它对全局变量的依赖。JS 大神 Douglas Crockford 甚至称之为“毒瘤”。想象一下,一个全局变量可以被程序的任何部分在任意时间修改,将使得程序的行为变得极度复杂。可怕的全局变量还带来了以下问题:

  1. 命名冲突
  2. 代码的脆弱性
  3. 难以测试

共有三种方式定义全局变量,这些方式都是我们要避免的:

1
2
3
var foo = value;       // 1、在任何函数之外放置一个 var 语句
window.foo = value; // 2、直接给全局对象添加属性
foo = value; // 3、直接使用未经声明的变量,即隐式的全局变量。一般都是开发者忘记声明,这将导致查找 bug 非常困难

下面是一些解决办法:

零全局变量

如果你编写的是一段不会被其它脚本访问到的完全独立的脚本,可以使用一个立即执行的匿名函数来创建私有作用域

单全局变量

最小化使用全局变量的方法之一是为你的应用创建唯一一个全局变量,并将你所有的功能代码都挂载到这个全局对象上。这种做法既降低了模块之间发生冲突的可能,又能保证模块之间的正常通信。可以参考 JavaScript 模块模式

目前这种单全局变量模式已经在各种流行的库中广泛使用了:

  • jQuery 定义了两个全局对象,$jQuery。只有在 $ 被其它库使用了的情况下,为了避免冲突,才使用 jQuery
  • YUI 定义了唯一一个 YUI 全局对象。
  • Dojo 定义了唯一一个 dojo 全局对象。
  • ……

模块化

最后一种、也是最为推崇的做法是使用“模块化”方式组织代码:

不是你的对象不要动

JavaScript 独一无二之处在于任何东西都不是神圣不可侵犯的。默认情况下,你可以修改任何你可以触及的对象。解析器根本就不在乎这些对象是开发者定义的还是默认执行环境的一部分——只要是能访问到的对象都可以修改。如果你的代码没有创建这些对象,禁止修改它们,包括:

  • 原生对象(ObjectArray 等等);
  • 文档对象模型(DOM)(document 等等);
  • 浏览器对象模型(BOM)(window 等等);
  • 类库的对象($jQuery 等等)。

原则

不覆盖方法

覆盖方法将会导致所有依赖该方法的代码失效:

1
2
3
4
// 不好的写法 - 覆盖了 DOM 方法
document.getElementById = function() {
// 任意代码
};

不新增方法

新增方法将会导致未来潜在的命名冲突,因为一个对象此刻没有某个方法不代表它未来没有。更糟糕的是如果将来原生的方法和你新增的方法行为不一致,将会陷入一场代码维护的噩梦:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 不好的写法,在 DOM 对象上增加了方法
document.getElementsByClassName = function(classes) {
// 非原生实现。
// 该新增方法在 HTML 5 中被官方实现了,这将会导致所有依赖该方法的代码报错。
};
// 不好的写法,在原生对象上增加了方法
Array.prototype.reverseSort = function() {
return this.sort().reverse();
};
// 不好的写法,在库对象上增加了方法
$.doSomeThing = function() {
// 任意代码
};

不删除方法

删除方法将会导致所有依赖该方法的代码运行时错误。对于已发布的库来说,无用的方法应该被标识位“废弃”而不是直接删掉:

1
2
// 不好的写法 - 删除了 DOM 方法
document.getElementById = null;

解决办法

下面介绍一些解决方法:

继承

如果一种类型的对象已经做到了你想要的大多数工作,那么继承它然后再新增一些功能是最好的做法。JavaScript 中有两种基本的继承形式:

  • 基于对象的继承
  • 基于类型的继承

例如:

1
2
3
4
5
var MyError = function(message) {
this.message = message;
};

MyError.prototype = new Error(); // 基于类型的继承,继承自原生的 Error 类

门面模式

JavaScript 的继承有一些很大的限制,就是无法继承自 DOM 或 BOM 对象。解决办法是利用门面模式为这些已存在的对象创建一个新的接口,达到二次封装的效果。jQuery 和 YUI 的 DOM 接口都使用了门面模式。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 自定义一个 DOM 对象包装器
var DOMWrapper = function(element) {
this.element = element;
};

DOMWrapper.prototype = {
constructor: DOMWrapper,
addClass: function(className) {
this.element.className += ' ' + className;
},
remove: function() {
this.element.parentNode.removeChild(this.element);
}
};

// 用法
var wrapper = new DOMWrapper(document.getElementById("my-div"));
// 添加一个 className
wrapper.addClass("selected");
// 删除元素
wrapper.remove();

事件处理

解耦事件处理

事件处理常见的问题是将事件处理程序和业务逻辑紧紧耦合在一起,降低了代码的可维护性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 不好的写法
var handleClick = function(event) {
// DOM Level 2
event.preventDefault();
event.stopPropagation();

// 耦合业务逻辑
var popup = document.getElementById("popup");
popup.style.left = event.clientX + "px";
popup.style.top = event.clientY + "px";
popup.className = "reveal";
}

document.getElementById('btn-action')
.addEventListener("click", handleClick, false); // DOM Level 2

正确的做法应该是解耦事件处理程序和业务逻辑,提高代码的可维护性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 好的写法

// 事件处理程序,唯一能接触 event 对象的函数
var handleClick = function(event) {
// DOM Level 2
event.preventDefault();
event.stopPropagation();

showPopup(event.clientX, event.clientY);
},
// 抽取业务逻辑,与事件隔离,便于重用与测试
showPopup = function(x, y) {
var popup = document.getElementById("popup");
popup.style.left = x + "px";
popup.style.top = y + "px";
popup.className = "reveal";
}

document.getElementById('btn-action')
.addEventListener("click", handleClick, false); // DOM Level 2

可见,业务逻辑不应该依赖于 event 对象来完成功能,原因如下:

  • 好的 API 一定是对于期望和依赖都是透明的,因此方法接口应该表明哪些数据是必要的。将 event 对象作为参数并不能告诉你 event 的哪些属性是有用的,用来干什么?
  • 如果想测试这个方法,你必须构建一个 event 对象并作为参数传入。这迫使你关注方法内部实现,以确切地知道这个方法使用了哪些信息,这样才能正确地写出测试代码。

使用事件委托

关于“事件绑定(Event Binding)”和“事件委托(Event Delegation)”两种机制的区别在 本文 有详细的描述。简而言之,从“内存消耗”、“处理速度”、“新增元素的处理”三方面考虑,都更建议使用“事件委托”。下例演示了如何使用 jQuery 语法进行“事件委托”:

1
2
3
$('#list').on('click', 'li', function() {
//function code here.
});

#list 内任一 li 子元素被点击时,click 事件将冒泡到其父元素 #list 并触发 #list 的事件处理程序,即子元素的事件都委托给父元素进行处理。这种做法有利于提升性能,推荐使用。

UI 层保持松耦合

保持 Web UI 层的松耦合,以便在以下场景中调试代码,定位问题:

  • 当发生了文本或结构相关的问题,通过查找 HTML 即可定位;
  • 当发生了样式相关的问题,通过查找 CSS 即可定位;
  • 当发生了行为和交互相关的问题,通过查找 JavaScript 即可定位。

这种快速定位问题的能力是 Web 界面可维护性的核心关键。

将 JavaScript 从 CSS 中抽离

禁止使用 CSS 表达式(CSS Expression)。

1
2
3
4
// 不好的写法
.box {
width: expression(document.body.offsetWidth + "px");
}

CSS 表达式是 IE8 及更早版本中的一个特性,它允许你将 JavaScript 直接插入到 CSS 中,这样可以在 CSS 代码中直接执行运算或其它操作。但 CSS 表达式会带来两个问题:

  • 性能问题
  • 代码可维护性问题

将 CSS 从 JavaScript 中抽离

禁止在 JavaScript 脚本中直接操作 CSS 样式:

1
2
3
4
// 不好的写法
element.style.color = 'red';
element.style.left = '10px';
element.style.cssText = 'color: red; left: 10px';

当需要通过 JavaScript 来操作元素样式的时候,最佳方法是操作 CSS 的 className

1
2
element.className = 'className';    // 原生方法
$(element).addClass('className'); // jQuery

CSS 的 className 应该成为 CSS 和 JavaScript 之间通信的桥梁。JavaScript 不应当直接操作 CSS 样式,以便保持和 CSS 的松耦合。

将 JavaScript 从 HTML 中抽离

禁止在 HTML 标签中嵌入 JavaScript 脚本:

1
2
<!-- 不好的写法,不该直接为 HTML 标签的 on 属性挂载事件处理程序 -->
<button onclick="doSomeThing()" id="btn-action">Click Me</button>

这样会导致 HTML 页面和 JavaScript 脚本紧紧耦合。正确的做法应当是在外部脚本文件中添加事件处理程序:

1
2
3
4
5
var doSomeThing() {  }

document.getElementById('btn-action')
.addEventListener("click", doSomeThing, false); // DOM Level 2
$('#btn-action').click(doSomeThing); // jQuery

这种做法的优势在于,函数 doSomeThing() 的定义和事件处理程序的绑定都是在同一个文件中完成的。如果函数名称需要修改,则只需修改一个文件即可;如果点击发生时想额外做一些动作,也只需在一处做修改。

此外,不到迫不得已,不建议在 HTML 页面中嵌入 JavaScript 脚本:

1
2
3
4
<!--  不好的做法 -->
<script>
doSomeThing();
</script>

将 HTML 从 JavaScript 中抽离

不建议在 JavaScript 脚本文件中嵌入 HTML 操作:

1
2
3
// 不好的做法
var div = document.getElementById('my-div');
div.innerHTML = "<h3>Error</h3><p>Invalid e-mail address.</p>";

这样会导致 JavaScript 脚本和 HTML 标签紧紧耦合,从而降低了代码的可维护性,增加了跟踪文本和结构性问题的复杂度。正常来说,调试上述这段标签的典型方法,应当是先去浏览器调试工具中的 DOM 树中查找,然后打开页面的 HTML 源码对比其不同。一旦 JavaScript 脚本文件中做了除简单 DOM 操作之外的事情,如渲染标签,追踪 Bug 就变得很麻烦。因为脚本和标签都耦合成一坨了,让人望而却步。

HTML 文本和标签应该只存放于一个地方:可以控制你 HTML 代码的地方。最为推崇的做法是利用 JavaScript 模板引擎 解决这个问题。

项目中我引入了模板引擎 artTemplate 进行 HTML 渲染,并通过修改源码内置了两个常用的格式化工具:

详见 DEMO:finance-marketres-mobi\js\utility\util-demo.html

参考

  • 《编写可维护的 JavaScript》
  • 《JavaScript 高级程序设计》
  • 《JavaScript 权威指南》
  • 《JavaScript 语言精粹》