通过Chef安装MySQL无法访问的问题

通过自动化的配置管理工具可以简化项目的部署,我们尝试通过Chef来开通一个测试环境,其中用到了mysql。
做为最经常用到的技术栈之一,MySQL已经有了现成的cookbook,其安装也较为简单。然后安装成功之后,发现Web应用程序无法连接,然后通过命令行可以!

所以可以肯定是一个配置的问题,首先怀疑端口,但是检查后发现都是默认的3306端口。
然后怀疑数据库的连接地址,Web应用程序中的配置:localhost:3306。通过netstat -atn查看一下:看到这样一条:
tcp 0 0 10.29.1.25:3306 0.0.0.0:* LISTEN

问题就出在这里,10.29.1.25意味着只监听远程网络的端口调用,而不会监听本地端口。最简单的方法是把应用程序中的localhost:3306改成10.29.1.25:3306。

另外一个解决方法让mysql服务支持本地端口调用。查看/etc/mysql/my.cnf文件,打开可以看到这一行:
bind-address:10.29.1.25
如果把后面的地址改为:0.0.0.0,这意味着服务会绑定本机上所有的ip地址。如果机器有多个ip地址,这尤其适用。重启mysql服务,发现应用程序工作正常。

所以在使用chef开通mysql时,要注意绑定正确的ip地址。

Posted in Cloud | Tagged | Leave a comment

领域对象建模:保持数据的完整性

上一节中,我们介绍了通过createdAt和updatedAt两个字段可以简化领域对象的设计,因为它们捕捉到了最常用的数据,可以简化模型设计。但是常用不代表够用,数据的完整性是任何一个系统必须考虑的。请看:

小明设计了PromotionItem表示促销商品,并知道商品的最新状态和更新时间。春节过后,框框网的促销专区还算可以,不及预想的那么火爆,但是销量比之前上升明显。业务人员想从中做一些数据分析,比如商品从上架到卖出花费多长时间,从而知道哪些类型的商品好卖,以及跟折扣的关系。

这种情况下,小明是没法数据分析的,因为他的建模缺少了历史数据:商品几月几号上架,几月几号下架了,几月几号卖出了。这些实实在在发生过的数据,在PromotionItem一个对象中是很难完整保留的。即使采用之前的onlineTime/offlineTime/expireTime等多个字段也不行,因为商品有可能下架之后重新上架。

所以一旦业务有了新的需求,就无法满足需要。更要命的是,如果系统已经上线,业务再提出这些需求,我们就永远失去了这些历史数据。

这种情况我们叫做丢失了数据的完整性。

此时,我们在建模时,可以给关键领域对象设置关联的历史纪录(或者事件),比如PromotionItemHistory(当然也可以是PromotionItemEvent,一样的思路),把促销商品的每一个状态变化记录下来:

有了历史记录,就可以真正做到“手中有粮,心里不慌”了。这与徐昊在InfoQ上的文章《》其实是类似的思路:通过时标查找重要的领域对象,每个时标都会带来领域对象的变化,而且很多时候是状态的变化,并且把状态变化捕捉下来,以达到数据的完整性。

通过记录状态的变化,可以很容易和基于事件的,或者事件驱动的架构结合起来。比如,业务又来新需求了:每当有产品卖出的时候,业务的头想立刻收到邮件…

这个功能本身不难实现,但考虑到发送邮件不是业务的核心功能,而只是业务支撑,或者业务的增强,我们可以把发送邮件的代码与业务的核心代码分开。怎么分呢,发送邮件的Service可以监听商品的状态发生变化,并适时实现自己的功能,从时序图来说,就是从原来的:

替换为下面的实现:

通过修改,把短信和商品的紧耦合编程了松耦合。而且,随着业务量的增大,如果这里出现了性能瓶颈,可以考虑把同步的通知改成异步,甚至可以考虑引入消息中间件——当然,这取决于系统的规模和业务量。

Posted in Domain Model | Tagged | Leave a comment

领域对象建模:正确使用createdAt和updatedAt

编程时,大家会经常用到两个字段,createdAt和updatedAt。不但很多公司有这样的数据库规范要求,一些框架对它们也有天然的支持。不过你知道如何正确使用它们吗?别忙,先让我们看一个小例子。

框框网是一家刚起步的B2C网站,为了招揽人气,准备在春节前搞一个促销专区,吸引大家抢购。做为框框网的开发人员,小明参与了这个任务的开发。他收到的需求是:管理员可以在指定时间把促销商品上架,用户可以看到并购买。小明有一定的工作经验,稍做分析,就建立了名为“促销商品”(PromotionItem)的对象,包含两个状态:NEW和ONLINE,分别表示新建和已上架。并且设计了一个额外的字段onlineTime,如图所示:

这样,不但可以知道促销商品是否已经上架,如果需要的话,管理员也可以知道上架的时间。
开发完成以后,一切工作正常。几天后,小明接到业务的反馈:如果缺货或者万一出错,管理员可以把促销商品撤下来,即下架。小明挠了挠脑袋,添加了一个新的状态:OFFLINE来表示下架。但是如果不添加offlineTime,万一业务想查看商品啥时候下架怎么办?考虑来考虑去,又在数据库表上添加了一个字段offlineTime。同时他想到应该把商品的卖出时间也记录下来,干脆一块加上:

添加一个状态还比较容易,但添加字段就麻烦了,要写数据库脚本,而且根据框框网的开发规范,所有新增的SQL脚本都要汇报给开发的李头,再找DBA审查。这一来一去耽误不少功夫。不过好歹改完了,审查也通过了。小明暗自埋怨业务怎么不一次说清楚,折腾来折腾去的多麻烦。

又过了一段时间,小明突然被告知,原来做的上下架部分需要有点小改动,这些促销商品只能在指定期限内内卖,过了促销期就不再卖了。用户看不到,但是管理员可以在“过期的促销商品”里面看到。略加思索,小明心中有了数,应该如何修改。首先要添加一个新的状态EXPIRED,但是要不要加一个字段expiredTime呢?

他很纠结,时间字段越加越多,不但上面麻烦的流程要重新走一遍。万一业务再改怎么办?

其实他大可不必如此纠结,甚至这些字段全都不要。因为框框网的数据库规范上已经标明,任何数据库表都要有createdAt和updatedAt两个字段,只是他从未用过,曾经问过别的同事,但大家都说不出所以然来。我们使用这两个字段之后,发现促销商品简单多了:

在这个图中,createdAt表示促销商品的创建时间,updatedAt则根据状态而定:如果当前状态是已上架,则updatedAt表示上架时间;如果当前下架,updatedAt表示下架时间;如果当前过期,则表示过期时间。
通过两个字段,不但可以知道商品的最新状态,也可以知道发生的时间。小明之前纠结的问题解决了。即使业务有类似新的需求,都不必改动太多。

结论:促销商品是一个领域对象,从新建到上架,再到下架或者过期,它状态的迁移明确表明了业务的流程变化。对这类领域对象,我们关注它的状态,可以借用createdAt和updatedAt来捕捉一些重要的数据,从而简化领域模型。

Posted in Domain Model | Leave a comment

通过JavaScript Template简化前端JavaScript开发

在使用RESTFul架构的项目中,服务器端通过URI暴露可用的资源和状态表示,很多时候浏览器作为客户端来聚合不同的资源,这必然涉及到大量AJAX的使用,带来的问题就是前端JavaScript越来越重量级,不容易维护。下面通过一个例子来说明前端JavaScript膨胀的后果,以及如何通过JavaScript模板来进行简化。
某电子商务网站中,需要查看促销商品列表及其概要信息,假设对应的页面是这样的:

<div class="resource-holder" data-resource-uri="items/recommended">
<table id="recommendedItems" style="display:none">
<thead>
<tr>
<th>名称</th>
<th>价格</th>
<th>折扣</th>
<th>描述</th>
</tr>
</thead>
</table>
<div id="noRecommendedItems" style="display: none">
<h2>对不起, 当前没有促销商品</h2>
</div>
</div>

浏览器通过AJAX获取促销商品列表,并对数据进行处理:如果没有任何促销商品,显示一条简单信息;有至少一个,则显示对应的table:

$(function() {
$.getJSON($(".resource-holder").attr('data-resource-uri'), function(data) {
if(data.length > 0){
var recommendedItemsView = RecommendedItems.renderView(data);
$("#promotionalItems").append();
$("#promotionalItems").show();
}
else{
$("#noPromotionalItems").show();
}
});
});

其中生成表格中tbody部分的函数实现如下:

var RecommendedItems = (function() {
var renderView = function(items) {
var body = $('<tbody></tbody>');
var i = 0;
for (i; i < items.length; i++) {
var item = items[i];
var tds = '<td>' + item.name + "</td><td>" + formatter.asRMB(item.price) + "</td><td>" + formatter.asPercentage(item.discount) + "</td><td>" + item.description + "</td>";
body.append('<tr>' + tds + "</tr>");
}
return body;
};
return {
renderView:renderView
};
})();

在上面这段JavaScript中,renderView方法根据服务器返回的JSON数据,生成需要显示的tbody内容。由于资源中,价格是数字,折扣是小数,而前端页面显示时会进行一定的格式化,请看for循环里面的两个格式化方法:formatter.asRMB会把指定的金额转换成人民币的形式显示,如10000显示成¥10,000; formatter.asPercentage会把小数显示成分成,如0.40显示成40%。最终返回的tbody会被加入到table中。

由于在JavaScript中混入了HTML的标签,导致代码可读性很差;并且难以利用IDE的自动补全和检查功能,很容易出错。

这么费事,当然让人怀念服务器端的模板引擎。所幸,JavaScript也有模板引擎,比如JST(http://code.google.com/p/trimpath/wiki/JavaScriptTemplates)就很不错。在页面导入相应类库(如trimpath/template.js)后,我们的页面部分就可以改为:

<div data-resource-uri="items/recommended">
</div>

<textarea id="recommendedItemsTemplate" style="display:none">
{if model.items.length > 0}
<table id="recommendedItems">
<thead>
<tr>
<th>名称</th>
<th>价格</th>
<th>折扣</th>
<th>描述</th>
</tr>
</thead>
<tbody>
{for item in model.items}
<tr>
<td>item.name</td>
<td>item.price</td>
<td>item.discount</td>
<td>item.description</td>
</tr>
{/for}
</tbody>
</table>
{else}
<div id="noRecommendedItems">
<h2>对不起, 当前没有促销商品</h2>
</div>
{/if}

</textarea>

非常类似服务器端的模板引擎,只不过是在浏览器端JavaScript对textarea内的模板内容进行解析,并添加到resource-holder中。

$(function() {
$.getJSON($(".resource-holder").attr('data-resource-uri'), function(data) {
var formattedData = RecommendedItems.formatData(data);
var mergedTemplate = TrimPath.parseTemplate($("#recommendedItemsTemplate").html(), "#recommendedItemsTemplate").process({model:formattedData})
$(".resource-holder").append(mergedTemplate);
$("#recommendedItemsTemplate").remove();
});
});
var RecommendedItems = (function() {
var formatData = function(items) {
var rows = [];
var i = 0;
for (i; i < items.length; i++) {
rows.push({"name":items[i].planNumber,
"price":formatter.asRMB(items[i].price),
"discount":formatter.asPercentage(items[i].discount),
"description":items[i].description});
}
return rows;
};
return {
formatData:formatData
};
})();

修改之后,把页面呈现逻辑(AJAX调用)和格式化逻辑完全分开。FormatData只负责数据的格式化,比如金额显示成人民币的央视;折扣显示成百分比,因此职责清晰,易于编写单元测试;AJAX方法先调用formatData方法,然后把模板内容和数据传递给JST的模板引擎,并最终在callback方法中把生成的内容添加到合适的位置,属于页面呈现的内容,无需单元测试。

至此,我们可以进行一些简单的重构:
1. 在引入单独的模板vm文件,比如把recommendedItemsTemplate的所有内容抽取到单独的recommended_items_template.vm文件

<textarea id="recommendedItemsTemplate" style="display:none">
#include("recommended_items_template.vm")
</textarea>

抽取模板文件后,代码更清晰,而且模板vm可以被重用。

2. 封装JavaScript模板引擎方法,比如mergeAndShow方法,只需要提供两个参数:原始模板内容;格式化之后的数据。改方法调用模板引擎生成页面,并把页面内容以callback的方式传回:

jsTemplate.mergeAndShow($("#recommendedItemsTemplate").html(), formattedData, function(mergedTemplate) {
$(".resource-holder").append(mergedTemplate);
$("#recommendedItemsTemplate").remove();
});

与最初的方法相比,使用JavaScript模板并进行简单的重构之后,JavaScript代码和页面代码都比原来简洁的多,呈现和业务逻辑又了更好的分离,易于编写单元测试,代码质量也有了提高。

Posted in JavaScript | Tagged , , , , | Leave a comment

没有QA的团队

在胡凯近期组织的新任PM/TL经验交流会上,提到了什么是合适的leverage模型:给一年级PM/TL的信 - 交付与团队成长。碰巧Mike Gualtieri最近一篇文章中提到了没有QA的团队,让我觉得,没有QA的团队,不但靠谱,而且没准会有奇效。

没有QA,缺少了最后的保障,软件质量是否会一降千里?不会的。没有单独的QA,并不意味着没有人去做测试和质量保证,而是让每一名dev承担测试的责任。很多人的经验表明,开发过程很常见的一个问题是,Dev匆匆忙忙做完story,就认为任务已经完成,剩下都是QA的事情,即使出了问题,也是QA测试不周。在心理学上,这是一种典型的心理依赖。由于认为自己只承担开发责任,Dev会用很大的精力追求开发速度,这是导致缺陷过多、质量下降的主要原因。在没有QA的团队,Dev要100%对质量负责,这种责任的转移,让Dev去掉了侥幸心理,从而会重视每一个Story的质量。

另外,敏捷软件开发中,常提的一个概念是“关注交付”。软件被开发完成,没有任何价值,只有上线,并给客户带来价值,才算真正交付。这种说法很多人在很多场合都曾提过,但是“纸上得来终觉浅”,不亲身体验,很难体会其中含义。没有了QA的团队,会创造这样一个“绝知此事要躬行”的条件,让Dev的视角不再局限于开发,而延伸到软件生命周期中更接近交付的地方。这样的体验,会不断冲击Dev惯有的思维,让他们思考并理解交付的真正含义。

没有QA,很容易实现完全的自动化测试。自己完成的story被别人破坏怎么办?没有Dev愿意每次都手工回归测试,只能是用自动化。而Dev编写自动化测试,具有QA无可比拟的优势。举个常见的例子,很多项目会采用依赖注入机制,不光可以减少代码的耦合,同时可以提高项目的可测试性,非常易于编写单元测试。这对自动化的功能测试同样有效,Dev在基础架构上,在开发中时刻都会关注可测性,从而避免很多问题,比如我经历的一个案例:某Web开发团队,Dev只开发Story,QA则经常抱怨,Web页面非常难于编写自动化测试。

谈了一些没有QA的好处,我觉得它的局限在于:1.某些遗留系统中,对环境的依赖性比较强,很难做到完全的自动化测试,必须依赖QA的手工测试。相反可以从新项目开始尝试,引导甚至强制团队编写易于测试的程序。2.大团队怎么办?敏捷中,几十人的团队就算作大团队了,而我认为大团队是反敏捷的,应该拆成十人以下的小团队。小团队更具可控性,对软件质量会有更高的保障。

如果下半年有机会开始新项目,我一定要做这样的尝试:没有QA的团队,可以交付更高质量的软件。

Posted in Agile | Tagged , , , , , , , , | 4 Comments

ThoghtWorks technology radar

技术雷达(Technologhy Radar)ThoughtWorks定期发布的技术白皮书,它对当下流行的技术趋势采用多种维度评估,并以客观、中立的角度将其分为采用、试用、评估、保留四个级别,供IT企业的决策者进行参考。最新一期技术雷达发布于2011年1月。
目前ThoughtWorks技术雷达的发布格式为PDF,因此做了个小项目,提供Web版的技术雷达,如下图所示:

该项目的实现非常简单:HTML Canvas+JavaScript。首先尝试了Canvas原生API,但是无法忍受没有轮子的生活,转而采用类库RGraph。在RGraph支持的众多图表类型中,就是雷达图。唯一可惜的是,其扩展接口太少,只好修改了部分代码。
您可以点击这里查看该项目。如果您有问题或者有更好的创意,欢迎联系我。

Posted in HTML, JavaScript | Tagged , , , , | Leave a comment

magic cube on html5

在去年秋天ThoughtWorks举行的Html5/CSS3/JavaScript比赛中,我的参赛作品是魔方。当时对Canvas比较感兴趣,第一感觉就是做一个类似JFreeChart/Google Chart的图形类库,但是看过了RGraph之后,念头立刻打消。于是想到了做一个具有3D效果的魔方。
从创意到绘制草图,从学习API到算法,累计花了一天的时间,总算做出来了,下面是效果图:

虽然很简单的一个小游戏,但有一些值得总结的地方:
1.Canvas的原生API虽然简单,但是事件处理、效果等等都需要自己处理,因此代码量较大,而这正应是类库发挥作用的时候。对于复杂一些的应用,完全值得花时间去研究一下相关的类库,比如基于Canvas的RGraph图形类库、用于实现3D效果的WebGL、three.js等,甚至一些游戏引擎等等;
2. 魔方的算法非常有意思,如果以魔方的中间为原点,则所有的面都可以用-1、0、1三个值表示的坐标系标示出来。因此总觉得会有一个合适的矩阵可以表示,这样所有的旋转操作都可以看作矩阵的变换,算法也应该当想简单。但穷尽了我的矩阵知识,以失败告终,于是回到了最土的方式计算。
3. 随着Html5的日益流行,除了游戏,图片类库等等肯定大有用武之地。类似JFreeChart这样后台生成图片的方式已经很落伍;Google Chart也需要google服务器生成,而基于Canvas的图片很容易实现原来只有Flash能实现的缩放、旋转、事件处理等功能,所以不难理解除了RGraph,还已经有了不少这样的类库。

Posted in HTML | Tagged , , , , | Leave a comment

打造一致的工作环境

对强调消除浪费、提高效率的团队来说,保持一致的工作环境非常重要。工作环境不仅仅包括开发环境,也包括软件交付团队日常打交道的各种设施,比如工作目录、版本控制工具、敏捷团队的故事墙等等。这里,我们参考丰田精益的5S,对需要整理的环境进行一个总结,供大家搭建自己的工作环境时参考:

1、 工作目录

工作目录是指开发中用到的项目、软件、参考资料等在电脑硬盘中的存放位置。通常来说,我们要把它们放在相同的工作目录。

1.1 项目工程

比如都放到D:\work\myProject下,这样会利于继承开发环境(IDE)的管理。

1.2 相关资料,如果无法放到版本控制工具,也要放到相同的目录

比如下载的软件都放到D:\downloads目录下,第三方开源软件的源码都放到D:\3rdparty目录下。要在所有开发机器上保持一致,这样便于任何一位开发人员查找、共享相关资料。最重要的一点是要确保大小写也是一致的,相信我,这会给你少惹很多麻烦。

2、集成开发环境(IDE)

你是否有过这样的经验,其它机器上能编译通过的代码,本地编译不过;其它机器上成功的测试,本地确实失败的。查来查去,发现是环境或者配置问题。在越来越倚重IDE的今天,保持它的一致非常重要。

2.1 使用相同的IDE、插件和快捷键

对IDE,不同人会有不同的偏好,但对一个团队来说,还是要达成一致,选择最适合的一个。同时保持大家使用一样的快捷键和插件。否则,当大家交换结对时,需要在不同的上下文中切换,会影响开发效率。

2.2 使用相同的工程文件和模版文件

工程文件,比如IDEA的.ipr和iml文件等,用来保存项 目工程的组织结构、类库信息等等。而模板文件,比如文件模板和代码风格模板,这有利于整个团队保持一致的代码风格。把工程文件和模版文件保存在版本控制工具中,一旦某个人做了修改,每个机器上都可以及时更新,省去了单独配置的麻烦。

3、项目依赖的类库

同种类型的类库,只选择一种。有的时候,这很容易选择,比如用于测试的mock框架,如果同时使用EasyMock和Mockito,可以选择Mockito。而有时,类库并没有明显的优劣,或者项目中同一类库使用了不同的版本,可以由团队自行决定选择其一。
在有些遗留系统中,这种现象比较多,比如我正在工作的一个项目,是超过10年的遗留系统。在其众多项目中,同时用到了单元测试框架JUnit3/JUnit4/TestNG, Mock框架EasyMock/Mockito/PowerMock;Spring2.5/Spring3等等。可以通过技术白卡的方式,逐渐对它们进行清理。新项目这种情况会少一些,但出现了也要及时清理掉,除非你愿意它变成遗留系统。

4、清理版本控制工具

版本控制工具(VCS)是开发团队的必备工具,但时间长了,难免会出现垃圾:多余的文件、混乱的目录结构等等。有些冗余文件很好识别,比如编译或构建过程产生的中间文件,可以把它们删掉让版本控制工具瘦身。目录结构也可以重新清理,以让大家方便的查找。
除了这些,大家通常容易忽略的一个问题是,某些文件也应该放到VCS中,比如前面提到的工程文件和模版文件等等。通过VCS管理,可以让一个人的修改迅速应用到所有开发人员的机器上。
使用VCS时,有两个典型的坏味道:很多文件只在本地,被加入到忽略列表中;很多文件在本地做了修改,与VCS中的不同。一个典型的例子是用于测试的属性文件,由于需要配置不同的数据库连接信息,所以本地会与VCS中的不同。出现这些味道,就是项目可以优化的地方。

5、自动化脚本

好的团队善于利用脚本,把重复的操作自动化起来。它是面向整个团队的,所以不要出现某个人写后只有一个人偷偷在用的情况,要勇于向整个团队共享。并且用版本控制工具管理起来,利于以后脚本的修改。

6、 故事墙

敏捷团队用故事墙反映当前的交付状态,是团队的脸面所在,所以需要经常洗漱打扮、擦脂抹粉。

6.1 只保留当前迭代的卡片

就像你不会在夏天把棉衣挂在客厅里,同样也不要把上一个迭代甚至几个迭代前已经做完的卡片还留在故事墙上。

6.2 及时更新故事卡片的状态

关心的人通过故事墙了解到当前状态,所以故事墙的状态要是正确的、让人信任的。故事的状态发生变化,卡片要及时移动。

6.3 清理Block状态的卡片

Block状态的卡片一般是故事墙上寿命最长的卡片,有时会横跨几个迭代。可能半年过去了,对Block卡片仍然力所不及,也可能,大家根本忘了这张卡。所以定期检查,要么解决之,要么抛弃之。

6.4 装饰故事墙及状态墙

如果团队足够幸运的话,拥有一面很大的墙,除了常用的故事墙(故事卡片状态、燃尽图等等),还会把它用做团队内部交流的session墙、用于展示团队风貌的照片墙等等,对它们也要经常清理,维护内容的新鲜,会让团队成员保持很高的关注度。

7、清理代码

什么是简洁代码,如何重构,书籍或者文章多如牛毛,这里我只提3点。

7.1 删除被注释的代码

你可能把代码注释掉,希望有朝一日重新使用它们,或者你有时看到别人注释掉的代码。但事实上,维护的人换了一茬又一茬,已经没有人知道为什么被注释,这些代码也永无出头之日了。大胆地删掉它们吧,何况,我们有版本控制工具呢。

7.2 清理TODO项

在代码中加一些TODO项,你可能是想提醒自己或者别人,有一些事情还需要做。我当前的团队就存在这样的问题,大家写了一些TODO,但后来发现永远不会再理会它们。所以要全部找出来:要么做掉,要么删掉。7.3 命名中的大小写问题
即使团队有编码规范,有命名规范,大小写仍然是值得注意的问题。注意的好,能够明显减少代码的混乱程度。

8. 用户名密码

开发人员每天都要重复无数次的动作是:输入用户名、密码。这可能是在登录另外一台电脑、连接数据库、连接版本控制工具等等。如果其数量众多,每个人不得不记住很多用户名、密码。绝大多数时候,这不会涉及公司的核心机密,也不会带来明显的信息安全,不妨让整个团队使用相同的密码。

要打造一致的工作环境,我们常用的做法是,开发初期,首先搭建一个基本的开发环境(Dev Box),做成镜像之后,克隆到其它的开发机器。但是随着项目的进行,不同机器上的开发环境差别会越来越大,并逐渐积累出一些小的问题,如果不及时解决,对开发效率会造成较大的影响。所以,这不是一蹴而就的事情,需要在开发的过程中时刻注意,或者定期进行清理。相信在一致环境下工作的团队,会取得更高的效率

Posted in Agile | Tagged , , , , | Leave a comment

jQueryNumberLettersPlugin 0.1.1 Released

休了几天假,让我有时间写了一个jQuery的插件:NumberLetters。这个想法来自于前两篇博客:如何做一个完美的HTML INPUT输入框()和(),希望做成jQuery的插件后,能够有人使用。

在前面的例子中,通过在keyPress()事件判断该按键是否允许,从而拦截非法字符。但是该例子非常特殊,因为需求非常明确,可以预先判断哪些字符是允许的。想写一个通用的插件,需要考虑用户自定义的情况,比如用户想在INPUT输入框中限制输入IP地址,只靠字符本身不够,必须通过正则表达式。于是,插件的实现使用正则表达式做的,这样可以带来最大的灵活性。

既然决定用正则表达式,面临的一个直接问题是:需要知道该输入框的值是多少,因为用户可能在后面添加字符,有可能在中间插入字符,甚至可能选中后替换。而这只有keyDown()之后,用户输入的字符显示出来,才能得到,但此时已经无法阻止用户输入,体验会比较差。

经过考虑,采用的方法是keyPress()时,通过JavaScript Event来判断,该键按下之后,输入框的值可能变成什么。这用到了jQuery对event事件的封装,通过selectionStart和selectionEnd查看鼠标位置,是否选中字符,并把其变成按键对应的字符。经过实验,该方法还是可以的,于是jQueryNumberLettersPlugin 0.1.1 发布啦。

其用法如下:

$("input").letters();
$("input").ip();

更多信息,您可以在jQuery Plugin页面上查看,也可以在jQueryNumberLettersPlugin主页查看

Posted in JavaScript, jQuery Plugin | Tagged , , , , , , , , , , , , | Leave a comment

如何做一个完美的HTML INPUT输入框(下)

上篇中,我们列出了几种常见的实现方式,但由于其各有优缺点,要想做一个完美的INPUT输入框,只能自己写了。
基本思路是,当用户按下按键时,截获keyDown/keyUp或者keyPress事件,阻止其默认行为,这样就可以禁止用户输入非法字符。
为了后面理解清晰,我们把按键进行分类:第一类是特殊键,比如Shift、Control、Alt、Home、End等等。特殊键没有对应的Ascii代码,因此点击不会产生字符。第二类是普通键,有对应的Ascii码,又分为可以显示的字符键和不可以显示的控制键。字符键包括字母、数字以及特殊字符,控制键包括Backspace、回车键、Tab键。简单分类如下:

-特殊键 Special Key,比如Home、End、Insert、Delete以及Function Keys
+修饰键 (Shift、Control、Alt)
+导航键 Home、End、Page Up等)
+功能键 (F1、F2等)
+其它特殊键 (大写键、Insert、Delete等)
-普通键 Normal Key
+控制键 (非字符,不可显示,比如Backspace、回车、Tab等键)
-字符键 (字符,可显示)
+允许输入的字符 (本例中比如数字键、字母键)
+不允许输入的字符 (本例中的非数字字母键])

为了提高输入框的易用性,需要支持特殊键、普通键中的控制键,并允许用户输入指定的字符键,而不允许输入非法字符。因此需要识别不同的按键,JavaScript中event对象提供了有用的几个属性:event.keyCode、event.which和event.charCode。同时还有一些标记属性可以参考:event.shiftKey、event.ctrlKey、event.altKey、event.metaKey。有了它们,就可以判断按下的是特殊键还是普通键,是控制键还是字符键,从而可以决定是否禁止事件的默认行为。
现在一个主要的问题是,当按键按下时,不同浏览器的行为相差甚远,详情可以参考文章JavaScript Madness: Keyboard Events。不过闲话少说,在使用JQuery类库的基础上,还是亲自试一试吧,看看不同浏览器到底有什么区别:
(注:下表中的×代表不会产生keyPress事件;37|0|0分别表示keyCode、which和charCode;U代表undefined)

Example key Firefox WebKit(Safari&Chrome) Internet Explorer Opera 阻止显示?
特殊键(Home) × × × 36|0|U No
控制键(左键) 37|0|0 × × 37|0|U No
Legal Char(a) 0|97|97 97|97|97 97|97|U 97|97|U No
Illegal Char(%) 0|37|37 37|37|37 37|37|U 37|37|U Yes

从上表中不难发现,我们只需要检测到输入的非法字符,并禁止显示即可。而对所有其他的键需要支持。
测试中也发现了新的问题,某些控制键的keyCode和字符键的charCode值相同,比如左键和“%”的都是37,因此处理时需要留心,不要为禁止%,而禁掉了左键。这也是alphaNumeric插件的Firefox下的一个问题。

对上表的进一步分析表明,虽然不同浏览器的处理迥异,对经过JQuery的封装,event事件的which属性有一定的规律:对所有的特殊键和控制键,要么不产生keyPress事件,要么其值为0。对所有的可显示字符,which属性就是字符对应的ascii码。所以which属性可以很好的标示按键的种类。
有了这样的结论,代码也就不难写了(点击查看实际效果):

$("input[name='name']").bind('keypress', function(event) {
    if (!isSpecialKey(event.which) && !isNumberOrLetter(event.which)) {
         event.preventDefault();
    }
});
function isNumberOrLetter(key) {
    return (key >= 48 && key <= 57) || (key >= 65 && key <= 90) || (key >= 97 && key <= 122);
}
function isSpecialKey(key) {
    return key == 0 || key == 8 || key == 13 || key == 9;
}

要想在输入框中禁用输入法,加上这么一句:

$("input[name='name']").css('ime-mode', 'disabled');

只剩下对粘贴的处理了,因为用户有可能粘贴一些非法字符,而多数浏览器并不会为此出发keyPress事件。如果想禁用粘贴键,可以加入者些代码:

$("input[name='name']").bind("contextmenu", function(){
    return false;
});

我们的输入框基本完成了,说基本,是因为用户仍然可以点击菜单上的Edit->Paste粘贴非法字符。如果你对此很介意,可以考虑参考前面的方法,在keyUp时检测并删除非法字符。

好的,HTML INPUT输入框就做到这里。如果你发现本文有什么问题,或者有更好的实现方式,欢迎联系我:vinci.zhang@gmail.com。

Posted in HTML | Tagged , , , , , , | 2 Comments