连接协议(Protocol)对比

常见的几种连接协议:

  • SSH2(默认,相对于SSH1进行了加密算法的改进,使用最广泛)
  • SSH1
  • Telnet
  • Telnet/SSL
  • Rlogin
  • Serial
  • TAPI

在出现 SSH 之前,系统管理员需要登入远程服务器执行系统管理任务时,都是用 telnet 来实现的,telnet 协议采用明文密码传送,在传送过程中对数据也不加密,很容易被不怀好意的人在网络上监听到密码。

同样,在 SSH 工具出现之前 R 系列命令也很流行(由于这些命令都以字母 r 开头,故把这些命令合称为 R 系列命令,R 是 remote 的意思),比如 rexec 是用来执行远程服务器上的命令的,和 telnet 的区别是 telnet 需要先登录远程服务器再实行相关的命令,而 R 系列命令可以把登录和执行命令并登出系统的操作整合在一起。这样就不需要为在远程服务器上执行一个命令而特地登录服务器了。

SSH 全称 Secure SHell,顾名思义就是非常安全的 shell 的意思,SSH 协议是 IETF(Internet Engineering Task Force) 的 Network Working Group 所制定的一种协议。SSH 的主要目的是用来取代传统的 telnet 和 R 系列命令rloginrshrexec 等)远程登录和远程执行命令的工具,实现对远程登录和远程执行命令加密。防止由于网络监听而出现的密码泄漏,对系统构成威胁。

SSH 是一种加密协议,不仅在登录过程中对密码进行加密传送,而且对登录后执行的命令的数据也进行加密,这样即使别人在网络上监听并截获了你的数据包,他也看不到其中的内容。SSH 协议底层使用 TCP 协议,端口号 22。

OpenSSH 实现

OpenSSH 是 SSH 协议的免费开源实现。OpenSSH 套件使用 ssh 程序替代 telnetrlogin ,使用 scp 替代 rcp ,使用 sftp 替代 ftp 。OpenSSH 套件还包含一个 sshd 服务端程序(一个运行于服务端的独立守护进程(standalone daemon)),以及一系列 SSH 工具:

  • ssh-add Tool which adds private keys to the authentication agent.
  • ssh-agent An authentication agent that can store private keys.
  • ssh-keysign Helper program for host-based authentication.
  • ssh-keyscan Utility for gathering public host keys from a number of hosts.
  • ssh-keygen Key generation tool.

鉴权方式(Authentication)

不同于 telnet 只支持 Password 密码鉴权,SSH 同时支持以下几种鉴权方式:

  • Password(密码)
  • Public Key(公钥)
  • Keyboard Interactive(键盘交互)
  • GSSAPI

目前 SSH 最常用的鉴权方式有 Password 和 Public key 。

Public Key 鉴权方式

使用 ssh 进行远程登录时,由于默认使用的是 Password 鉴权方式,因此每次登录都需要输入密码,操作麻烦。下面介绍 Public Key 鉴权方式进行免密登录。

Public Key 非对称(asymmetric)鉴权认证使用一对相关联的 Key Pair(一个公钥 Public Key,一个私钥 Private Key)来代替传统的密码(Password)。顾名思义,Public Key 是用来公开的,可以将其放到 SSH 服务器自己的帐号中,而 Private Key 只能由自己保管,用来证明自己身份。

使用 Public Key 加密过的数据只有用与之相对应的 Private Key 才能解密。这样在鉴权的过程中,Public Key 拥有者便可以通过 Public Key 加密一些东西发送给对应的 Private Key 拥有者,如果在通信的双方都拥有对方的 Public Key(自己的 Private Key 只由自己保管),那么就可以通过这对 Key Pair 来安全地交换信息,从而实现相互鉴权。

Linux 系统中,这些文件分别存放在以下位置:

文件 描述
~/.ssh/id_rsa.pub 公钥(Public Key)
~/.ssh/id_rsa 私钥(Private Key)
~/.ssh/known_hosts 位于客户端的公钥列表文件,首次与目标主机建立 SSH 连接时,需要添加对方的公钥到这个文件以便后续通信
~/.ssh/authorized_keys 位于服务端的公钥列表文件,列出了所有被允许登录进来的可信公钥信息(Lists the public keys that are permitted for logging in)

公私钥创建与添加流程:

  1. 创建一对公私钥:
1
2
ssh-keygen -t rsa -C who@where
询问密码时,保持为空并回车
  1. 将私钥添加到 SSH 认证代理程序 ssh-agent
1
ssh-add ~/.ssh/id_rsa
  1. 将公钥上传到服务端,添加到被登录帐户可信列表文件
1
scp ~/.ssh/id_rsa.pub who@where:~/.ssh/authorized_keys
  1. 修改服务端文件权限:
1
2
chmod 700 ~/.ssh
chmod 600 ~/.ssh/*

之后再使用 ssh 登录时,客户端的 ssh-agent 会发送私钥去和服务端上的公钥做匹配,如果匹配成功就可以免密登录了。

参考

5 Unix / Linux ssh-add Command Examples to Add SSH Key to Agent

本文目的:

  • 能够理解事件绑定和事件委托两种机制的区别
  • 能够使用原生 API 和 jQuery API 两种方式进行事件委托

Native API

项目开发时遇到一个需求:修改页面中所有 A 链接的默认行为。

事件绑定

最开始想到了用“事件绑定”机制进行实现:

1
2
3
4
5
6
7
8
9
10
11
var showMessage = function(event) {
// 阻止 A 链接的默认行为(不进行跳转)
event.preventDefault();
// 仅弹窗显示链接的 href 属性
alert(event.currentTarget.href);
};

var links = document.getElementsByTagName('a');
for (var i = 0; i < links.length; i++) {
links[i].addEventListener('click', showMessage, false);
}

这种做法的问题是:如果页面中绑定了大量的事件处理程序,将直接影响页面的整体运行性能,因为:

  1. 函数即对象,对象越多,越占用内存,性能就越差。
  2. 事件绑定前,必须先找到指定的 DOM 元素。而 DOM 元素查找次数越多,页面的交互就绪时间就越长。

更麻烦的是,如果页面加载完后再次插入新元素,需要再次绑定事件处理程序,灵活性差:

1
2
3
4
5
var newLink = document.createElement("a");
newLink.innerHTML = 'Click Me';
newLink.href = 'http://localhost';
newLink.addEventListener('click', showMessage, false);
document.body.appendChild(newLink);

事件委托

利用事件委托机制可以同时解决上述两个问题。只需在 DOM 树中尽量最高的层次上添加一个事件处理程序,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var showMessage = function(event, target) {
// 阻止 A 链接的默认行为(不进行跳转)
event.preventDefault();
// 仅弹窗显示链接的 href 属性
alert(target.href);
},
// 递归查询指定父元素
findTarget = function(target, tagName) {
while (target.tagName && target.tagName !== tagName.toUpperCase()) {
target = target.parentNode;
}
return (target.tagName && target.tagName === tagName.toUpperCase()) ? target : null;
};

document.body.addEventListener('click', function(event) {
// 间接判断 A 链接是否被点击
var target = findTarget(event.target, 'a');
if (target) {
showMessage(event, target);
}
}, false);

由于所有 A 链接都是 body 元素的子节点,并且它们的事件都会冒泡,因此点击事件最终会被 body 上添加的事件处理程序所处理。代码重构后在以下方面提升了页面性能:

  • 由于 document 对象很快就可以访问,而且可以在页面生命周期的任何时点上为它添加事件处理程序(无需等待 DOMContentLoadedload 事件),因此只要可点击的元素呈现在页面上,就可以立即具备适当的功能。
  • 由于只添加一个事件处理程序,因此所需的 DOM 引用更少,整个页面占用的内存空间也更少。

此外,事件会关联到当前以及以后添加的子元素上面,可以避免反复为新元素绑定事件处理程序,可谓一劳永逸。

jQuery API

理解了两种机制的区别后,看看如何使用 jQuery 进行最快的实现:

on()

jQuery 1.7+ 推出了 on() 方法,其目的有两个:

  1. 统一接口

  2. 提高性能

用法如下:

  • 事件绑定:on(events,[data],fn) ,用于替换 bind()
  • 事件委托:on(events,[selector],[data],fn) ,用于替换 live()delegate() 。这里的 [selector] 参数很关键,起到了一个过滤器的效果,只有被选中元素的 子元素 才会触发事件。

on() 方法重构后的代码如下:

1
2
3
4
$('body').on('click', 'a', function(event) {
event.preventDefault();
alert(event.currentTarget.href);
});

可见,代码重构后非常简洁,推荐使用。

参考

本文目的:

  • 理解并能按需使用各种事件绑定 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 服务可供选择。不过对于国内用户来说,还是使用国内服务最快、最稳定。

本文演示如何动态加载脚本。即脚本在页面加载时不存在,但将来的某一时刻通过修改 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() 是一样的。

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

分支模型实践

创建特性分支

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

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

do something and commit...

$ git push origin feature-test

定期合并

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

决断代码

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

合并特性分支

开发完成后,开发人员(Developer)需要申请将特性分支合并回预发布分支,以便发布新版本。具体做法就是发起一次合并请求即可,项目管理员(Master)在代码审查通过后就会接受该次合并请求。

标记新版本

版本发布之后,Master 应该标记该新版本,以便后续回顾:

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

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

删除特性分支

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

1
2
$ git branch -d feature-test
$ git push --delete origin feature-test

总结

代码提交指南

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

分支管理指南

  • 主分支 master 、预发布分支 release-* 一般不提交代码,只合并代码。
  • 开发人员只需发起合并请求。合并特性代码到预发布分支的操作,由项目管理员负责。
  • 各特性分支要定期将 dev 分支合并进来,并在发起合并请求前将预发布分支也合并进来,避免后续处理合并请求时产生冲突,以减轻项目管理员的工作负担。
  • 发版之后,项目管理员要记得打 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 编号>

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

参考

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

这样的需求最适合用 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 全局对象。
  • ……

模块化

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

  • ECMAScript 6 可以使用最新的原生模块标准语法;
  • 否则可以使用主流的模块框架规范,如 AMD 和 CMD(AMD 和 CMD 的区别有哪些?)。

不是你的对象不要动

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 语言精粹》